f068311cac
- 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>
215 lines
5.9 KiB
TypeScript
215 lines
5.9 KiB
TypeScript
"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";
|
||
}
|