diff --git a/langgraph/src/main.tsx b/langgraph/src/main.tsx index 3c8f092..e4466dd 100644 --- a/langgraph/src/main.tsx +++ b/langgraph/src/main.tsx @@ -73,6 +73,16 @@ function deduplicateUiItems(items: UIMsgLocal[]): UIMsgLocal[] { }); } +// ─── Message deduplication by id ──────────────────────────────────────────── +// With streamSubgraphs:true, subgraph values events push messages that the +// parent graph later merges. The same message.id can appear multiple times in +// thread.messages. Keep only the last (most up-to-date) copy per id. +function deduplicateMessages(msgs: Message[]): Message[] { + const seen = new Map(); + msgs.forEach((m, i) => { if (m.id) seen.set(m.id, i); }); + return msgs.filter((m, i) => !m.id || seen.get(m.id) === i); +} + // ─── Tool groups ──────────────────────────────────────────────────────────── const TOOL_GROUPS = [ { key: "knowledge", label: "知识库", icon: BookOpen, tools: ["kb_search"] }, @@ -551,12 +561,13 @@ function App() { if (thread.messages.length > 0) { lastMessagesRef.current = thread.messages; } - const activeMessages: Message[] = + const activeMessages: Message[] = deduplicateMessages( thread.messages.length > 0 ? thread.messages : lastMessagesRef.current.length > 0 ? lastMessagesRef.current - : historicalMessages; + : historicalMessages + ); // ── Collect completed tool call IDs ───────────────────────────────────── const completedToolIds = new Set( @@ -677,11 +688,16 @@ function App() { )} {activeMessages.map((message, idx) => { - // Render UI cards attached to this message + // Render UI cards attached to this message. + // Apply deduplicateUiItems to the full ui array first so that cards + // pushed by both subgraph and parent-graph values events (same card_id) + // are collapsed before any per-message matching logic runs. // eslint-disable-next-line @typescript-eslint/no-explicit-any - const allUi: UIMsgLocal[] = thread.messages.length > 0 - ? ((thread.values as any)?.ui ?? []) - : historicalUi; + const allUi: UIMsgLocal[] = deduplicateUiItems( + thread.messages.length > 0 + ? ((thread.values as any)?.ui ?? []) + : historicalUi + ); // Match UI items by message_id; for subgraph messages where IDs // may not align, fallback: distribute orphaned cards by tool name. let matchedUi = allUi.filter(