Files
sundynix-agentix/sundynix-desktop/frontend/src/lib/chartspec.ts
T
Blizzard 592b2a3d97 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>
2026-06-24 17:24:41 +08:00

61 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 图表 spec 与「从模型输出里抽取 ```chart 代码块」的解析。
// 约定:chart 工具返回图表 JSONagent 在答复里用 ```chart 围栏原样包裹;前端据此渲染 SVG。
export interface ChartSeries {
name?: string;
data: number[];
}
export interface ChartSpec {
type: "bar" | "line" | "pie";
title?: string;
labels: string[];
series: ChartSeries[];
}
// Segment:把一段输出拆成「普通文本」与「图表」交错的有序片段,供输出区分别渲染。
export type Segment = { kind: "text"; text: string } | { kind: "chart"; spec: ChartSpec };
const chartFence = /```chart\s*\n([\s\S]*?)```/g;
// isChartSpec 校验解析出的对象是否是合法图表 spec(防脏数据炸渲染)。
export function isChartSpec(v: unknown): v is ChartSpec {
if (!v || typeof v !== "object") return false;
const o = v as Record<string, unknown>;
if (o.type !== "bar" && o.type !== "line" && o.type !== "pie") return false;
if (!Array.isArray(o.labels) || o.labels.length === 0) return false;
if (!Array.isArray(o.series) || o.series.length === 0) return false;
return (o.series as unknown[]).every(
(s) => s && typeof s === "object" && Array.isArray((s as ChartSeries).data),
);
}
// extractChartBlocks 把输出文本拆为 text / chart 片段(保持原顺序)。
// ```chart 块解析失败或非法 → 回退为普通文本,绝不丢内容。
export function extractChartBlocks(input: string): Segment[] {
if (!input) return [];
const segs: Segment[] = [];
let last = 0;
for (let m = chartFence.exec(input); m; m = chartFence.exec(input)) {
if (m.index > last) segs.push({ kind: "text", text: input.slice(last, m.index) });
let parsed: unknown;
try {
parsed = JSON.parse(m[1].trim());
} catch {
parsed = null;
}
if (isChartSpec(parsed)) segs.push({ kind: "chart", spec: parsed });
else segs.push({ kind: "text", text: m[0] }); // 解析失败:原样当文本,不丢
last = m.index + m[0].length;
}
chartFence.lastIndex = 0;
if (last < input.length) segs.push({ kind: "text", text: input.slice(last) });
return segs;
}
// hasChart 快速判断输出里是否含图表块(决定是否走分段渲染)。
export function hasChart(input: string): boolean {
const has = /```chart\s*\n/.test(input);
return has;
}