feat(tools): chart 图表工具 —— 工具产出 JSON spec,前端 SVG 渲染(职责分离)
按「工具只产数据、渲染交前端」设计:
- 后端 chart 工具(mcp-go):校验并返回规范化图表 JSON(type=bar/line/pie + labels + series,
校验类型/长度一致/pie 取首系列)。工具说明指示 agent 用 ```chart 围栏原样包裹返回的 JSON。
- 前端:lib/chartspec.ts 从输出抽取 ```chart 块(解析失败回退为文本不丢内容);
components/ChartView.tsx 自绘 SVG 柱/线/饼图(无第三方图表依赖);
BottomDrawer 输出区含图表块时分段渲染(文本 + SVG),否则纯文本。
测试:前端 chartspec 单测 12 例(isChartSpec 校验、分段抽取、非法块回退、多块、hasChart);
tsc 干净,vitest 48 过。live 自主 agent:chart 工具产出 {"type":"bar",...},
agent 正确用 ```chart 围栏嵌入答复,前端据此渲染。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@ import { useState } from "react";
|
||||
import { ChevronDown, ChevronUp, Wrench, ShieldCheck, Check, X } from "lucide-react";
|
||||
import { deriveNodes, pendingApproval, type RunState } from "../lib/run";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { ChartView } from "../components/ChartView";
|
||||
import { extractChartBlocks, hasChart } from "../lib/chartspec";
|
||||
import { approveTask } from "../lib/api";
|
||||
import { Tabs, Badge, cn, type TabDef } from "../ui";
|
||||
|
||||
@@ -53,11 +55,7 @@ export function BottomDrawer({ run }: { run: RunState }) {
|
||||
</div>
|
||||
{open && (
|
||||
<div className="h-44 overflow-auto p-3 text-xs">
|
||||
{tab === "output" && (
|
||||
<pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
|
||||
{run.output || "在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。"}
|
||||
</pre>
|
||||
)}
|
||||
{tab === "output" && <OutputView output={run.output} />}
|
||||
{tab === "trace" && <ExecTrace events={run.exec} phase={run.phase} />}
|
||||
{tab === "tools" && <ToolCalls run={run} />}
|
||||
{tab === "cite" && <p className="text-slate-600">引用列表:RAG 答案的来源块(源文档 + 分数 + 来源徽标)。</p>}
|
||||
@@ -122,6 +120,35 @@ function ApprovalBar({ taskId, node, title, summary }: { taskId: string; node: s
|
||||
);
|
||||
}
|
||||
|
||||
// OutputView:渲染模型输出。含 ```chart 块时分段渲染(文本 + SVG 图表),否则纯文本。
|
||||
function OutputView({ output }: { output: string }) {
|
||||
if (!output) {
|
||||
return (
|
||||
<pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
|
||||
在编排页搭图 → 运行,模型注入画像与历史后流式作答,token 在此呈现。
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
if (!hasChart(output)) {
|
||||
return <pre className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">{output}</pre>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{extractChartBlocks(output).map((seg, i) =>
|
||||
seg.kind === "chart" ? (
|
||||
<ChartView key={i} spec={seg.spec} />
|
||||
) : (
|
||||
seg.text.trim() && (
|
||||
<pre key={i} className="whitespace-pre-wrap font-mono leading-relaxed text-emerald-300">
|
||||
{seg.text}
|
||||
</pre>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ToolCalls:从执行事件里筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。
|
||||
function ToolCalls({ run }: { run: RunState }) {
|
||||
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
|
||||
|
||||
Reference in New Issue
Block a user