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
+25
-5
@@ -24,6 +24,23 @@ import ToolCallStatus from "@/components/ToolCallStatus.tsx";
|
|||||||
const LANGGRAPH_URL =
|
const LANGGRAPH_URL =
|
||||||
import.meta.env.VITE_LANGGRAPH_URL ?? "http://localhost:2024";
|
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 ────────────────────────────────────────────────────────────
|
// ─── Tool groups ────────────────────────────────────────────────────────────
|
||||||
const TOOL_GROUPS = [
|
const TOOL_GROUPS = [
|
||||||
{ key: "knowledge", label: "知识库", icon: BookOpen, tools: ["kb_search"] },
|
{ key: "knowledge", label: "知识库", icon: BookOpen, tools: ["kb_search"] },
|
||||||
@@ -419,9 +436,11 @@ function App() {
|
|||||||
{thread.messages.map((message, idx) => {
|
{thread.messages.map((message, idx) => {
|
||||||
// Render UI cards attached to this message
|
// Render UI cards attached to this message
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const uiItems = ((thread.values as any)?.ui ?? []).filter(
|
const uiItems = deduplicateUiItems(
|
||||||
(ui: UIMsgLocal) => ui.metadata?.message_id === message.id,
|
((thread.values as any)?.ui ?? []).filter(
|
||||||
) as UIMsgLocal[];
|
(ui: UIMsgLocal) => ui.metadata?.message_id === message.id,
|
||||||
|
) as UIMsgLocal[]
|
||||||
|
);
|
||||||
|
|
||||||
if (message.type === "human") {
|
if (message.type === "human") {
|
||||||
const humanText = typeof message.content === "string"
|
const humanText = typeof message.content === "string"
|
||||||
@@ -565,13 +584,14 @@ function App() {
|
|||||||
|
|
||||||
{/* Streaming UI cards not yet attached to a completed message */}
|
{/* Streaming UI cards not yet attached to a completed message */}
|
||||||
{thread.isLoading &&
|
{thread.isLoading &&
|
||||||
(thread.values?.ui ?? [])
|
deduplicateUiItems(
|
||||||
.filter((ui: UIMsgLocal) => {
|
(thread.values?.ui ?? []).filter((ui: UIMsgLocal) => {
|
||||||
const attachedToExisting = thread.messages.some(
|
const attachedToExisting = thread.messages.some(
|
||||||
(m) => m.id === ui.metadata?.message_id,
|
(m) => m.id === ui.metadata?.message_id,
|
||||||
);
|
);
|
||||||
return !attachedToExisting;
|
return !attachedToExisting;
|
||||||
})
|
})
|
||||||
|
)
|
||||||
.map((ui: UIMsgLocal) => (
|
.map((ui: UIMsgLocal) => (
|
||||||
<LoadExternalComponent
|
<LoadExternalComponent
|
||||||
key={ui.id}
|
key={ui.id}
|
||||||
|
|||||||
Reference in New Issue
Block a user