import { ArrowLeftIcon, DownloadIcon, Loader2Icon, RefreshCwIcon, ShieldCheckIcon, Trash2Icon, } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import type { RetrievalTraceDetail, RetrievalTraceExportResult, RetrievalTraceLabelResult, RetrievalTraceListResult, RetrievalTracePurgeResult, } from "../../../core/retrieval-trace-management"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, } from "../components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "../components/ui/table"; import { apiFetch } from "../hooks/use-api"; import { TraceEvidenceList, type TraceEvidenceSelection, TraceLabelForm, TracePurgeNotice, } from "./trace-history-detail"; interface PageProps { navigate: (to: string | number) => void; } type ConfirmAction = | { kind: "delete"; traceId: string } | { kind: "purge" } | null; const statusVariant = ( status: string ): "default" | "destructive" | "outline" | "secondary" => { if (status === "completed") return "default"; if (status === "failed" || status === "cancelled") return "destructive"; if (status === "partial") return "secondary"; return "outline"; }; const downloadJson = (value: unknown, filename: string): void => { const blob = new Blob([`${JSON.stringify(value, null, 2)}\n`], { type: "application/json", }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; anchor.click(); URL.revokeObjectURL(url); }; export default function TraceHistory({ navigate }: PageProps) { const [history, setHistory] = useState(null); const [detail, setDetail] = useState(null); const [selected, setSelected] = useState>(new Set()); const [busy, setBusy] = useState("list"); const [error, setError] = useState(null); const [confirmAction, setConfirmAction] = useState(null); const [label, setLabel] = useState< "relevant" | "irrelevant" | "missing_expected" >("relevant"); const [targetRef, setTargetRef] = useState(""); const [sourceHash, setSourceHash] = useState(""); const [selectedEvidence, setSelectedEvidence] = useState(null); const [purgeReceipt, setPurgeReceipt] = useState(null); const loadHistory = useCallback(async (): Promise => { setBusy("list"); setError(null); const result = await apiFetch( "/api/traces?limit=100" ); if (result.error) { setError(result.error); } else { setHistory(result.data); const ids = new Set(result.data?.traces.map((trace) => trace.traceId)); setSelected( (current) => new Set([...current].filter((traceId) => ids.has(traceId))) ); } setBusy(null); }, []); useEffect(() => { void loadHistory(); }, [loadHistory]); const inspect = useCallback(async (traceId: string): Promise => { setBusy(`show:${traceId}`); setError(null); const result = await apiFetch( `/api/traces/${encodeURIComponent(traceId)}?detailLimit=500` ); setSelectedEvidence(null); setTargetRef(""); setSourceHash(""); setDetail(result.data); setError(result.error); setBusy(null); }, []); const submitLabel = async (): Promise => { if (!detail || !targetRef.trim()) return; setBusy("label"); setError(null); const result = await apiFetch( `/api/traces/${encodeURIComponent(detail.trace.traceId)}/judgments`, { method: "POST", body: JSON.stringify({ label, targetRef: targetRef.trim(), ...(label !== "missing_expected" && selectedEvidence?.ref === targetRef.trim() ? { targetKind: selectedEvidence.targetKind, startLine: selectedEvidence.startLine, endLine: selectedEvidence.endLine, sourceHash: selectedEvidence.sourceHash, docid: selectedEvidence.docid, } : {}), ...(label === "missing_expected" && sourceHash.trim() ? { sourceHash: sourceHash.trim() } : {}), }), } ); if (result.error) { setError(result.error); } else { setTargetRef(""); setSourceHash(""); setSelectedEvidence(null); await inspect(detail.trace.traceId); } setBusy(null); }; const exportSelected = async (): Promise => { if (selected.size === 0) return; setBusy("export"); setError(null); const result = await apiFetch( "/api/traces/export", { method: "POST", body: JSON.stringify({ traceIds: [...selected], format: "agentic-receipt", }), } ); if (result.data) { downloadJson( result.data.artifact, `gno-traces-${result.data.manifest.artifactHash.slice(0, 12)}.json` ); } setError(result.error); setBusy(null); }; const runConfirmedAction = async (): Promise => { if (!confirmAction) return; const action = confirmAction; setConfirmAction(null); setBusy(action.kind); setError(null); const endpoint = action.kind === "purge" ? "/api/traces" : `/api/traces/${encodeURIComponent(action.traceId)}`; const result = await apiFetch(endpoint, { method: "DELETE", }); if (result.error) { setError(result.error); } else { if (action.kind === "purge") setPurgeReceipt(result.data); setDetail(null); setSelected(new Set()); await loadHistory(); } setBusy(null); }; const terminalSelected = useMemo(() => { const byId = new Map( history?.traces.map((trace) => [trace.traceId, trace]) ?? [] ); return [...selected].every( (traceId) => byId.get(traceId)?.status !== "open" ); }, [history, selected]); return (

Trace history

Private receipts. Local evidence. Explicit feedback only.

Local receipts {error && (
{error}
)} {busy === "list" && !history ? (
Loading traces
) : ( Use Trace Status Mode Created {history?.traces.map((trace) => ( void inspect(trace.traceId)} > { event.stopPropagation(); setSelected((current) => { const next = new Set(current); if (next.has(trace.traceId)) { next.delete(trace.traceId); } else { next.add(trace.traceId); } return next; }); }} onClick={(event) => event.stopPropagation()} type="checkbox" /> {trace.traceId} {trace.status} {trace.redactionMode} {new Date(trace.createdAtMs).toLocaleString()} ))}
)}
Receipt detail {!detail ? (

Select a trace to inspect bounded evidence and add explicit feedback.

) : (
{detail.trace.traceId}
{detail.trace.status} {detail.trace.redactionMode}
{( [ ["Runs", detail.runs.length], ["Events", detail.events.length], ["Labels", detail.judgments.length], ["Exports", detail.exports.length], ] as const ).map(([name, count]) => (
{count}
{name}
))}
{ setSelectedEvidence(selection); setTargetRef(selection.ref); setSourceHash(selection.sourceHash ?? ""); }} /> void submitLabel()} onTargetRefChange={(value) => { setTargetRef(value); setSelectedEvidence(null); }} sourceHash={sourceHash} targetRef={targetRef} />
)}
{ if (!open) setConfirmAction(null); }} open={confirmAction !== null} > {confirmAction?.kind === "purge" ? "Purge every retrieval trace?" : "Delete this retrieval trace?"} This removes local receipt data and cannot be undone. Source documents are not changed.
); }