imeeting/frontend/src/pages/business/meetingAnalysis.ts

191 lines
5.7 KiB
TypeScript
Raw Normal View History

import type { MeetingVO } from "../../api/business/meeting";
export type AnalysisChapter = {
time?: string;
title: string;
summary: string;
};
export type AnalysisSpeakerSummary = {
speaker: string;
summary: string;
};
export type AnalysisKeyPoint = {
title: string;
summary: string;
speaker?: string;
time?: string;
};
export type MeetingAnalysis = {
overview: string;
keywords: string[];
chapters: AnalysisChapter[];
speakerSummaries: AnalysisSpeakerSummary[];
keyPoints: AnalysisKeyPoint[];
todos: string[];
};
export const ANALYSIS_EMPTY: MeetingAnalysis = {
overview: "",
keywords: [],
chapters: [],
speakerSummaries: [],
keyPoints: [],
todos: [],
};
const splitLines = (value?: string | null) =>
(value || "")
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const parseLooseJson = (raw?: string | null) => {
const input = (raw || "").trim();
if (!input) return null;
const tryParse = (text: string) => {
try {
return JSON.parse(text);
} catch {
return null;
}
};
const direct = tryParse(input);
if (direct && typeof direct === "object") return direct;
const fenced = input.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
if (fenced) {
const fencedParsed = tryParse(fenced);
if (fencedParsed && typeof fencedParsed === "object") return fencedParsed;
}
const start = input.indexOf("{");
const end = input.lastIndexOf("}");
if (start >= 0 && end > start) {
const wrapped = tryParse(input.slice(start, end + 1));
if (wrapped && typeof wrapped === "object") return wrapped;
}
return null;
};
const extractSection = (markdown: string, aliases: string[]) => {
const lines = markdown.split(/\r?\n/);
const lowerAliases = aliases.map((item) => item.toLowerCase());
const cleanHeading = (line: string) => line.replace(/^#{1,6}\s*/, "").trim().toLowerCase();
let start = -1;
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index].trim();
if (!line.startsWith("#")) continue;
const heading = cleanHeading(line);
if (lowerAliases.some((alias) => heading.includes(alias))) {
start = index + 1;
break;
}
}
if (start < 0) return "";
const buffer: string[] = [];
for (let index = start; index < lines.length; index += 1) {
const line = lines[index];
if (line.trim().startsWith("#")) break;
buffer.push(line);
}
return buffer.join("\n").trim();
};
const parseBulletList = (content?: string | null) =>
splitLines(content)
.map((line) => line.replace(/^[-*•\s]+/, "").replace(/^\d+[.)]\s*/, "").trim())
.filter(Boolean);
const parseOverviewSection = (markdown: string) =>
extractSection(markdown, ["全文概要", "概要", "摘要", "概览"]) || markdown.replace(/^---[\s\S]*?---/, "").trim();
const parseKeywordsSection = (markdown: string, tags: string) => {
const section = extractSection(markdown, ["关键词", "关键字", "标签"]);
const fromSection = parseBulletList(section)
.flatMap((line) => line.split(/[,、/]/))
.map((item) => item.trim())
.filter(Boolean);
if (fromSection.length) {
return Array.from(new Set(fromSection)).slice(0, 12);
}
return Array.from(new Set((tags || "").split(",").map((item) => item.trim()).filter(Boolean))).slice(0, 12);
};
export const buildMeetingAnalysis = (
sourceAnalysis: MeetingVO["analysis"] | undefined,
summaryContent: string | undefined,
tags: string,
): MeetingAnalysis => {
const parseStructured = (parsed: Record<string, any>): MeetingAnalysis => {
const chapters = Array.isArray(parsed.chapters) ? parsed.chapters : [];
const speakerSummaries = Array.isArray(parsed.speakerSummaries) ? parsed.speakerSummaries : [];
const keyPoints = Array.isArray(parsed.keyPoints) ? parsed.keyPoints : [];
const todos = Array.isArray(parsed.todos)
? parsed.todos
: Array.isArray(parsed.actionItems)
? parsed.actionItems
: [];
return {
overview: String(parsed.overview || "").trim(),
keywords: Array.from(
new Set((Array.isArray(parsed.keywords) ? parsed.keywords : []).map((item) => String(item).trim()).filter(Boolean)),
).slice(0, 12),
chapters: chapters
.map((item: any) => ({
time: item?.time ? String(item.time).trim() : undefined,
title: String(item?.title || "").trim(),
summary: String(item?.summary || "").trim(),
}))
.filter((item: AnalysisChapter) => item.title || item.summary),
speakerSummaries: speakerSummaries
.map((item: any) => ({
speaker: String(item?.speaker || "").trim(),
summary: String(item?.summary || "").trim(),
}))
.filter((item: AnalysisSpeakerSummary) => item.speaker || item.summary),
keyPoints: keyPoints
.map((item: any) => ({
title: String(item?.title || "").trim(),
summary: String(item?.summary || "").trim(),
speaker: item?.speaker ? String(item.speaker).trim() : undefined,
time: item?.time ? String(item.time).trim() : undefined,
}))
.filter((item: AnalysisKeyPoint) => item.title || item.summary),
todos: todos.map((item: any) => String(item).trim()).filter(Boolean).slice(0, 10),
};
};
if (sourceAnalysis) {
return parseStructured(sourceAnalysis as Record<string, any>);
}
const raw = (summaryContent || "").trim();
if (!raw && !tags) return ANALYSIS_EMPTY;
const loose = parseLooseJson(raw);
if (loose) {
return parseStructured(loose);
}
return {
overview: parseOverviewSection(raw),
keywords: parseKeywordsSection(raw, tags),
chapters: [],
speakerSummaries: [],
keyPoints: [],
todos: [],
};
};