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:
hupenglong1
2026-05-25 11:43:10 +08:00
parent 57f5b60a3e
commit f068311cac
17 changed files with 1571 additions and 270 deletions
@@ -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";
}