From cba2a726a23a09c6dd1f33cd5e08a515afc27693 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Mon, 22 Jun 2026 22:35:36 +0200 Subject: [PATCH] feat: aggregate artifacts from messages --- .../chat-artifacts/lib/collect-artifacts.ts | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/surfsense_web/features/chat-artifacts/lib/collect-artifacts.ts b/surfsense_web/features/chat-artifacts/lib/collect-artifacts.ts index 74da8fd0b..1e01fda94 100644 --- a/surfsense_web/features/chat-artifacts/lib/collect-artifacts.ts +++ b/surfsense_web/features/chat-artifacts/lib/collect-artifacts.ts @@ -1,4 +1,32 @@ -import type { ArtifactKind, ArtifactStatus } from "../model/artifact"; +import type { ThreadMessageLike } from "@assistant-ui/react"; +import { + ARTIFACT_TOOL_KINDS, + type ArtifactKind, + type ArtifactStatus, + type ChatArtifact, +} from "../model/artifact"; + +interface ToolCallPart { + type: "tool-call"; + toolCallId: string; + toolName: string; + args?: Record; + result?: unknown; +} + +function isToolCallPart(part: unknown): part is ToolCallPart { + return ( + typeof part === "object" && + part !== null && + (part as { type?: unknown }).type === "tool-call" && + typeof (part as { toolCallId?: unknown }).toolCallId === "string" && + typeof (part as { toolName?: unknown }).toolName === "string" + ); +} + +function asRecord(value: unknown): Record { + return typeof value === "object" && value !== null ? (value as Record) : {}; +} function firstString(...values: unknown[]): string | null { for (const value of values) { @@ -64,3 +92,48 @@ function describeArtifact( } } } + +/** + * Aggregate the deliverable artifacts referenced across a thread's messages. + * + * Scans assistant tool-call parts, keeps recognized deliverable tools, and + * dedupes by backing entity (so a regenerated report collapses to one entry, + * refreshed in place to keep chronological order). Errored deliverables are + * dropped — they have nothing to open or jump to. + */ +export function collectArtifacts(messages: readonly ThreadMessageLike[]): ChatArtifact[] { + const byKey = new Map(); + + for (const message of messages) { + if (message.role !== "assistant" || !Array.isArray(message.content)) continue; + + for (const part of message.content) { + if (!isToolCallPart(part)) continue; + const kind = ARTIFACT_TOOL_KINDS[part.toolName]; + if (!kind) continue; + + const args = asRecord(part.args); + const result = asRecord(part.result); + const { title, entityId, status } = describeArtifact( + kind, + args, + result, + part.result !== undefined + ); + if (status === "error") continue; + + const key = entityId != null ? `${kind}:${entityId}` : part.toolCallId; + byKey.set(key, { + key, + kind, + title, + status, + toolCallId: part.toolCallId, + entityId, + contentType: kind === "resume" ? "typst" : "markdown", + }); + } + } + + return Array.from(byKey.values()); +}