import { useState, useRef, useMemo, useEffect } from "react"; import { ResponsiveContainer, AreaChart, Area, XAxis, YAxis, CartesianGrid, ReferenceLine, ReferenceArea, Tooltip, } from "recharts"; const API_URL = import.meta.env.VITE_API_URL || "/api/predict"; /* Extract contiguous anomaly regions [start, end] from the frame list. These become the glowing alarm bands behind the trace. */ function anomalyBands(frames) { const bands = []; let start = null; for (const f of frames) { if (f.is_anomaly) { if (start === null) start = f.frame_idx; } else if (start !== null) { bands.push([start, f.frame_idx - 1]); start = null; } } if (start !== null) bands.push([start, frames[frames.length - 1].frame_idx]); return bands; } /* Tooltip shows the threshold-relative value (intuitive) AND the raw MSE score (technical transparency). */ function makeTip(threshold) { return function TipBox({ active, payload }) { if (!active || !payload || !payload.length) return null; const p = payload[0].payload; if (p.rawScore === null || p.rawScore === undefined) return null; const rel = p.rawScore / threshold; return (
FRAME{p.frame}
SURPRISE {rel.toFixed(2)}×
RAW {p.rawScore.toExponential(2)}
); }; } export default function App() { const [file, setFile] = useState(null); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [drag, setDrag] = useState(false); const [currentFrame, setCurrentFrame] = useState(0); const inputRef = useRef(null); const videoRef = useRef(null); // Playable URL for the uploaded file (revoke on change to avoid leaks) const videoUrl = useMemo(() => (file ? URL.createObjectURL(file) : null), [file]); useEffect(() => { return () => { if (videoUrl) URL.revokeObjectURL(videoUrl); }; }, [videoUrl]); const status = loading ? "reading" : result ? "flagged" : "standby"; const statusLabel = loading ? "READING FEED" : result ? "ANALYSIS COMPLETE" : "FEED STANDBY"; function pick(f) { if (!f) return; setFile(f); setResult(null); setError(null); setCurrentFrame(0); } async function analyze() { if (!file) return; setLoading(true); setError(null); setResult(null); setCurrentFrame(0); try { const body = new FormData(); body.append("file", file); const res = await fetch(API_URL, { method: "POST", body }); if (!res.ok) { const detail = await res.json().catch(() => ({})); throw new Error(detail.detail || `Server returned ${res.status}`); } setResult(await res.json()); } catch (e) { setError( e.message?.includes("fetch") ? "Cannot reach the detector. Start the backend, then run again." : e.message ); } finally { setLoading(false); } } const fps = result?.fps || 10; const threshold = result?.threshold || 1; // Trace data: store BOTH the threshold-relative value (for the axis) and the // raw score (for the tooltip). Tripwire sits at 1.0x. const chartData = useMemo( () => result?.frames.map((f) => ({ frame: f.frame_idx, rel: f.score === null ? null : f.score / threshold, rawScore: f.score, is_anomaly: f.is_anomaly, })) ?? [], [result, threshold] ); // Progressive reveal: hide values beyond the playhead so the trace draws in sync const revealedData = useMemo( () => chartData.map((d) => ({ ...d, rel: d.frame <= currentFrame ? d.rel : null })), [chartData, currentFrame] ); const bands = useMemo(() => (result ? anomalyBands(result.frames) : []), [result]); const peakRel = useMemo(() => { const s = result?.frames.map((f) => f.score).filter((v) => v !== null) ?? []; return s.length ? Math.max(...s) / threshold : 0; }, [result, threshold]); const liveFrame = result?.frames[currentFrame]; const liveRel = liveFrame && liveFrame.score !== null ? liveFrame.score / threshold : null; const TipBox = useMemo(() => makeTip(threshold), [threshold]); return (
AUGUR
Every frame, predicted. Every surprise, flagged.
{statusLabel}
inputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); pick(e.dataTransfer.files?.[0]); }} >
VIDEO FEED INPUT
Drop a video, or click to select
The detector learns normal motion, then flags what it cannot predict.
{file &&
{file.name}
} pick(e.target.files?.[0])} />
{error &&
{error}
}
{result && (
FRAMES
{result.total_frames}
FLAGGED
{result.frames.filter((f) => f.is_anomaly).length}
PEAK SURPRISE
{peakRel.toFixed(2)}×
ALARM LINE
1.00×
)} {/* Synced playback: video + live readout */} {result && videoUrl && (
)}
THE SURPRISE TRACE
signal tripwire anomaly
Surprise shown relative to the detection threshold — 1.0× is the alarm line.
{result ? ( {/* glowing alarm bands behind the trace (full range, not revealed) */} {bands.map(([a, b], i) => ( ))} `${v.toFixed(1)}×`} width={48} tickLine={false} /> {/* the amber tripwire — now fixed at 1.0x */} {/* the playhead — follows the video */} } cursor={{ stroke: "#66768A", strokeDasharray: "3 3" }} /> {/* connectNulls=false leaves a gap over warm-up AND beyond the playhead */} ) : (
{loading ? "READING FEED…" : "AWAITING FEED"}
SURPRISE / FRAME
)}
{/* Most anomalous moments — heatmap overlays */} {result && result.top_anomalies?.length > 0 && (
MOST ANOMALOUS MOMENTS
Where the model was most surprised — heatmap over frame
{result.top_anomalies.map((a) => (
{`Frame
FRAME {a.frame_idx} {(a.score / threshold).toFixed(2)}×
))}
)}
); }