Files
zk-data-agent/frontend/app/components/training/metrics-chart-panel.tsx
T
wangsenhao 85ce8c0e69 fix: session resume, config_set schema, metrics chart, subagent logging
- Fix session resume: remove messages.length>1 guard so sessionStorage
  ID is used as resumeSessionId on first message (prevents new session
  creation when user sends "继续")
- Fix config_set tool schema: add missing `items: {}` to array type in
  oneOf (Azure OpenAI strict validation rejects it otherwise)
- Metrics chart: use actual target set filename from trigger text
  instead of generic "target_subset" key
- Add sub-agent skill call logging to agent_runtime.py (full
  prompt/response, no truncation)
- Various metric enrichment and dedup fixes in server.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-27 17:02:24 +08:00

220 lines
6.0 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",
target_subset: "目标集合",
icl_test: "ICL Test",
overall: "大盘整体",
triage_err_rate: "Triage 错误率",
bvt_nav: "导航BVT",
talkable_controllable: "可聊可控",
badcase_2025: "Badcase 2025",
autotask: "自动任务",
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";
}