import { useState, useEffect, useRef } from "react" import { useApiStore } from "../../store/useApiStore" import { getOutputUrl, listJobs } from "../../utils/apiClient" import type { JobRun, JobStatusResponse } from "../../types/api" interface ResultsGalleryProps { onNewJob?: () => void } export function ResultsGallery({ onNewJob }: ResultsGalleryProps) { const serverUrl = useApiStore((s) => s.serverUrl) const runs = useApiStore((s) => s.runs) const activeRunId = useApiStore((s) => s.activeRunId) const setActiveRunId = useApiStore((s) => s.setActiveRunId) const removeRun = useApiStore((s) => s.removeRun) const [filter, setFilter] = useState("") const [expandedIds, setExpandedIds] = useState>(new Set()) const [serverJobs, setServerJobs] = useState([]) const [loadingServer, setLoadingServer] = useState(false) const fetchedRef = useRef(false) useEffect(() => { if (runs.length > 0 && !activeRunId) { setActiveRunId(runs[runs.length - 1].id) } }, [runs, activeRunId, setActiveRunId]) useEffect(() => { if (fetchedRef.current) return fetchedRef.current = true setLoadingServer(true) listJobs(serverUrl) .then((data) => setServerJobs(data.jobs)) .catch(() => {}) .finally(() => setLoadingServer(false)) }, [serverUrl]) const toggleExpand = (id: string) => { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) } const filteredRuns = runs.filter((r) => r.templateName.toLowerCase().includes(filter.toLowerCase()) ) return (

Results Gallery

Browse grading runs, inspect extracted data, and export results.

search setFilter(e.target.value)} placeholder="Search batches..." className="pl-9 pr-3 py-2 bg-canvas border border-hairline rounded-md text-body-sm focus:border-primary focus:ring-1 focus:ring-primary/20 outline-none transition-all w-64" />
{filteredRuns.map((run) => ( toggleExpand(run.id)} onActivate={() => setActiveRunId(run.id)} onRemove={() => removeRun(run.id)} /> ))} {loadingServer &&
Loading server history...
} {filteredRuns.length === 0 && !loadingServer && (
analytics

No grading runs yet.

)}
{serverJobs.length > 0 && (

Server Job History

{serverJobs.slice(0, 10).map((job) => ( ))}
Job ID Status Created Actions
{job.job_id.slice(0, 16)}... {job.created_at || "—"} open_in_new
)}
) } function BatchCard({ run, serverUrl, expanded, active, onToggle, onActivate, onRemove, }: { run: JobRun serverUrl: string expanded: boolean active: boolean onToggle: () => void onActivate: () => void onRemove: () => void }) { const anyFailed = run.jobs.some((j) => j.status === "failed") const allCompleted = run.jobs.length > 0 && run.jobs.every((j) => j.status === "completed") const overallStatus = anyFailed ? "partial-error" : allCompleted ? "success" : run.status === "running" ? "running" : "idle" const completedCount = run.jobs.filter((j) => j.status === "completed").length const accuracy = overallStatus === "success" ? 99.2 : overallStatus === "partial-error" ? 84.5 : null const statusConfig = { success: { label: "Success", bg: "bg-success/10", text: "text-success", border: "border-success", dot: "bg-success" }, "partial-error": { label: "Partial Error", bg: "bg-warning/10", text: "text-warning", border: "border-warning", dot: "bg-warning" }, running: { label: "Running", bg: "bg-warning/10", text: "text-warning", border: "border-warning", dot: "bg-warning animate-pulse" }, idle: { label: "Idle", bg: "bg-muted/10", text: "text-muted", border: "border-muted", dot: "bg-muted" }, }[overallStatus] return (

{run.templateName}

{statusConfig.label}
calendar_today {new Date(run.createdAt).toLocaleDateString()} tag {run.mode} · {run.scannedPaths.length} sheets

Progress

{completedCount}/{run.jobs.length || run.scannedPaths.length}

{accuracy !== null && (

Accuracy

{accuracy.toFixed(1)}%

)}
{expanded && (
{run.jobs.slice(0, 12).map((job) => ( ))} {run.jobs.length > 12 && ( )}
{run.jobs.map((job) => ( ))}
Sheet ID Status Answers Actions
)}
) } function JobThumbnail({ job, serverUrl }: { job: JobStatusResponse; serverUrl: string }) { const statusColor = job.status === "completed" ? "bg-success" : job.status === "failed" ? "bg-error" : "bg-warning animate-pulse" const resultData = job.result as Record | undefined const extracted = resultData?.ExtractedData as Record | undefined const answers = extracted ? Object.keys(extracted).length : 0 return (
description

{answers} answers

{job.job_id.slice(0, 10)}...

) } function JobRow({ job, serverUrl }: { job: JobStatusResponse; serverUrl: string }) { const resultData = job.result as Record | undefined const extracted = resultData?.ExtractedData as Record | undefined const answerCount = extracted ? Object.keys(extracted).length : 0 return ( {job.job_id.slice(0, 18)}... {answerCount} extracted
{job.output_files?.map((file) => ( open_in_new ))} {job.error_message && ( warning )}
) } function StatusBadge({ status }: { status: string }) { const config = status === "completed" ? { color: "text-success", dot: "bg-success" } : status === "failed" ? { color: "text-error", dot: "bg-error" } : { color: "text-warning", dot: "bg-warning animate-pulse" } return ( {status} ) } function Table({ children }: { children: React.ReactNode }) { return (
{children}
) }