rasAli02 commited on
Commit
6b3f603
·
1 Parent(s): 1508d64

feat: add navigable report view from feed

Browse files
app.py CHANGED
@@ -316,6 +316,33 @@ async def handle_list(request: Request):
316
  items = [_summarize(doc) for doc in docs]
317
  return {"data": [json.dumps({"items": items, "total": len(items)})]}
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  @app.post("/api/metrics")
320
  async def handle_metrics(request: Request):
321
  docs = await _db_list_inspections(500)
 
316
  items = [_summarize(doc) for doc in docs]
317
  return {"data": [json.dumps({"items": items, "total": len(items)})]}
318
 
319
+ @app.get("/api/inspections/{inspection_id}")
320
+ async def get_inspection(inspection_id: str):
321
+ inspection = None
322
+ if _inspections_col is not None:
323
+ inspection = await _inspections_col.find_one({"id": inspection_id}, {"_id": 0})
324
+ else:
325
+ inspection = next((i for i in _mem_inspections if i["id"] == inspection_id), None)
326
+
327
+ if not inspection:
328
+ return JSONResponse({"detail": "Inspection not found"}, status_code=404)
329
+ return inspection
330
+
331
+ @app.post("/api/get_inspection")
332
+ async def handle_get_inspection(request: Request):
333
+ data = await request.json()
334
+ inspection_id = data.get("data", [""])[0]
335
+
336
+ inspection = None
337
+ if _inspections_col is not None:
338
+ inspection = await _inspections_col.find_one({"id": inspection_id}, {"_id": 0})
339
+ else:
340
+ inspection = next((i for i in _mem_inspections if i["id"] == inspection_id), None)
341
+
342
+ if not inspection:
343
+ return JSONResponse({"detail": "Inspection not found"}, status_code=404)
344
+ return {"data": [json.dumps(inspection)]}
345
+
346
  @app.post("/api/metrics")
347
  async def handle_metrics(request: Request):
348
  docs = await _db_list_inspections(500)
frontend/src/App.js CHANGED
@@ -7,6 +7,7 @@ import Console from "@/pages/Console";
7
  import Feed from "@/pages/Feed";
8
  import Blueprint from "@/pages/Blueprint";
9
  import Journal from "@/pages/Journal";
 
10
 
11
  function App() {
12
  return (
@@ -19,6 +20,7 @@ function App() {
19
  <Route path="/feed" element={<Feed />} />
20
  <Route path="/blueprint" element={<Blueprint />} />
21
  <Route path="/journal" element={<Journal />} />
 
22
  </Routes>
23
  <Toaster theme="dark" position="bottom-right" />
24
  </BrowserRouter>
 
7
  import Feed from "@/pages/Feed";
8
  import Blueprint from "@/pages/Blueprint";
9
  import Journal from "@/pages/Journal";
10
+ import ReportView from "@/pages/ReportView";
11
 
12
  function App() {
13
  return (
 
20
  <Route path="/feed" element={<Feed />} />
21
  <Route path="/blueprint" element={<Blueprint />} />
22
  <Route path="/journal" element={<Journal />} />
23
+ <Route path="/report/:id" element={<ReportView />} />
24
  </Routes>
25
  <Toaster theme="dark" position="bottom-right" />
26
  </BrowserRouter>
frontend/src/lib/api.js CHANGED
@@ -68,6 +68,13 @@ export const forgesight = {
68
  return data;
69
  },
70
 
 
 
 
 
 
 
 
71
  // GET /api/metrics
72
  async getMetrics() {
73
  if (useGradio) return gradioCall("metrics");
 
68
  return data;
69
  },
70
 
71
+ // GET /api/inspections/:id
72
+ async getInspection(id) {
73
+ if (useGradio) return gradioCall("get_inspection", id);
74
+ const { data } = await api.get(`/inspections/${id}`);
75
+ return data;
76
+ },
77
+
78
  // GET /api/metrics
79
  async getMetrics() {
80
  if (useGradio) return gradioCall("metrics");
frontend/src/pages/Console.jsx CHANGED
@@ -1,5 +1,6 @@
1
  import { useCallback, useRef, useState } from "react";
2
- import { Upload, Image as ImageIcon, PlayCircle, RotateCcw } from "lucide-react";
 
3
  import { toast } from "sonner";
4
  import { forgesight, fileToBase64 } from "@/lib/api";
5
  import TelemetryWidget from "@/components/TelemetryWidget";
@@ -186,11 +187,20 @@ export default function Console() {
186
  <SummaryStat label="Defects" value={summary.defect_count} />
187
  <SummaryStat label="Priority" value={summary.priority} />
188
  </div>
189
- <ReportDownloader
190
- targetRef={reportRef}
191
- inspectionId={result?.id}
192
- disabled={!result}
193
- />
 
 
 
 
 
 
 
 
 
194
  </div>
195
  </div>
196
  )}
 
1
  import { useCallback, useRef, useState } from "react";
2
+ import { Link } from "react-router-dom";
3
+ import { Upload, Image as ImageIcon, PlayCircle, RotateCcw, LayoutList } from "lucide-react";
4
  import { toast } from "sonner";
5
  import { forgesight, fileToBase64 } from "@/lib/api";
6
  import TelemetryWidget from "@/components/TelemetryWidget";
 
187
  <SummaryStat label="Defects" value={summary.defect_count} />
188
  <SummaryStat label="Priority" value={summary.priority} />
189
  </div>
190
+ <div className="flex items-center gap-4">
191
+ <Link
192
+ to="/feed"
193
+ className="fs-chip inline-flex items-center gap-1.5 hover:border-white/40 hover:text-white transition-colors"
194
+ >
195
+ <LayoutList className="w-3 h-3" />
196
+ Feed
197
+ </Link>
198
+ <ReportDownloader
199
+ targetRef={reportRef}
200
+ inspectionId={result?.id}
201
+ disabled={!result}
202
+ />
203
+ </div>
204
  </div>
205
  </div>
206
  )}
frontend/src/pages/Feed.jsx CHANGED
@@ -1,5 +1,5 @@
1
  import { useEffect, useState } from "react";
2
- import { Link } from "react-router-dom";
3
  import { forgesight } from "@/lib/api";
4
  import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from "recharts";
5
  import { AlertTriangle, CheckCircle2, XCircle, TrendingUp } from "lucide-react";
@@ -7,6 +7,7 @@ import { AlertTriangle, CheckCircle2, XCircle, TrendingUp } from "lucide-react";
7
  export default function Feed() {
8
  const [metrics, setMetrics] = useState(null);
9
  const [items, setItems] = useState([]);
 
10
 
11
  const load = async () => {
12
  try {
@@ -136,7 +137,11 @@ export default function Feed() {
136
  </thead>
137
  <tbody>
138
  {items.map((it) => (
139
- <tr key={it.id} className="border-b border-white/5 hover:bg-white/[0.02]">
 
 
 
 
140
  <Td mono>{new Date(it.created_at).toLocaleString()}</Td>
141
  <Td>
142
  <span className={`fs-chip fs-chip-${it.verdict}`}>{it.verdict}</span>
 
1
  import { useEffect, useState } from "react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
  import { forgesight } from "@/lib/api";
4
  import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from "recharts";
5
  import { AlertTriangle, CheckCircle2, XCircle, TrendingUp } from "lucide-react";
 
7
  export default function Feed() {
8
  const [metrics, setMetrics] = useState(null);
9
  const [items, setItems] = useState([]);
10
+ const navigate = useNavigate();
11
 
12
  const load = async () => {
13
  try {
 
137
  </thead>
138
  <tbody>
139
  {items.map((it) => (
140
+ <tr
141
+ key={it.id}
142
+ onClick={() => navigate(`/report/${it.id}`)}
143
+ className="border-b border-white/5 hover:bg-white/[0.04] cursor-pointer transition-colors"
144
+ >
145
  <Td mono>{new Date(it.created_at).toLocaleString()}</Td>
146
  <Td>
147
  <span className={`fs-chip fs-chip-${it.verdict}`}>{it.verdict}</span>
frontend/src/pages/ReportView.jsx ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState, useRef } from "react";
2
+ import { useParams, Link } from "react-router-dom";
3
+ import { forgesight } from "@/lib/api";
4
+ import AgentTranscript from "@/components/AgentTranscript";
5
+ import ReportDownloader from "@/components/ReportDownloader";
6
+ import { ArrowLeft, Loader2 } from "lucide-react";
7
+
8
+ export default function ReportView() {
9
+ const { id } = useParams();
10
+ const [result, setResult] = useState(null);
11
+ const [loading, setLoading] = useState(true);
12
+ const reportRef = useRef(null);
13
+
14
+ useEffect(() => {
15
+ async function load() {
16
+ try {
17
+ const data = await forgesight.getInspection(id);
18
+ setResult(data);
19
+ } catch (e) {
20
+ console.error("Failed to load inspection", e);
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ }
25
+ load();
26
+ }, [id]);
27
+
28
+ if (loading) {
29
+ return (
30
+ <div className="mx-auto max-w-[1400px] px-6 py-20 flex items-center justify-center">
31
+ <Loader2 className="w-8 h-8 animate-spin text-zinc-500" />
32
+ </div>
33
+ );
34
+ }
35
+
36
+ if (!result) {
37
+ return (
38
+ <div className="mx-auto max-w-[1400px] px-6 py-20 text-center text-zinc-500 font-mono">
39
+ Inspection not found.
40
+ </div>
41
+ );
42
+ }
43
+
44
+ // Same logic to extract summary as in backend, since frontend might not have `result.summary` populated if we just fetched raw doc
45
+ // Wait, backend does not attach `.summary` to raw doc, it's generated by `_summarize(doc)` for Feed.
46
+ // We can write a quick helper here.
47
+ const agents = result?.transcript?.agents || [];
48
+ const inspector = agents.find((a) => a.role === "inspector")?.output?.parsed || {};
49
+ const reporter = agents.find((a) => a.role === "reporter")?.output?.parsed || {};
50
+ const action = agents.find((a) => a.role === "action")?.output?.parsed || {};
51
+
52
+ const defects = inspector.defects || [];
53
+ const summary = {
54
+ verdict: inspector.verdict || "warn",
55
+ confidence: inspector.confidence || 0,
56
+ defect_count: defects.length,
57
+ priority: action.priority || "P2",
58
+ };
59
+
60
+ return (
61
+ <div className="mx-auto max-w-[1400px] px-6 py-10" data-testid="report-view-page">
62
+ <header className="mb-8 flex items-center justify-between">
63
+ <div>
64
+ <Link to="/feed" className="inline-flex items-center gap-2 text-zinc-400 hover:text-white transition-colors mb-4 fs-label">
65
+ <ArrowLeft className="w-4 h-4" /> Back to Feed
66
+ </Link>
67
+ <h1 className="font-display font-black tracking-tighter text-4xl md:text-5xl">
68
+ Inspection Report
69
+ </h1>
70
+ <p className="text-zinc-400 mt-3 font-mono text-sm">ID: {result.id}</p>
71
+ </div>
72
+ </header>
73
+
74
+ <div className="grid lg:grid-cols-12 gap-6">
75
+ {/* RIGHT — transcript (Using 12 columns since we don't have the image input UI here) */}
76
+ <div className="lg:col-span-10 lg:col-start-2 space-y-6" ref={reportRef}>
77
+ <div className="border border-white/10 bg-[#141416] p-5 fs-rise" data-testid="summary-panel">
78
+ <div className="flex items-start justify-between gap-4 flex-wrap mb-4">
79
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 flex-1">
80
+ <SummaryStat label="Verdict" value={summary.verdict.toUpperCase()} kind={summary.verdict} />
81
+ <SummaryStat label="Confidence" value={`${Math.round(summary.confidence * 100)}%`} />
82
+ <SummaryStat label="Defects" value={summary.defect_count} />
83
+ <SummaryStat label="Priority" value={summary.priority} />
84
+ </div>
85
+ <ReportDownloader
86
+ targetRef={reportRef}
87
+ inspectionId={result?.id}
88
+ disabled={!result}
89
+ />
90
+ </div>
91
+ </div>
92
+
93
+ <AgentTranscript transcript={result.transcript} />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ function SummaryStat({ label, value, kind }) {
101
+ const color =
102
+ kind === "pass" ? "text-[#10B981]" :
103
+ kind === "warn" ? "text-[#F59E0B]" :
104
+ kind === "fail" ? "text-[#ED1C24]" : "text-white";
105
+ return (
106
+ <div>
107
+ <div className="fs-label mb-1">{label}</div>
108
+ <div className={`font-display font-black text-2xl tabular-nums ${color}`}>{value}</div>
109
+ </div>
110
+ );
111
+ }