feat(R3): deduplicate UI cards by card_id (loading → complete merge)
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:
co-authored by
Claude Sonnet 4.6
parent
c4c5c9916e
commit
3b1ce32bc4
+24
-4
@@ -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(
|
||||
const uiItems = deduplicateUiItems(
|
||||
((thread.values as any)?.ui ?? []).filter(
|
||||
(ui: UIMsgLocal) => ui.metadata?.message_id === message.id,
|
||||
) as UIMsgLocal[];
|
||||
) 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}
|
||||
|
||||
Reference in New Issue
Block a user