"use client"; import { ExternalLinkIcon, FileIcon, RefreshCwIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { MarkdownText } from "@/components/assistant-ui/markdown-text"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; import { cn } from "@/lib/utils"; type StepDetail = { step: string; run_dic: number | null; format: "markdown" | "jsonl" | "json" | "csv" | "text" | "empty"; content: string; source_path: string | null; fetched_at_ms: number; error?: string; tried_paths?: string[]; }; type Props = { sessionId: string | null; stepKey: string | null; stepTitle?: string; stepStatus?: string; open: boolean; onOpenChange: (open: boolean) => void; }; export function StepDetailSheet({ sessionId, stepKey, stepTitle, stepStatus, open, onOpenChange, }: Props) { const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [reloadCount, setReloadCount] = useState(0); useEffect(() => { if (!open || !sessionId || !stepKey) return; let cancelled = false; setLoading(true); setDetail(null); const url = `/api/claw/training/step-detail?session_id=${encodeURIComponent(sessionId)}&step=${encodeURIComponent(stepKey)}`; fetch(url, { cache: "no-store" }) .then((res) => (res.ok ? res.json() : null)) .then((payload) => { if (cancelled) return; setDetail(payload); setLoading(false); }) .catch(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [open, sessionId, stepKey, reloadCount]); return (
{stepTitle ?? stepKey ?? "—"} {stepStatus ? : null} {detail?.run_dic ? ( runDic={detail.run_dic} ) : null}
{detail?.source_path ? (
{detail.source_path}
) : null}
{loading ? (

加载中...

) : !detail ? (

无法获取详情

) : detail.format === "empty" ? ( ) : detail.format === "markdown" ? ( ) : detail.format === "json" ? ( ) : detail.format === "jsonl" ? ( ) : ( )}
{detail?.source_path && !loading ? (
{detail.format} ·{" "} {detail.fetched_at_ms ? new Date(detail.fetched_at_ms).toLocaleTimeString() : ""}
) : null}
); } function EmptyView({ detail }: { detail: StepDetail }) { return (

{detail.error ?? "暂无产物。"}

{detail.tried_paths && detail.tried_paths.length > 0 ? (
尝试的路径
    {detail.tried_paths.map((p) => (
  • {p}
  • ))}
) : null}
); } function JsonView({ text }: { text: string }) { const pretty = useMemo(() => { try { return JSON.stringify(JSON.parse(text), null, 2); } catch { return text; } }, [text]); return (
			{pretty}
		
); } function JsonLinesView({ text }: { text: string }) { const lines = useMemo( () => text .split("\n") .map((l) => l.trim()) .filter(Boolean) .map((l) => { try { return JSON.stringify(JSON.parse(l), null, 2); } catch { return l; } }), [text], ); return (
{lines.length} 行
{lines.map((l, idx) => (
行 #{idx + 1}
						{l}
					
))}
); } function RawView({ text }: { text: string }) { return (
			{text}
		
); } function StatusPill({ status }: { status: string }) { const map: Record = { complete: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300", running: "bg-sky-500/20 text-sky-700 dark:text-sky-200", pending: "bg-muted text-muted-foreground", failed: "bg-rose-500/15 text-rose-700 dark:text-rose-300", cancelled: "bg-muted text-muted-foreground", }; return ( {status.toUpperCase()} ); }