import { useEffect, useMemo, useState } from 'react'; import { ArrowLeft, Beaker, CheckCircle2, FileText, ScrollText, Swords, XCircle } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { api } from '@/api/client'; import type { BattleShape, FlowRunRow } from '@/api/types'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { useSidebar } from '@/hooks/useSidebar'; import { cn } from '@/lib/utils'; // ─── Independent section data fetcher (same pattern as Analytics.tsx) ──────── function useFetch(fetcher: () => Promise): { data: T | null; loading: boolean; error: string | null; retry: () => void; } { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); function load() { setLoading(true); setError(null); fetcher() .then(setData) .catch((err: unknown) => { setError(err instanceof Error ? err.message : 'failed to load data'); }) .finally(() => setLoading(false)); } useEffect(() => { load(); }, []); // eslint-disable-line react-hooks/exhaustive-deps return { data, loading, error, retry: load }; } // ─── Skeleton ──────────────────────────────────────────────────────────────── function SkeletonBar({ className }: { className?: string }) { return
; } // ─── Formatters ────────────────────────────────────────────────────────────── function formatDate(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } function formatDuration(startIso: string, endIso?: string | null): string { const start = new Date(startIso).getTime(); const end = endIso ? new Date(endIso).getTime() : Date.now(); const ms = end - start; if (ms < 0) return '—'; const s = Math.round(ms / 1000); if (s < 60) return `${s}s`; if (s < 3600) return `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`; return `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}m`; } function truncate(str: string, max: number): string { if (str.length <= max) return str; return str.slice(0, max) + '…'; } // ─── Status dot (shared visual language with OrchestratorPane/ArenaPane) ────── type DotStatus = 'running' | 'completed' | 'failed' | 'cancelled' | 'pending'; function StatusDot({ status }: { status: DotStatus }) { if (status === 'running') { return ( ); } const cls = status === 'completed' ? 'bg-emerald-500' : status === 'failed' ? 'bg-destructive' : status === 'cancelled' ? 'bg-muted-foreground/20' : 'bg-muted-foreground/40'; // pending return ; } // ─── Tab bar ───────────────────────────────────────────────────────────────── type TabId = 'runs' | 'battles'; function TabBar({ active, onChange }: { active: TabId; onChange: (t: TabId) => void }) { return (
{[ { id: 'runs' as TabId, label: 'Analysis Runs', icon: FileText }, { id: 'battles' as TabId, label: 'Arena Battles', icon: Swords }, ].map((tab) => ( ))}
); } // ─── Empty state ───────────────────────────────────────────────────────────── function EmptyState({ message }: { message: string }) { return

{message}

; } // ─── Project selector ──────────────────────────────────────────────────────── function ProjectSelector({ projects, value, onChange, }: { projects: Array<{ id: string; name: string }>; value: string; onChange: (id: string) => void; }) { return ( ); } // ─── Analysis Runs tab ─────────────────────────────────────────────────────── function AnalysisRunsTab({ projectId }: { projectId: string }) { const { data, loading, error, retry } = useFetch(() => api.runs.list(projectId).then((r) => r.runs)); const [selectedRun, setSelectedRun] = useState(null); if (loading) { return (
{[0, 1, 2, 3].map((i) => ( ))}
); } if (error) { return (
{error}
); } if (!data || data.length === 0) { return ; } return (
{data.map((run) => (
{/* Expanded detail — report preview */} {selectedRun?.id === run.id && run.status === 'completed' && run.report && (
{truncate(run.report, 3000)}
)}
))}
); } // ─── Arena Battles tab ─────────────────────────────────────────────────────── function ArenaBattlesTab({ projectId }: { projectId: string }) { const { data, loading, error, retry } = useFetch(() => api.battles.list(projectId).then((r) => r.battles)); const [selectedBattle, setSelectedBattle] = useState(null); if (loading) { return (
{[0, 1, 2, 3].map((i) => ( ))}
); } if (error) { return (
{error}
); } if (!data || data.length === 0) { return ; } return (
{data.map((battle) => { const hasAnalysis = battle.status === 'completed' && battle.results_path; return (
{/* Expanded detail — analysis preview */} {selectedBattle?.id === battle.id && hasAnalysis && (
)}
); })}
); } // ─── Battle analysis preview (fetches analysis.md on expand) ───────────────── function AnalysisPreview({ battleId }: { battleId: string }) { const { data, loading, error, retry } = useFetch(() => api.battles.getAnalysis(battleId).then((r) => r.text)); if (loading) { return (
); } if (error) { return (
{error}
); } return (
{data ? truncate(data, 3000) : 'No analysis available.'}
); } // ─── Summary strip ─────────────────────────────────────────────────────────── function SummaryCards({ runs, battles, }: { runs: FlowRunRow[] | null; battles: BattleShape[] | null; }) { const totalRuns = runs?.length ?? 0; const completedRuns = runs?.filter((r) => r.status === 'completed').length ?? 0; const totalBattles = battles?.length ?? 0; const completedBattles = battles?.filter((b) => b.status === 'completed').length ?? 0; const cards = [ { label: 'Total Runs', value: totalRuns, icon: FileText, color: 'text-blue-500' }, { label: 'Completed Runs', value: completedRuns, icon: CheckCircle2, color: 'text-emerald-500' }, { label: 'Total Battles', value: totalBattles, icon: Swords, color: 'text-violet-500' }, { label: 'Completed Battles', value: completedBattles, icon: CheckCircle2, color: 'text-emerald-500' }, ]; return (
{cards.map((c) => (
{c.value}
{c.label}
))}
); } function SummaryCardsSkeleton() { return (
{[0, 1, 2, 3].map((i) => ( ))}
); } // ─── Main Page ─────────────────────────────────────────────────────────────── export function Results() { const navigate = useNavigate(); const { data: sidebar, activeSession } = useSidebar(); const [tab, setTab] = useState('runs'); const [projectId, setProjectId] = useState(null); // Derive default project from active session or first project. const projects = useMemo(() => { return sidebar?.projects?.map((p: { id: string; name: string }) => p) ?? []; }, [sidebar]); useEffect(() => { if (!projectId && projects.length > 0) { // Prefer active session's project, else first project. const defaultId = activeSession?.project_id ?? projects[0]!.id; setProjectId(defaultId); } }, [projects, activeSession, projectId]); function handleBack() { if (window.history.length > 1) { navigate(-1); } else { navigate('/'); } } const runsFetch = useFetch( projectId ? () => api.runs.list(projectId).then((r) => r.runs) : () => Promise.resolve([] as FlowRunRow[]), ); const battlesFetch = useFetch( projectId ? () => api.battles.list(projectId).then((r) => r.battles) : () => Promise.resolve([] as BattleShape[]), ); const summaryLoading = runsFetch.loading && battlesFetch.loading; return (
{/* Header */}

Results

Completed orchestrator runs and arena battles.

{projects.length > 0 && projectId && ( )}
{/* Summary Cards */} {summaryLoading ? ( ) : ( )} {/* Tab bar */} {/* Tab content */} {!projectId ? ( ) : tab === 'runs' ? ( ) : ( )}
); }