feat(R3): deduplicate UI cards by card_id (loading → complete merge)
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 27s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 46s

Cards with the same card_id in props are deduplicated to show only the
last (complete) version, replacing any earlier loading skeleton cards.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-11 22:33:45 +08:00
co-authored by Claude Sonnet 4.6
parent c4c5c9916e
commit 3b1ce32bc4
+25 -5
View File
@@ -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<string, number>();
// 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) => (
<LoadExternalComponent
key={ui.id}