diff --git a/langgraph/src/components/MessageBubble.tsx b/langgraph/src/components/MessageBubble.tsx index 17b9e18..6d8fb65 100644 --- a/langgraph/src/components/MessageBubble.tsx +++ b/langgraph/src/components/MessageBubble.tsx @@ -148,15 +148,40 @@ function CodeBlock({ ); } +const CONCLUSION_KEYWORDS = /总结|结论|建议|综上|总体|总的来说|核心|要点|小结/; +const FILLER_PATTERN = /^(根据|针对|您好|你好|好的|感谢|当然|如您所述|以下是|下面是)/; + function extractSummary(text: string): string { + const lines = text.split("\n").map((l) => l.trim()).filter(Boolean); + + // 1. 优先:含总结关键词的段落首句 + for (const line of lines) { + if (CONCLUSION_KEYWORDS.test(line) && line.length > 8 && !line.startsWith("#")) { + return line.replace(/^[*_#>\s-]+/, "").slice(0, 120); + } + } + + // 2. 次优先:Markdown 标题(## / ###)之后的第一个非空行 + for (let i = 0; i < lines.length; i++) { + if (/^#{2,3}\s/.test(lines[i]) && lines[i + 1]) { + const next = lines[i + 1].replace(/^[*_>\s-]+/, ""); + if (next.length > 8) return next.slice(0, 120); + } + } + + // 3. Fallback:跳过开场白,取第一个有实质内容的句子 const sentences = text.split(/(?<=[.。!!??])\s+/); + const meaningful = sentences.find((s) => s.length > 10 && !FILLER_PATTERN.test(s.trim())); + if (meaningful) return meaningful.trim().slice(0, 120); + + // 4. 最终 fallback return sentences.slice(0, 2).join(" ").trim(); } export default function MessageBubble({ content }: MessageBubbleProps) { const [summaryExpanded, setSummaryExpanded] = useState(false); const contentLines = content.split("\n").length; - const isLong = contentLines > 20; + const isLong = contentLines > 25 || content.length > 800; const summary = isLong ? extractSummary(content) : null; return ( @@ -166,7 +191,7 @@ export default function MessageBubble({ content }: MessageBubbleProps) {

{summary}…

+ )} + )} ); } + + diff --git a/langgraph/src/components/ThreadSidebar.tsx b/langgraph/src/components/ThreadSidebar.tsx index aa20170..ac7dde7 100644 --- a/langgraph/src/components/ThreadSidebar.tsx +++ b/langgraph/src/components/ThreadSidebar.tsx @@ -80,9 +80,9 @@ export function updateThreadLastActive(threadId: string) { } } -function formatTime(iso: string) { +function formatTime(isoOrMs: string | number) { try { - const d = new Date(iso); + const d = typeof isoOrMs === "number" ? new Date(isoOrMs) : new Date(isoOrMs); const now = new Date(); const diffMs = now.getTime() - d.getTime(); const diffHrs = diffMs / (1000 * 60 * 60); @@ -224,7 +224,7 @@ export function ThreadSidebar({ {displayLabel}

- {formatTime(t.created_at)} + {formatTime(getLastActive(t.thread_id, t.created_at))}