diff --git a/langgraph/src/main.tsx b/langgraph/src/main.tsx index 0848b92..d4eaa95 100644 --- a/langgraph/src/main.tsx +++ b/langgraph/src/main.tsx @@ -24,6 +24,23 @@ import ToolCallStatus from "@/components/ToolCallStatus.tsx"; const LANGGRAPH_URL = import.meta.env.VITE_LANGGRAPH_URL ?? "http://localhost:2024"; +// ─── Card deduplication by card_id ────────────────────────────────────────── +// When the backend pushes loading then complete cards with the same card_id, +// keep only the last (most recent) card per card_id. Cards without card_id pass through. +function deduplicateUiItems(items: UIMsgLocal[]): UIMsgLocal[] { + const seen = new Map(); + // Walk forward to find the last index for each card_id + items.forEach((item, idx) => { + const cardId = item.props?.card_id as string | undefined; + if (cardId) seen.set(cardId, idx); + }); + return items.filter((item, idx) => { + const cardId = item.props?.card_id as string | undefined; + if (!cardId) return true; + return seen.get(cardId) === idx; + }); +} + // ─── Tool groups ──────────────────────────────────────────────────────────── const TOOL_GROUPS = [ { key: "knowledge", label: "知识库", icon: BookOpen, tools: ["kb_search"] }, @@ -419,9 +436,11 @@ function App() { {thread.messages.map((message, idx) => { // Render UI cards attached to this message // eslint-disable-next-line @typescript-eslint/no-explicit-any - const uiItems = ((thread.values as any)?.ui ?? []).filter( - (ui: UIMsgLocal) => ui.metadata?.message_id === message.id, - ) as UIMsgLocal[]; + const uiItems = deduplicateUiItems( + ((thread.values as any)?.ui ?? []).filter( + (ui: UIMsgLocal) => ui.metadata?.message_id === message.id, + ) as UIMsgLocal[] + ); if (message.type === "human") { const humanText = typeof message.content === "string" @@ -565,13 +584,14 @@ function App() { {/* Streaming UI cards not yet attached to a completed message */} {thread.isLoading && - (thread.values?.ui ?? []) - .filter((ui: UIMsgLocal) => { + deduplicateUiItems( + (thread.values?.ui ?? []).filter((ui: UIMsgLocal) => { const attachedToExisting = thread.messages.some( (m) => m.id === ui.metadata?.message_id, ); return !attachedToExisting; }) + ) .map((ui: UIMsgLocal) => (