Files
zk-data-agent/frontend/app/components/training/metrics-chart-panel.tsx
T
hupenglong1 f068311cac backend/frontend/runtime: 训练面板 / session 隔离 / agent runtime 调整
- backend/api/server.py:训练 step-detail / pipeline 卡片排序逻辑
- frontend:thread-list / thread / training-pipeline-panel / step-detail-sheet 配套调整,新增 metrics-chart-panel
- src/agent_*:tool spec / runtime / prompting 配套
- .gitignore 加 .logs/

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 11:43:10 +08:00

215 lines
5.9 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.
"use client";
import {
CartesianGrid,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { cn } from "@/lib/utils";
export type Round = {
iteration: number;
runDic: number;
label: string;
timestamp: string;
values: Record<string, number | null>;
};
export type MetricsHistory = {
metrics: string[];
rounds: Round[];
};
const METRIC_LABELS: Record<string, string> = {
req_set_car: "需求集合(车载)",
dapan_car: "大盘车载",
specific_test: "Specific Test",
triage_err_rate: "Triage 错误率",
bvt_nav: "导航BVT",
talkable_controllable: "可聊可控",
target_subset_count: "目标子集数量",
target_subset_wrong: "目标子集错误数",
target_subset_correct: "目标子集正确数",
target_subset_acc: "目标子集准确率",
icl_test_overall: "ICL 测试整体",
specific_test_overall: "专项测试整体",
overrall: "大盘集",
overrall_che_zai: "大盘集车载",
overrall_shou_ji: "大盘集手机",
overrall_dian_shi: "大盘集电视",
overrall_yan_jing: "大盘集眼镜",
overrall_you_ping: "大盘集有屏",
overrall_wu_ping: "大盘集无屏",
};
const LINE_COLORS = [
"#34d399", // emerald
"#38bdf8", // sky
"#a78bfa", // violet
"#fbbf24", // amber
"#f472b6", // rose
"#22d3ee", // cyan
"#a3e635", // lime
"#fb923c", // orange
];
function labelOf(key: string): string {
return METRIC_LABELS[key] ?? key;
}
function formatValue(v: number | null | undefined): string {
if (v === null || v === undefined || Number.isNaN(v)) return "—";
return v.toFixed(4);
}
function formatDelta(curr: number | null | undefined, base: number | null | undefined): {
text: string;
tone: "up" | "down" | "flat";
} {
if (
curr === null ||
curr === undefined ||
base === null ||
base === undefined ||
Number.isNaN(curr) ||
Number.isNaN(base)
) {
return { text: "—", tone: "flat" };
}
const d = curr - base;
if (Math.abs(d) < 1e-6) return { text: "±0", tone: "flat" };
const sign = d > 0 ? "+" : "";
return { text: `${sign}${d.toFixed(4)}`, tone: d > 0 ? "up" : "down" };
}
function chartRowsFor(history: MetricsHistory, key: string): Array<Record<string, unknown>> {
return history.rounds.map((r) => {
const v = r.values[key];
return {
label: r.label,
[key]: v === undefined || v === null ? null : v,
};
});
}
export function MetricsChartPanel({ history }: { history: MetricsHistory | null }) {
if (!history || history.rounds.length === 0) {
return (
<div className="flex h-full items-center justify-center text-zinc-500 text-xs">
</div>
);
}
const latest = history.rounds[history.rounds.length - 1];
const baseline = history.rounds[0];
const prev =
history.rounds.length >= 2 ? history.rounds[history.rounds.length - 2] : baseline;
const colorFor = (key: string): string => {
const idx = history.metrics.indexOf(key);
return LINE_COLORS[idx >= 0 ? idx % LINE_COLORS.length : 0];
};
return (
<div className="h-full min-h-0 overflow-y-auto pr-1">
<div className="grid gap-2 [grid-template-columns:repeat(auto-fit,minmax(280px,1fr))]">
{history.metrics.map((k) => {
const curr = latest.values[k];
const dBase = formatDelta(curr, baseline.values[k]);
const dPrev = formatDelta(curr, prev.values[k]);
const color = colorFor(k);
const rows = chartRowsFor(history, k);
return (
<div
key={k}
className="flex flex-col rounded-md border border-zinc-800 bg-zinc-900/40 p-2"
>
<div className="flex shrink-0 items-baseline justify-between gap-2">
<div className="flex items-center gap-1.5 min-w-0">
<span
className="size-1.5 shrink-0 rounded-full"
style={{ backgroundColor: color }}
/>
<span className="truncate font-medium text-[11px] text-zinc-200">
{labelOf(k)}
</span>
</div>
<span className="font-mono font-semibold text-[14px] text-zinc-50 leading-none">
{formatValue(curr)}
</span>
</div>
<div className="mt-0.5 flex shrink-0 gap-2 font-mono text-[10px]">
<span className={deltaClass(dBase.tone)}>
Δbase {dBase.text}
</span>
<span className={deltaClass(dPrev.tone)}>
Δprev {dPrev.text}
</span>
</div>
<div className="mt-1.5 h-32">
<ResponsiveContainer width="100%" height="100%">
<LineChart
data={rows}
margin={{ top: 4, right: 8, left: -12, bottom: 0 }}
>
<CartesianGrid stroke="#27272a" strokeDasharray="3 3" />
<XAxis
dataKey="label"
stroke="#a1a1aa"
tick={{ fontSize: 10 }}
/>
<YAxis
stroke="#a1a1aa"
tick={{ fontSize: 10 }}
tickFormatter={(v: number) =>
typeof v === "number" ? v.toFixed(4) : v
}
domain={["auto", "auto"]}
width={60}
/>
<Tooltip
contentStyle={{
background: "#18181b",
border: "1px solid #3f3f46",
borderRadius: 6,
fontSize: 11,
}}
labelStyle={{ color: "#e4e4e7" }}
itemStyle={{ color: "#e4e4e7" }}
formatter={(value: number, name: string) => [
typeof value === "number" ? value.toFixed(4) : value,
labelOf(name),
]}
/>
<Line
type="monotone"
dataKey={k}
stroke={color}
strokeWidth={1.5}
dot={{ r: 2.5 }}
activeDot={{ r: 4 }}
connectNulls={false}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
})}
</div>
</div>
);
}
function deltaClass(tone: "up" | "down" | "flat"): string {
if (tone === "up") return "text-emerald-400";
if (tone === "down") return "text-rose-400";
return "text-zinc-500";
}