diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index b53f8a06..f6f724d7 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -158,6 +158,7 @@ button:disabled { cursor: default; opacity: .45; } .raw-evidence-receipt { margin: 0 var(--space-3) var(--space-3); border-top: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent); padding-top: var(--space-2); } .raw-evidence-receipt summary { padding: var(--space-1) 0; color: var(--color-ink-tertiary); font-size: 12px; } .raw-evidence-receipt pre { max-height: 180px; overflow: auto; margin: var(--space-2) 0 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; line-height: 1.5; } +.report-download-button { margin-top: var(--space-3); border: 1px solid var(--color-border); border-radius: var(--radius-md); background: var(--color-canvas-soft); color: var(--color-ink-secondary); cursor: pointer; padding: 7px 10px; font-size: 12px; } .message-markdown p { margin: 0 0 14px; } .message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; } .message-markdown li { margin: 6px 0; } diff --git a/frontend/src/components/chat-message-row.tsx b/frontend/src/components/chat-message-row.tsx index 3aded28a..149b714e 100644 --- a/frontend/src/components/chat-message-row.tsx +++ b/frontend/src/components/chat-message-row.tsx @@ -2,6 +2,7 @@ import { ChatMessageContent } from "@/components/chat-message-content"; import { ClaimBoundaryBadge } from "@/components/claim-boundary-badge"; import { EvidenceAuditPanel } from "@/components/evidence-audit-panel"; import type { ChatMessageView } from "@/lib/chat-message-view"; +import { consultationReportMarkdown, downloadMarkdownReport } from "@/lib/consultation-report-export"; export function AgentAvatar() { return ; @@ -25,7 +26,7 @@ export function ChatMessageRow({ message }: { readonly message: ChatMessageView {message.role === "assistant" ? ( message.state === "thinking" ?
{message.text}
} diff --git a/frontend/src/lib/consultation-report-export.ts b/frontend/src/lib/consultation-report-export.ts index 88aee559..7d12ad72 100644 --- a/frontend/src/lib/consultation-report-export.ts +++ b/frontend/src/lib/consultation-report-export.ts @@ -23,3 +23,15 @@ export function consultationReportMarkdown(input: { "本报告保留证据边界;未闭环内容不得包装成确定预测。", ].join("\n"); } + +export function downloadMarkdownReport(title: string, markdown: string) { + const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `${title.replace(/[\\/:*?"<>|]+/g, "-")}.md`; + document.body.append(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); +} diff --git a/frontend/tests/consultation-report-export.test.ts b/frontend/tests/consultation-report-export.test.ts index 90cb8ee6..f35c242e 100644 --- a/frontend/tests/consultation-report-export.test.ts +++ b/frontend/tests/consultation-report-export.test.ts @@ -1,6 +1,9 @@ import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import test from "node:test"; -import { consultationReportMarkdown } from "../src/lib/consultation-report-export.ts"; +import { consultationReportMarkdown, downloadMarkdownReport } from "../src/lib/consultation-report-export.ts"; +const rowSource = readFileSync(new URL("../src/components/chat-message-row.tsx", import.meta.url), "utf8"); +const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8"); test("exports latest consultation answer with workflow receipt and claim boundary", () => { const report = consultationReportMarkdown({ @@ -28,3 +31,10 @@ test("exports latest consultation answer with workflow receipt and claim boundar assert.match(report, /missing_layers: MEVG/); assert.match(report, /未闭环内容不得包装成确定预测/); }); + +test("assistant answer exposes a markdown report download button", () => { + assert.equal(typeof downloadMarkdownReport, "function"); + assert.match(rowSource, /下载本次报告/); + assert.match(rowSource, /downloadMarkdownReport/); + assert.match(globalStyles, /\.report-download-button/); +});