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:
@@ -8,20 +8,25 @@ import {
|
||||
ClipboardListIcon,
|
||||
ClockIcon,
|
||||
DnaIcon,
|
||||
ExternalLinkIcon,
|
||||
FileTextIcon,
|
||||
FlaskConicalIcon,
|
||||
GraduationCapIcon,
|
||||
HourglassIcon,
|
||||
Loader2Icon,
|
||||
type LucideIcon,
|
||||
Maximize2Icon,
|
||||
MicroscopeIcon,
|
||||
Minimize2Icon,
|
||||
RefreshCwIcon,
|
||||
ShieldCheckIcon,
|
||||
TrendingUpIcon,
|
||||
UserCheckIcon,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
MetricsChartPanel,
|
||||
type MetricsHistory,
|
||||
} from "@/components/training/metrics-chart-panel";
|
||||
import { StepDetailSheet } from "@/components/training/step-detail-sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -103,6 +108,7 @@ type PipelinePayload = {
|
||||
items: PipelineItem[];
|
||||
in_flight?: InFlight | null;
|
||||
logs: LogLine[];
|
||||
metrics_history?: MetricsHistory | null;
|
||||
};
|
||||
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
@@ -145,7 +151,9 @@ export function TrainingPipelinePanel({
|
||||
key: string;
|
||||
title: string;
|
||||
status: string;
|
||||
runId?: string;
|
||||
} | null>(null);
|
||||
const [bottomTab, setBottomTab] = useState<"log" | "metrics">("log");
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -182,7 +190,7 @@ export function TrainingPipelinePanel({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 flex-1 flex-col overflow-hidden border-r bg-muted/20">
|
||||
<div className="relative flex min-w-0 flex-1 flex-col overflow-hidden border-r bg-muted/20">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-6 py-5">
|
||||
<HeaderRow status={data?.status ?? null} loading={loading} />
|
||||
<KpiRow kpis={data?.kpis ?? []} />
|
||||
@@ -222,6 +230,7 @@ export function TrainingPipelinePanel({
|
||||
key: stripNamespace(card.key),
|
||||
title: card.title,
|
||||
status: card.status,
|
||||
runId: item.run_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -238,6 +247,7 @@ export function TrainingPipelinePanel({
|
||||
key: stripNamespace(item.key),
|
||||
title: item.title,
|
||||
status: item.status,
|
||||
runId: item.run_id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
@@ -250,11 +260,16 @@ export function TrainingPipelinePanel({
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<LegendBar />
|
||||
<LogStream logs={data?.logs ?? []} />
|
||||
<BottomPanel
|
||||
logs={data?.logs ?? []}
|
||||
history={data?.metrics_history ?? null}
|
||||
tab={bottomTab}
|
||||
onTabChange={setBottomTab}
|
||||
/>
|
||||
<StepDetailSheet
|
||||
sessionId={sessionId}
|
||||
stepKey={openCard?.key ?? null}
|
||||
runId={openCard?.runId ?? null}
|
||||
stepTitle={openCard?.title}
|
||||
stepStatus={openCard?.status}
|
||||
open={openCard !== null}
|
||||
@@ -774,96 +789,165 @@ function MiniStatus({ status }: { status: CardStatus }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LegendBar() {
|
||||
const items = [
|
||||
{
|
||||
icon: BarChart3Icon,
|
||||
label: "Measure",
|
||||
sub: "Quantify performance",
|
||||
color:
|
||||
"bg-sky-500/10 text-sky-700 dark:text-sky-300 border-sky-500/30",
|
||||
},
|
||||
{
|
||||
icon: DnaIcon,
|
||||
label: "Improve",
|
||||
sub: "Data, training, quality",
|
||||
color:
|
||||
"bg-emerald-500/10 text-emerald-700 dark:text-emerald-300 border-emerald-500/30",
|
||||
},
|
||||
{
|
||||
icon: MicroscopeIcon,
|
||||
label: "Understand",
|
||||
sub: "Analyze & diagnose",
|
||||
color:
|
||||
"bg-violet-500/10 text-violet-700 dark:text-violet-300 border-violet-500/30",
|
||||
},
|
||||
{
|
||||
icon: RefreshCwIcon,
|
||||
label: "Iterate",
|
||||
sub: "Repeat & grow",
|
||||
color:
|
||||
"bg-amber-500/10 text-amber-700 dark:text-amber-300 border-amber-500/30",
|
||||
},
|
||||
];
|
||||
const BOTTOM_PANEL_DEFAULT_HEIGHT = 288;
|
||||
const BOTTOM_PANEL_MIN_HEIGHT = 120;
|
||||
|
||||
function BottomPanel({
|
||||
logs,
|
||||
history,
|
||||
tab,
|
||||
onTabChange,
|
||||
}: {
|
||||
logs: LogLine[];
|
||||
history: MetricsHistory | null;
|
||||
tab: "log" | "metrics";
|
||||
onTabChange: (next: "log" | "metrics") => void;
|
||||
}) {
|
||||
const [height, setHeight] = useState(BOTTOM_PANEL_DEFAULT_HEIGHT);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const dragRef = useRef<{ startY: number; startH: number } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFullscreen) return;
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setIsFullscreen(false);
|
||||
};
|
||||
window.addEventListener("keydown", onKey);
|
||||
return () => window.removeEventListener("keydown", onKey);
|
||||
}, [isFullscreen]);
|
||||
|
||||
const onHandlePointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (isFullscreen) return;
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
dragRef.current = { startY: e.clientY, startH: height };
|
||||
};
|
||||
const onHandlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragRef.current) return;
|
||||
const dy = dragRef.current.startY - e.clientY;
|
||||
const max =
|
||||
typeof window !== "undefined"
|
||||
? Math.max(BOTTOM_PANEL_MIN_HEIGHT, window.innerHeight - 100)
|
||||
: 800;
|
||||
setHeight(
|
||||
Math.max(
|
||||
BOTTOM_PANEL_MIN_HEIGHT,
|
||||
Math.min(max, dragRef.current.startH + dy),
|
||||
),
|
||||
);
|
||||
};
|
||||
const onHandlePointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragRef.current) return;
|
||||
dragRef.current = null;
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-4 border-t bg-background/80 px-6 py-2.5 overflow-x-auto">
|
||||
{items.map(({ icon: Icon, label, sub, color }) => (
|
||||
<div key={label} className="flex items-center gap-2 shrink-0">
|
||||
<span
|
||||
className={cn(
|
||||
"flex size-7 items-center justify-center rounded-full border",
|
||||
color,
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
</span>
|
||||
<div className="leading-tight">
|
||||
<div className="text-[11px] font-semibold text-foreground">
|
||||
{label}
|
||||
</div>
|
||||
<div className="text-[10px] text-muted-foreground">{sub}</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col bg-zinc-950",
|
||||
isFullscreen
|
||||
? "absolute inset-0 z-50 border-t"
|
||||
: "shrink-0 border-t",
|
||||
)}
|
||||
style={isFullscreen ? undefined : { height: `${height}px` }}
|
||||
>
|
||||
{!isFullscreen && (
|
||||
<div
|
||||
onPointerDown={onHandlePointerDown}
|
||||
onPointerMove={onHandlePointerMove}
|
||||
onPointerUp={onHandlePointerUp}
|
||||
onPointerCancel={onHandlePointerUp}
|
||||
className="group h-1.5 shrink-0 cursor-ns-resize bg-zinc-900 hover:bg-sky-500/40 active:bg-sky-500/60"
|
||||
title="拖动调整高度"
|
||||
>
|
||||
<div className="mx-auto mt-[2px] h-[2px] w-10 rounded-full bg-zinc-700 group-hover:bg-sky-400" />
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
<div className="flex min-h-0 flex-1 flex-col px-5 py-3">
|
||||
<div className="mb-2 flex shrink-0 items-center justify-between">
|
||||
<div className="inline-flex rounded-full bg-zinc-900/80 p-0.5">
|
||||
<PillTab active={tab === "log"} onClick={() => onTabChange("log")}>
|
||||
Live Log
|
||||
</PillTab>
|
||||
<PillTab
|
||||
active={tab === "metrics"}
|
||||
onClick={() => onTabChange("metrics")}
|
||||
>
|
||||
Metrics
|
||||
</PillTab>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsFullscreen((v) => !v)}
|
||||
className="inline-flex items-center gap-1 text-[11px] text-zinc-400 transition-colors hover:text-zinc-200"
|
||||
title={isFullscreen ? "退出 (Esc)" : "查看全部"}
|
||||
>
|
||||
查看全部
|
||||
{isFullscreen ? (
|
||||
<Minimize2Icon className="size-3" />
|
||||
) : (
|
||||
<Maximize2Icon className="size-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
{tab === "log" ? (
|
||||
<LogStreamBody logs={logs} />
|
||||
) : (
|
||||
<MetricsChartPanel history={history} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LogStream({ logs }: { logs: LogLine[] }) {
|
||||
function PillTab({
|
||||
active,
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1 font-semibold text-[11px] uppercase tracking-[0.12em] transition-colors",
|
||||
active
|
||||
? "bg-zinc-700 text-zinc-50"
|
||||
: "text-zinc-400 hover:text-zinc-200",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function LogStreamBody({ logs }: { logs: LogLine[] }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollTop = ref.current.scrollHeight;
|
||||
}, [logs]);
|
||||
return (
|
||||
<div className="flex h-72 shrink-0 flex-col border-t bg-zinc-950 px-5 py-3">
|
||||
<div className="mb-2 flex shrink-0 items-center justify-between">
|
||||
<span className="font-semibold text-xs uppercase tracking-[0.18em] text-zinc-300">
|
||||
Live Log
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-1 text-[11px] text-zinc-400 transition-colors hover:text-zinc-200"
|
||||
>
|
||||
查看全部
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
ref={ref}
|
||||
className="min-h-0 flex-1 overflow-y-auto font-mono text-[12px] leading-6 text-zinc-100"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="text-zinc-500">暂无日志</div>
|
||||
) : (
|
||||
logs.map((line, idx) => (
|
||||
<div key={idx} className="flex gap-3">
|
||||
<span className="text-zinc-500">{line.ts}</span>
|
||||
<span className="text-emerald-400">[iter={line.iter}]</span>
|
||||
<span>{line.text}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
ref={ref}
|
||||
className="h-full min-h-0 overflow-y-auto font-mono text-[12px] text-zinc-100 leading-6"
|
||||
>
|
||||
{logs.length === 0 ? (
|
||||
<div className="text-zinc-500">暂无日志</div>
|
||||
) : (
|
||||
logs.map((line, idx) => (
|
||||
<div key={idx} className="flex gap-3">
|
||||
<span className="text-zinc-500">{line.ts}</span>
|
||||
<span className="text-emerald-400">[iter={line.iter}]</span>
|
||||
<span>{line.text}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user