From 4676361b1566b1b5c60a70a5ac11341c1a163f9d Mon Sep 17 00:00:00 2001 From: Kerwin Zheng <1747269691@qq.com> Date: Sun, 3 May 2026 15:35:26 +0800 Subject: [PATCH] fix(chat): show inline report artifacts - Derive report round artifacts from markdown-content parts - Preserve inline report markdown on artifact selection - Cover report artifact hydration and assistant action rendering Co-authored-by: Codex --- app/chat/[id]/page.tsx | 3 ++ ...assistant-message-round-artifacts.test.tsx | 38 +++++++++++++++++++ lib/chat/workspace-hydration.test.ts | 24 ++++++++++++ lib/chat/workspace-hydration.ts | 35 +++++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/app/chat/[id]/page.tsx b/app/chat/[id]/page.tsx index 62833ac..8a5c069 100644 --- a/app/chat/[id]/page.tsx +++ b/app/chat/[id]/page.tsx @@ -11,6 +11,7 @@ import { showChartWorkspaceAtom, showDatasetWorkspaceAtom, showFileWorkspaceAtom, + showMarkdownWorkspaceAtom, vncUrlAtom, workspaceChartAtom, workspaceDatasetAtom, @@ -346,6 +347,8 @@ const ChatPage = ({ params }: ChatPageProps) => { filename: artifact.filename, }); } + } else if (artifact.markdownContent) { + jotaiStore.set(showMarkdownWorkspaceAtom, artifact.markdownContent); } else if (artifact.fileId && artifact.filename) { jotaiStore.set(showFileWorkspaceAtom, { downloadUrl: getFileDownloadUrl( diff --git a/app/chat/components/assistant-message-round-artifacts.test.tsx b/app/chat/components/assistant-message-round-artifacts.test.tsx index 082c4f0..5770689 100644 --- a/app/chat/components/assistant-message-round-artifacts.test.tsx +++ b/app/chat/components/assistant-message-round-artifacts.test.tsx @@ -79,6 +79,21 @@ const assistantMessageWithMultipleCharts = { ], } as unknown as UIMessage; +const assistantMessageWithInlineReport = { + id: "assistant-round-inline-report", + role: "assistant", + parts: [ + { + type: "text", + text: "Report completed", + }, + { + type: "markdown-content", + content: "# Analysis Report\n\nRevenue increased by 12%.", + }, + ], +} as unknown as UIMessage; + describe("AssistantMessage round artifact actions", () => { it("renders per-category buttons with icon prefix and opens the selected artifact", () => { const onSelectRoundArtifact = jest.fn(); @@ -147,4 +162,27 @@ describe("AssistantMessage round artifact actions", () => { expect(screen.getByText("CSV 312 Bytes")).toBeInTheDocument(); }); + + it("renders inline markdown report as a round artifact action", () => { + const onSelectRoundArtifact = jest.fn(); + + render( + , + ); + + fireEvent.click(screen.getByRole("button", { name: /Report/i })); + + expect(onSelectRoundArtifact).toHaveBeenCalledWith( + expect.objectContaining>({ + category: "report", + label: "Analysis Report", + markdownContent: "# Analysis Report\n\nRevenue increased by 12%.", + }), + ); + }); }); diff --git a/lib/chat/workspace-hydration.test.ts b/lib/chat/workspace-hydration.test.ts index 6647236..2bfa6e9 100644 --- a/lib/chat/workspace-hydration.test.ts +++ b/lib/chat/workspace-hydration.test.ts @@ -325,4 +325,28 @@ describe("workspace hydration", () => { ); expect(snapshot.view).toBe("dataset"); }); + + it("derives report artifacts from inline markdown content", () => { + const assistantMessage: UIMessage = { + id: "assistant-inline-report", + role: "assistant", + parts: [ + { + type: "markdown-content", + content: "# Analysis Report\n\nRevenue increased by 12%.", + } as unknown as UIMessage["parts"][number], + ], + }; + + const artifacts = deriveRoundArtifactsFromMessage(assistantMessage); + + expect(artifacts.report).toHaveLength(1); + expect(artifacts.report[0]).toEqual( + expect.objectContaining({ + category: "report", + label: "Analysis Report", + markdownContent: "# Analysis Report\n\nRevenue increased by 12%.", + }), + ); + }); }); diff --git a/lib/chat/workspace-hydration.ts b/lib/chat/workspace-hydration.ts index 4663e4a..70e8fcc 100644 --- a/lib/chat/workspace-hydration.ts +++ b/lib/chat/workspace-hydration.ts @@ -20,6 +20,7 @@ type WorkspaceRoundArtifact = { downloadUrl?: string; extension?: string; chart?: Record; + markdownContent?: string; title?: string; toolCallId?: string; }; @@ -100,6 +101,16 @@ const getFileDownloadUrl = ( return undefined; }; +const getMarkdownReportLabel = (content: string): string => { + const heading = content + .split("\n") + .map((line) => line.trim()) + .find((line) => line.startsWith("#")); + const normalizedHeading = heading?.replace(/^#+\s*/, "").trim(); + + return normalizedHeading || "Generated report"; +}; + // Resolve dataset attachments from user metadata even when preview is omitted. const resolveDatasetAttachment = ( attachments: ChatAttachment[] | undefined, @@ -284,6 +295,25 @@ const deriveRoundArtifactsFromMessage = ( continue; } + if ( + partRecord.type === "markdown-content" && + typeof partRecord.content === "string" && + partRecord.content.trim().length > 0 + ) { + const createdAt = nextCreatedAt(); + const markdownContent = partRecord.content.trim(); + const label = getMarkdownReportLabel(markdownContent); + pushRoundArtifact(artifacts, { + id: `${message.id}-report-markdown-${createdAt}`, + category: "report", + createdAt, + label, + markdownContent, + title: label, + }); + continue; + } + if ( typeof partRecord.type !== "string" || !partRecord.type.startsWith("tool-") @@ -539,6 +569,11 @@ const deriveWorkspaceSnapshotFromMessages = ( } if (artifact.category === "report") { + if (artifact.markdownContent) { + snapshot.markdownContent = artifact.markdownContent; + markViewUpdated("markdown"); + continue; + } if (!artifact.fileId || !artifact.filename) { continue; }