"use client"; import { AlertTriangleIcon, BarChart3Icon, CheckIcon, ChevronRightIcon, ClipboardListIcon, ClockIcon, DnaIcon, ExternalLinkIcon, FileTextIcon, FlaskConicalIcon, GraduationCapIcon, HourglassIcon, Loader2Icon, type LucideIcon, MicroscopeIcon, RefreshCwIcon, ShieldCheckIcon, TrendingUpIcon, UserCheckIcon, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { StepDetailSheet } from "@/components/training/step-detail-sheet"; import { cn } from "@/lib/utils"; type CardStatus = | "pending" | "running" | "waiting" | "complete" | "failed" | "cancelled"; type SectionType = "baseline" | "train" | "analysis"; type GateKind = "human-check" | "human-review"; type PipelineCard = { key: string; step: string; icon?: string; title: string; subtitle: string; status: CardStatus; progress?: number; }; type RoundItem = { type: "round"; index: number; run_id: string; run_index: number; section_type: SectionType; label: string; description: string; status: CardStatus; cards: PipelineCard[]; }; type GateItem = { type: "gate"; index: number; key: string; run_id: string; run_index: number; gate_kind: GateKind; title: string; description: string; status: CardStatus; reason?: string | null; }; type PipelineItem = RoundItem | GateItem; type InFlight = { run_id: string; section_type: SectionType; section_label: string; cards: PipelineCard[]; }; type Kpi = { label: string; value: string; icon?: string; }; type LogLine = { ts: string; iter: string | number; text: string; }; type PipelinePayload = { session_id: string; generated_at_ms: number; available?: boolean; status: { mode: string; state: CardStatus }; kpis: Kpi[]; items: PipelineItem[]; in_flight?: InFlight | null; logs: LogLine[]; }; const ICON_MAP: Record = { "alert-triangle": AlertTriangleIcon, "bar-chart": BarChart3Icon, microscope: MicroscopeIcon, "trending-up": TrendingUpIcon, "file-text": FileTextIcon, dna: DnaIcon, flask: FlaskConicalIcon, shield: ShieldCheckIcon, "graduation-cap": GraduationCapIcon, "clipboard-list": ClipboardListIcon, "refresh-cw": RefreshCwIcon, clock: ClockIcon, hourglass: HourglassIcon, "user-check": UserCheckIcon, }; const SECTION_PHASE_TONE: Record = { baseline: "from-violet-500/15 to-sky-500/10 border-violet-400/40", train: "from-emerald-500/15 to-amber-500/10 border-emerald-400/40", analysis: "from-sky-500/15 to-violet-500/10 border-sky-400/40", }; const SECTION_BADGE_TONE: Record = { baseline: "bg-violet-500/10 text-violet-700 dark:text-violet-300", train: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", analysis: "bg-sky-500/10 text-sky-700 dark:text-sky-300", }; export function TrainingPipelinePanel({ sessionId, }: { sessionId: string | null; }) { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [openCard, setOpenCard] = useState<{ key: string; title: string; status: string; } | null>(null); useEffect(() => { setLoading(true); const url = sessionId ? `/api/claw/training/pipeline/stream?session_id=${encodeURIComponent(sessionId)}` : "/api/claw/training/pipeline/stream"; const es = new EventSource(url); es.onmessage = (ev) => { try { const payload = JSON.parse(ev.data); if (payload && payload.available === false) { setData(null); } else { setData(payload); } setLoading(false); } catch { /* ignore malformed events */ } }; es.onerror = () => { setLoading(false); }; return () => { es.close(); }; }, [sessionId]); const stripNamespace = (key: string): string => { // "R1:cml" → "cml" — backend uses round-namespaced keys, but the // step-detail endpoint is keyed only by step name. const idx = key.indexOf(":"); return idx >= 0 ? key.slice(idx + 1) : key; }; return (
{(() => { if (!data) { return (
暂无 pipeline 数据
); } const items = data.items ?? []; if (items.length === 0) { const isRunning = data.status?.state === "running"; return (
{isRunning ? "首个阶段进行中,卡片即将出现……" : "等待 agent 进入第一个步骤……"}
); } return ( <> {items.map((item, idx) => { const showConnector = idx < items.length - 1; if (item.type === "round") { return (
setOpenCard({ key: stripNamespace(card.key), title: card.title, status: card.status, }) } /> {showConnector ? : null}
); } return (
setOpenCard({ key: stripNamespace(item.key), title: item.title, status: item.status, }) } /> {showConnector ? : null}
); })} ); })()}
{ if (!next) setOpenCard(null); }} />
); } function HeaderRow({ status, loading, }: { status: PipelinePayload["status"] | null; loading: boolean; }) { const statusLabel = loading ? "加载中" : status ? `${status.mode} · ${statusText(status.state)}` : "暂无运行"; const isRunning = status?.state === "running"; const isWaiting = status?.state === "waiting"; const isFailed = status?.state === "failed"; const isCancelled = status?.state === "cancelled"; const isErrored = isFailed || isCancelled; return (

Training Pipeline Monitor

{isRunning && !isErrored ? ( ) : null} {statusLabel}
); } function KpiRow({ kpis }: { kpis: Kpi[] }) { if (!kpis.length) { return null; } return (
{kpis.map((kpi) => { const Icon = kpi.icon ? ICON_MAP[kpi.icon] : null; return (
{kpi.label}
{kpi.value} {Icon ? ( ) : null}
); })}
); } function ItemConnector() { return (
); } function StatusPill({ status }: { status: CardStatus }) { const map: Record< CardStatus, { label: string; cls: string; icon: LucideIcon | null } > = { complete: { label: "COMPLETED", cls: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500/30", icon: CheckIcon, }, running: { label: "IN PROGRESS", cls: "bg-sky-500/15 text-sky-700 dark:text-sky-300 border-sky-500/30", icon: Loader2Icon, }, waiting: { label: "WAITING FOR YOU", cls: "bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-500/40", icon: HourglassIcon, }, pending: { label: "PENDING", cls: "bg-muted text-muted-foreground border-border", icon: null, }, failed: { label: "FAILED", cls: "bg-rose-500/15 text-rose-700 dark:text-rose-300 border-rose-500/30", icon: AlertTriangleIcon, }, cancelled: { label: "CANCELLED", cls: "bg-muted text-muted-foreground border-border", icon: null, }, }; const { label, cls, icon: Icon } = map[status]; return ( {Icon ? ( ) : null} {label} ); } function RoundSection({ item, onCardClick, }: { item: RoundItem; onCardClick?: (card: PipelineCard) => void; }) { const numLabel = item.index.toString().padStart(2, "0"); const phaseTone = SECTION_PHASE_TONE[item.section_type]; const badgeTone = SECTION_BADGE_TONE[item.section_type]; return (
{numLabel}
{item.label}

{item.description}

{item.cards.length > 0 ? (
{item.cards.map((card, idx) => (
onCardClick(card) : undefined} /> {idx < item.cards.length - 1 ? : null}
))}
) : null}
); } function CardArrow() { return (
); } function GateCard({ item, onClick, }: { item: GateItem; onClick?: () => void; }) { const numLabel = item.index.toString().padStart(2, "0"); const isWaiting = item.status === "waiting"; const isComplete = item.status === "complete"; const Tag = onClick ? "button" : "div"; const hint = item.gate_kind === "human-review" ? "在右侧对话里给出 OK / 调整后的指令,agent 才会继续。" : "在右侧对话里回 OK / 修改建议,确认后进入下一轮训练。"; return (
{numLabel}
{item.title} {item.run_id}
{item.reason ? (

{item.reason}

) : (

{item.description}

)} {isWaiting ? (

{hint}

) : null}
); } function PipelineCardView({ card, onClick, }: { card: PipelineCard; onClick?: () => void; }) { const isRunning = card.status === "running"; const isComplete = card.status === "complete"; const Icon = card.icon ? ICON_MAP[card.icon] : null; const tone = isComplete ? "border-emerald-500/40 bg-emerald-50/40 dark:bg-emerald-950/20" : isRunning ? "border-sky-400 bg-sky-50/60 dark:bg-sky-950/30 ring-2 ring-sky-300/50 dark:ring-sky-500/30 animate-pipeline-running-blue" : card.status === "failed" ? "border-rose-500/40 bg-rose-50/40 dark:bg-rose-950/20" : "border-border bg-background"; const Tag = onClick ? "button" : "div"; return (
{isComplete ? (
) : ( )}
{Icon ? ( ) : null} {card.title}
{card.subtitle}
); } function MiniStatus({ status }: { status: CardStatus }) { const map: Record = { complete: { label: "DONE", cls: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300", }, running: { label: "RUNNING", cls: "bg-sky-500/20 text-sky-700 dark:text-sky-200", }, waiting: { label: "WAITING", cls: "bg-amber-500/20 text-amber-700 dark:text-amber-200", }, pending: { label: "PENDING", cls: "bg-muted text-muted-foreground", }, failed: { label: "FAILED", cls: "bg-rose-500/15 text-rose-700 dark:text-rose-300", }, cancelled: { label: "CANCELLED", cls: "bg-muted text-muted-foreground", }, }; const { label, cls } = map[status]; return ( {status === "running" ? ( ) : null} {label} ); } 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", }, ]; return (
{items.map(({ icon: Icon, label, sub, color }) => (
{label}
{sub}
))}
); } function LogStream({ logs }: { logs: LogLine[] }) { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [logs]); return (
Live Log
{logs.length === 0 ? (
暂无日志
) : ( logs.map((line, idx) => (
{line.ts} [iter={line.iter}] {line.text}
)) )}
); } function statusText(state: CardStatus) { const map: Record = { running: "Running", waiting: "等待人工确认", complete: "Complete", failed: "Failed", pending: "Idle", cancelled: "Cancelled", }; return map[state]; }