ForgeSight / frontend /src /pages /ReportView.jsx
rasAli02's picture
feat: add navigable report view from feed
6b3f603
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>
);
}
// Same logic to extract summary as in backend, since frontend might not have `result.summary` populated if we just fetched raw doc
// Wait, backend does not attach `.summary` to raw doc, it's generated by `_summarize(doc)` for Feed.
// We can write a quick helper here.
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>
);
}