From 73a7175051563a0abf8bdee02c81356dc9c91c15 Mon Sep 17 00:00:00 2001 From: gongzhiyong Date: Sun, 12 Apr 2026 15:44:58 +0800 Subject: [PATCH] feat: cancelling status, ThreadSidebar lastActive time, MessageBubble smart summary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 点5: cancelling 独立状态 UI,用轮询替换 300ms setTimeout,StreamStatusBar 加橙色视觉 - 点6: ThreadSidebar formatTime 支持毫秒时间戳,时间展示改用 lastActive - 点7: MessageBubble 智能摘要(关键词优先/标题次优/跳开场白),折叠阈值提升至25行/800字,展开后可收起 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- langgraph/src/components/MessageBubble.tsx | 44 +++++++++++++++++++++- langgraph/src/components/ThreadSidebar.tsx | 6 +-- langgraph/src/main.tsx | 44 +++++++++++++++------- 3 files changed, 75 insertions(+), 19 deletions(-) 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))}