| import { useEffect, useState, useRef } from "react"; |
| import { useParams, Link } from "react-router-dom"; |
| import { forgesight } from "@/lib/api"; |
| import AgentTranscript from "@/components/AgentTranscript"; |
| import ReportDownloader from "@/components/ReportDownloader"; |
| import { ArrowLeft, Loader2 } from "lucide-react"; |
|
|
| export default function ReportView() { |
| const { id } = useParams(); |
| const [result, setResult] = useState(null); |
| const [loading, setLoading] = useState(true); |
| const reportRef = useRef(null); |
|
|
| useEffect(() => { |
| async function load() { |
| try { |
| const data = await forgesight.getInspection(id); |
| setResult(data); |
| } catch (e) { |
| console.error("Failed to load inspection", e); |
| } finally { |
| setLoading(false); |
| } |
| } |
| load(); |
| }, [id]); |
|
|
| if (loading) { |
| return ( |
| <div className="mx-auto max-w-[1400px] px-6 py-20 flex items-center justify-center"> |
| <Loader2 className="w-8 h-8 animate-spin text-zinc-500" /> |
| </div> |
| ); |
| } |
|
|
| if (!result) { |
| return ( |
| <div className="mx-auto max-w-[1400px] px-6 py-20 text-center text-zinc-500 font-mono"> |
| Inspection not found. |
| </div> |
| ); |
| } |
|
|
| |
| |
| |
| const agents = result?.transcript?.agents || []; |
| const inspector = agents.find((a) => a.role === "inspector")?.output?.parsed || {}; |
| const reporter = agents.find((a) => a.role === "reporter")?.output?.parsed || {}; |
| const action = agents.find((a) => a.role === "action")?.output?.parsed || {}; |
| |
| const defects = inspector.defects || []; |
| const summary = { |
| verdict: inspector.verdict || "warn", |
| confidence: inspector.confidence || 0, |
| defect_count: defects.length, |
| priority: action.priority || "P2", |
| }; |
|
|
| return ( |
| <div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="report-view-page"> |
| <header className="mb-8 flex items-center justify-between"> |
| <div> |
| <Link to="/feed" className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-4 fs-label"> |
| <ArrowLeft className="w-4 h-4" /> Back to Feed |
| </Link> |
| <h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl"> |
| Inspection Report |
| </h1> |
| <p className="text-zinc-400 mt-3 font-mono text-sm">ID: {result.id}</p> |
| </div> |
| </header> |
| |
| <div className="grid lg:grid-cols-12 gap-6"> |
| {/* RIGHT — transcript (Using 12 columns since we don't have the image input UI here) */} |
| <div className="lg:col-span-10 lg:col-start-2 space-y-6" ref={reportRef}> |
| <div className="border border-white/10 bg-[#141416] p-5 fs-rise" data-testid="summary-panel"> |
| <div className="flex items-start justify-between gap-4 flex-wrap mb-4"> |
| <div className="grid grid-cols-2 md:grid-cols-4 gap-4 flex-1"> |
| <SummaryStat label="Verdict" value={summary.verdict.toUpperCase()} kind={summary.verdict} /> |
| <SummaryStat label="Confidence" value={`${Math.round(summary.confidence * 100)}%`} /> |
| <SummaryStat label="Defects" value={summary.defect_count} /> |
| <SummaryStat label="Priority" value={summary.priority} /> |
| </div> |
| <ReportDownloader |
| targetRef={reportRef} |
| inspectionId={result?.id} |
| disabled={!result} |
| /> |
| </div> |
| </div> |
| |
| <AgentTranscript transcript={result.transcript} /> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function SummaryStat({ label, value, kind }) { |
| const color = |
| kind === "pass" ? "text-[#10B981]" : |
| kind === "warn" ? "text-[#F59E0B]" : |
| kind === "fail" ? "text-[#ED1C24]" : "text-white"; |
| return ( |
| <div> |
| <div className="fs-label mb-1">{label}</div> |
| <div className={`font-display font-black text-2xl tabular-nums ${color}`}>{value}</div> |
| </div> |
| ); |
| } |
|
|