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>
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"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";
|
||||
}
|
||||
Reference in New Issue
Block a user