"use client"; import { BarChart3Icon, CheckIcon, ChevronDownIcon, ClipboardListIcon, ClockIcon, DnaIcon, ExternalLinkIcon, FileTextIcon, FlaskConicalIcon, GitBranchIcon, GraduationCapIcon, HourglassIcon, Loader2Icon, MicroscopeIcon, type LucideIcon, RefreshCwIcon, ShieldCheckIcon, TrendingUpIcon, } 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" | "complete" | "failed" | "cancelled"; type PhaseTone = "complete" | "running" | "pending"; type PipelineCard = { key: string; icon?: string; title: string; subtitle: string; status: CardStatus; progress?: number; }; type PipelinePhase = { key: string; label: string; tone: PhaseTone; 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; status: { mode: string; state: CardStatus }; kpis: Kpi[]; phases: PipelinePhase[]; logs: LogLine[]; }; const ICON_MAP: Record = { "bar-chart": BarChart3Icon, microscope: MicroscopeIcon, "trending-up": TrendingUpIcon, "file-text": FileTextIcon, dna: DnaIcon, "git-branch": GitBranchIcon, flask: FlaskConicalIcon, shield: ShieldCheckIcon, "graduation-cap": GraduationCapIcon, "clipboard-list": ClipboardListIcon, "refresh-cw": RefreshCwIcon, clock: ClockIcon, hourglass: HourglassIcon, }; 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); // program.md missing → backend pushes {available: false}; clear panel. if (payload && payload.available === false) { setData(null); } else { setData(payload); } setLoading(false); } catch { /* ignore malformed events */ } }; es.onerror = () => { // EventSource auto-reconnects; just stop showing the loading state. setLoading(false); }; return () => { es.close(); }; }, [sessionId]); return (
{!data ? (
暂无 pipeline 数据
) : ( data.phases.map((phase) => ( setOpenCard({ key: card.key, title: card.title, status: card.status, }) } /> )) )}
{ 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"; return (

Training Pipeline Monitor

{isRunning ? ( ) : 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 PhaseSection({ phase, onCardClick, }: { phase: PipelinePhase; onCardClick?: (card: PipelineCard) => void; }) { const dotCls = phase.tone === "complete" ? "bg-emerald-500" : phase.tone === "running" ? "bg-sky-500" : "bg-violet-500"; const arrowComplete = phase.tone === "complete"; return (
{phase.label}
{phase.cards.map((card, idx) => (
onCardClick(card) : undefined} /> {idx < phase.cards.length - 1 ? ( ) : null}
))}
); } function PhaseArrow({ complete }: { complete: boolean }) { return (
); } 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 StatusBadge({ 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", }, 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 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", complete: "Complete", failed: "Failed", pending: "Idle", cancelled: "Cancelled", }; return map[state]; }