import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import {
LineChart, Line, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid, ReferenceLine, Area, AreaChart
} from 'recharts';
import {
Shield, Activity, AlertTriangle, Zap, Target,
Eye, Layers, Radio, Cpu, Users,
TrendingUp, Download, Crosshair, Map, BarChart2,
Wifi, WifiOff, Play, Square, RotateCcw, Database,
Maximize2, Upload, GitBranch, Thermometer, Move,
ZoomIn, ZoomOut, Wind, Brain, Gauge, Sun, Moon
} from 'lucide-react';
import './index.css';
// ─── helpers ──────────────────────────────────────────────────
const nowStr = () => new Date().toLocaleTimeString('en-US', { hour12: false });
const uid = () => Math.random().toString(36).slice(2, 8);
const secStr = (ms) => {
const s = Math.floor(ms / 1000);
return `${String(Math.floor(s / 60)).padStart(2,'0')}:${String(s % 60).padStart(2,'0')}`;
};
// ─── API Base URLs (reads from env var, falls back to localhost for dev) ──────
const rawApiBase = (import.meta.env.VITE_API_URL || '').trim();
const hasPlaceholderApiBase =
!rawApiBase || /YOUR_(HF_USERNAME|USERNAME)|your_hf_username|your_username/i.test(rawApiBase);
const isLocalHost =
typeof window !== 'undefined' &&
['localhost', '127.0.0.1'].includes(window.location.hostname);
const fallbackApiBase =
typeof window === 'undefined'
? 'http://127.0.0.1:8000'
: (isLocalHost ? 'http://127.0.0.1:8000' : '');
const API_BASE = (hasPlaceholderApiBase ? fallbackApiBase : rawApiBase).replace(/\/$/, '');
const WS_BASE = API_BASE.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
const API_CONFIG_ERROR = 'Backend API is not configured. Set VITE_API_URL to your FastAPI server URL.';
const THREAT = {
SAFE: { label: 'SAFE', cls: 'safe' },
MODERATE: { label: 'MODERATE', cls: 'moderate' },
DANGER: { label: 'DANGER', cls: 'danger' },
};
function getThreat(count, limit) {
const r = limit > 0 ? count / limit : 0;
if (r >= 1) return THREAT.DANGER;
if (r >= 0.75) return THREAT.MODERATE;
return THREAT.SAFE;
}
// Density classification per 10k pixel reference area
function getDensityLabel(count, limit) {
const r = limit > 0 ? count / limit : 0;
if (r >= 1) return { label: 'CRITICAL', cls: 'danger' };
if (r >= 0.75) return { label: 'HIGH', cls: 'moderate' };
if (r >= 0.40) return { label: 'MEDIUM', cls: 'blue' };
return { label: 'LOW', cls: '' };
}
// Simple linear regression on last N data points for predictive alerts
function predictNextFrameCount(history, n = 12) {
const data = history.slice(-n);
if (data.length < 3) return null;
const xs = data.map((_, i) => i);
const ys = data.map(d => d.count);
const xMean = xs.reduce((a, b) => a + b, 0) / xs.length;
const yMean = ys.reduce((a, b) => a + b, 0) / ys.length;
const num = xs.reduce((s, x, i) => s + (x - xMean) * (ys[i] - yMean), 0);
const den = xs.reduce((s, x) => s + (x - xMean) ** 2, 0);
if (den === 0) return null;
const slope = num / den;
const intercept = yMean - slope * xMean;
// predict 10 frames ahead
return Math.max(0, Math.round(slope * (xs.length + 10) + intercept));
}
// ─── Custom Recharts Tooltip ──────────────────────────────────
function CTooltip({ active, payload, label }) {
if (!active || !payload?.length) return null;
return (
{label}
{payload[0].value}
);
}
// ─── Alert Item ───────────────────────────────────────────────
function AlertItem({ type, title, msg, time }) {
const Icon = type === 'danger' ? AlertTriangle : type === 'warning' ? Zap : Radio;
return (
);
}
// ─── Main App ─────────────────────────────────────────────────
export default function App() {
// ── Theme ──
const [theme, setTheme] = useState(() => localStorage.getItem('cp_theme') || 'dark');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('cp_theme', theme);
}, [theme]);
const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark');
// ── File / mode ──
const [file, setFile] = useState(null);
const [fileType, setFileType] = useState('image');
const [preview, setPreview] = useState(null); // image preview URL
const [videoPreview, setVideoPreview] = useState(null); // video preview URL
const [resultImg, setResultImg] = useState(null);
const [loading, setLoading] = useState(false);
const [dragActive, setDragActive] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0); // 0-100
// ── Analytics ──
const [stats, setStats] = useState({ count: 0, unique: 0, latency: 0, frames: 0 });
const [history, setHistory] = useState([]);
const [peakCount, setPeak] = useState(0);
const [alerts, setAlerts] = useState([]);
const [sessions, setSessions] = useState(() => {
try { return JSON.parse(localStorage.getItem('cp_sessions') || '[]'); }
catch { return []; }
});
const sessionStartTs = useRef(null);
// ── WebSocket ──
const wsRef = useRef(null);
const [wsConnected, setWsConnected] = useState(false);
// ── Settings ──
const [settings, setSettings] = useState({
heatmap: false,
clustering: false,
showPoints: true,
motionVecs: false,
zoning: false,
mode: 'Balanced',
capacity: 150,
magnification: 1.5,
nmsRadius: 9.0,
frameSkip: 3,
overlayOpacity: 100, // GAP 2: opacity slider
});
// ── Zoom / Pan (GAP 4) ──
const [zoom, setZoom] = useState(1);
const [pan, setPan] = useState({ x: 0, y: 0 });
const isPanning = useRef(false);
const panStart = useRef({ x: 0, y: 0 });
const panOrigin = useRef({ x: 0, y: 0 });
// ── Zone fencing ──
const [fencePoints, setFencePoints] = useState([]);
const [drawingFence, setDrawingFence] = useState(false);
const viewerRef = useRef(null);
const fileInputRef = useRef(null);
// ── Derived ──
const threat = getThreat(stats.count, settings.capacity);
const density = getDensityLabel(stats.count, settings.capacity);
const anomalyActive = alerts.some(a => a.type === 'danger' && Date.now() - a._ts < 5000);
const predicted = useMemo(() => predictNextFrameCount(history), [history]);
const predictAlert = predicted !== null && predicted > settings.capacity;
// ── Clock ──
const [clock, setClock] = useState(nowStr());
useEffect(() => {
const t = setInterval(() => setClock(nowStr()), 1000);
return () => clearInterval(t);
}, []);
// ── FPS ──
const lastHistLen = useRef(0);
const [fps, setFps] = useState(0);
useEffect(() => {
const t = setInterval(() => {
const delta = history.length - lastHistLen.current;
setFps(Math.max(0, Math.round(delta / Math.max(1, settings.frameSkip) * settings.frameSkip)));
lastHistLen.current = history.length;
}, 1000);
return () => clearInterval(t);
}, [history, settings.frameSkip]);
// ── Average density from history ──
const avgCount = history.length > 0
? Math.round(history.reduce((s, h) => s + h.count, 0) / history.length)
: 0;
// ── Alert helper ──
const addAlert = useCallback((type, title, msg) => {
const entry = { id: uid(), type, title, msg, time: nowStr(), _ts: Date.now() };
setAlerts(prev => [entry, ...prev].slice(0, 60));
}, []);
useEffect(() => {
if (hasPlaceholderApiBase) {
addAlert(
'warning',
'API Fallback Active',
API_BASE
? `Using fallback backend: ${API_BASE}. Set VITE_API_URL for deployed builds.`
: API_CONFIG_ERROR
);
}
}, [addAlert]);
// ── Predictive alert (GAP optional-7) ──
useEffect(() => {
if (predictAlert && history.length % 15 === 0 && history.length > 0) {
addAlert('warning', '🔮 Predictive Alert', `Model predicts ~${predicted} subjects in ~10 frames — limit ${settings.capacity}`);
}
}, [predictAlert, predicted, history.length, settings.capacity, addAlert]);
// ── File handling ──
const handleFile = useCallback((f) => {
setFile(f);
setResultImg(null);
setHistory([]);
setPeak(0);
setFencePoints([]);
setZoom(1);
setPan({ x: 0, y: 0 });
setUploadProgress(0);
setStats({ count: 0, unique: 0, latency: 0, frames: 0 });
sessionStartTs.current = Date.now();
// Revoke any old preview URLs to avoid memory leaks
if (preview) URL.revokeObjectURL(preview);
if (videoPreview) URL.revokeObjectURL(videoPreview);
if (f.type.startsWith('video')) {
setFileType('video');
setPreview(null);
setVideoPreview(URL.createObjectURL(f));
} else {
setFileType('image');
setPreview(URL.createObjectURL(f));
setVideoPreview(null);
}
addAlert('info', 'Feed Loaded', `${f.name.slice(0, 28)} (${(f.size/1024/1024).toFixed(1)} MB)`);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [addAlert]);
const onDrop = (e) => {
e.preventDefault(); setDragActive(false);
if (e.dataTransfer.files[0]) handleFile(e.dataTransfer.files[0]);
};
const onDrag = (e) => {
e.preventDefault();
setDragActive(e.type === 'dragenter' || e.type === 'dragover');
};
// ── Zoom via scroll wheel (GAP 4) ──
const onWheel = (e) => {
e.preventDefault();
setZoom(z => Math.min(5, Math.max(1, z - e.deltaY * 0.002)));
};
// ── Pan via mouse drag (GAP 4) ──
const onMouseDown = (e) => {
if (zoom <= 1) return;
isPanning.current = true;
panStart.current = { x: e.clientX, y: e.clientY };
panOrigin.current = { ...pan };
e.currentTarget.style.cursor = 'grabbing';
};
const onMouseMove = (e) => {
if (!isPanning.current) return;
setPan({
x: panOrigin.current.x + (e.clientX - panStart.current.x),
y: panOrigin.current.y + (e.clientY - panStart.current.y),
});
};
const onMouseUp = (e) => {
isPanning.current = false;
if (e.currentTarget) e.currentTarget.style.cursor = zoom > 1 ? 'grab' : 'crosshair';
};
// ── Zone fencing click ──
const onViewerClick = (e) => {
if (!drawingFence || !viewerRef.current || isPanning.current) return;
const r = viewerRef.current.getBoundingClientRect();
const xr = (e.clientX - r.left) / r.width;
const yr = (e.clientY - r.top) / r.height;
setFencePoints(fp => [...fp, { x: xr, y: yr }]);
};
// ── Wheel listener (must be non-passive) ──
useEffect(() => {
const el = viewerRef.current;
if (!el) return;
el.addEventListener('wheel', onWheel, { passive: false });
return () => el.removeEventListener('wheel', onWheel);
});
// ── Image scan ──
const executeImageScan = async () => {
if (!file) return;
setLoading(true);
addAlert('info', 'Scan Initiated', 'Neural engine processing frame...');
const form = new FormData();
form.append('file', file);
form.append('confidence_threshold',
settings.mode === 'Performance' ? '0.45' : settings.mode === 'Accuracy' ? '0.25' : '0.35');
form.append('magnification', parseFloat(settings.magnification).toFixed(2));
form.append('nms_radius', parseFloat(settings.nmsRadius).toFixed(2));
form.append('use_heatmap', String(settings.heatmap));
form.append('use_clustering', String(settings.clustering));
form.append('use_motion_vectors', String(settings.motionVecs));
form.append('fencing_polygon', JSON.stringify(fencePoints));
form.append('inference_batch_size', '8');
form.append('patch_overlap',
settings.mode === 'Performance' ? '0.0' : settings.mode === 'Accuracy' ? '0.5' : '0.25');
form.append('inference_strategy', 'Auto');
form.append('max_resolution', '3840');
try {
if (!API_BASE) throw new Error(API_CONFIG_ERROR);
const res = await fetch(`${API_BASE}/api/process-image`, { method: 'POST', body: form });
const responseText = await res.text();
let data;
try {
data = responseText ? JSON.parse(responseText) : {};
} catch {
data = { detail: responseText || `HTTP ${res.status}` };
}
if (!res.ok) throw new Error(data.detail || `Image scan failed (HTTP ${res.status})`);
if (data.detail) throw new Error(data.detail);
setResultImg(`data:image/jpeg;base64,${data.imageB64}`);
const c = data.count;
const ts = nowStr();
setStats(s => ({ ...s, count: c, unique: c, latency: data.elapsed }));
setPeak(p => Math.max(p, c));
setHistory([{ label: ts, count: c }]);
const t = getThreat(c, settings.capacity);
if (t === THREAT.DANGER) addAlert('danger', '⚠ Capacity Breach', `${c} subjects — limit ${settings.capacity}`);
else if (t === THREAT.MODERATE) addAlert('warning', 'Elevated Density', `Zone at ${Math.round(c / settings.capacity * 100)}%`);
else addAlert('info', 'Scan Complete', `${c} subjects in ${data.elapsed.toFixed(2)}s`);
} catch (err) {
addAlert('danger', 'Scan Failed', err?.message || 'Unknown image scan error');
} finally {
setLoading(false);
}
};
// ── Video stream ──
const streamVideo = async () => {
if (!file) return;
setLoading(true);
setHistory([]);
setPeak(0);
setUploadProgress(0);
addAlert('info', 'Uploading Video', `${file.name.slice(0,28)} — ${(file.size/1024/1024).toFixed(1)} MB`);
try {
if (!API_BASE) throw new Error(API_CONFIG_ERROR);
// Use XMLHttpRequest so we can track upload progress
const file_id = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const form = new FormData();
form.append('file', file);
xhr.open('POST', `${API_BASE}/api/upload-video`, true);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
setUploadProgress(Math.round((e.loaded / e.total) * 100));
}
};
xhr.onload = () => {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data.file_id);
} catch {
reject(new Error('Invalid server response'));
}
} else {
try {
const err = JSON.parse(xhr.responseText);
reject(new Error(err.detail || `Upload failed (HTTP ${xhr.status})`));
} catch {
reject(new Error(`Upload failed (HTTP ${xhr.status})`));
}
}
};
xhr.onerror = () => reject(new Error('Network error during upload'));
xhr.send(form);
});
setUploadProgress(100);
addAlert('info', 'Upload Complete', 'Connecting to inference engine...');
const ws = new WebSocket(`${WS_BASE}/api/stream-video/${file_id}`);
wsRef.current = ws;
let lastCapacityAlertFrame = -999;
ws.onopen = () => {
setWsConnected(true);
addAlert('info', 'WebSocket Live', 'Real-time telemetry stream established');
ws.send(JSON.stringify({
settings: {
confidenceThresh:
settings.mode === 'Performance' ? 0.45 : settings.mode === 'Accuracy' ? 0.25 : 0.35,
magnification: parseFloat(parseFloat(settings.magnification).toFixed(2)),
nmsRadius: parseFloat(parseFloat(settings.nmsRadius).toFixed(2)),
useHeatmap: Boolean(settings.heatmap),
useClustering: Boolean(settings.clustering),
useMotionVecs: Boolean(settings.motionVecs),
frameSkip: Math.round(settings.frameSkip),
fencingPolygon: fencePoints,
capacityLimit: Math.round(settings.capacity),
}
}));
};
ws.onmessage = (e) => {
const payload = JSON.parse(e.data);
if (payload.status === 'playing') {
setResultImg(`data:image/jpeg;base64,${payload.imageB64}`);
const c = payload.count;
const ts = nowStr();
setStats(s => ({ ...s, count: c, unique: payload.total_unique, frames: payload.frame }));
setPeak(p => Math.max(p, c));
setHistory(h => [...h, { label: ts, count: c }]);
if (payload.anomalyEvent)
addAlert('danger', '⚠ CHAOS DETECTED', `Rapid movement at frame ${payload.frame}`);
const t = getThreat(c, settings.capacity);
if (t === THREAT.DANGER && payload.frame - lastCapacityAlertFrame > 30) {
lastCapacityAlertFrame = payload.frame;
addAlert('danger', 'Zone Overcrowding', `${c} subjects — ${Math.round(c / settings.capacity * 100)}%`);
}
} else if (payload.status === 'done') {
ws.close();
addAlert('info', 'Stream Complete', `${payload.total_unique ?? '?'} unique subjects archived`);
saveSession(file.name);
} else if (payload.status === 'error') {
ws.close();
addAlert('danger', 'Engine Error', payload.message || 'Unknown stream error');
}
};
ws.onerror = () => addAlert('danger', 'Stream Error', 'WebSocket connection failed');
ws.onclose = () => {
setWsConnected(false);
setLoading(false);
wsRef.current = null;
};
} catch (err) {
addAlert('danger', 'Upload Failed', err.message);
setLoading(false);
}
};
const terminateStream = () => {
wsRef.current?.close();
setLoading(false);
addAlert('warning', 'Stream Terminated', 'Operator manually terminated');
};
// ── Session save ──
const saveSession = (name) => {
const elapsed = sessionStartTs.current ? secStr(Date.now() - sessionStartTs.current) : '—';
const session = {
id: uid(), name, peak: peakCount,
avg: avgCount, alerts: alerts.length,
elapsed, time: new Date().toLocaleString(),
history: history.slice(-300),
};
setSessions(prev => {
const next = [session, ...prev].slice(0, 20);
localStorage.setItem('cp_sessions', JSON.stringify(next));
return next;
});
};
// ── Export ──
const exportReport = () => {
const report = {
generated: new Date().toISOString(),
file: file?.name, peakCount, avgCount,
alertCount: alerts.length, settings, history,
alerts: alerts.map(a => ({ time: a.time, type: a.type, title: a.title, msg: a.msg })),
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = `civic_pulse_${Date.now()}.json`; a.click();
URL.revokeObjectURL(url);
addAlert('info', 'Report Exported', 'Analytics report downloaded');
};
// ── Fence SVG ──
const fenceSvg = viewerRef.current && fencePoints.length > 0 ? (() => {
const { offsetWidth: w, offsetHeight: h } = viewerRef.current;
return { pts: fencePoints.map(p => `${p.x * w},${p.y * h}`).join(' '), w, h };
})() : null;
const handleExecute = () => fileType === 'video' ? streamVideo() : executeImageScan();
const toggleSetting = (key) => setSettings(s => ({ ...s, [key]: !s[key] }));
const setSetting = (key, val) => setSettings(s => ({ ...s, [key]: val }));
const countClass = threat === THREAT.DANGER ? 'danger' : threat === THREAT.MODERATE ? 'moderate' : '';
// ──────────────────────────────────────────────
return (
{/* ════════ TOP NAVBAR ════════ */}
{/* ════════ LEFT PANEL ════════ */}
{/* ════════ CENTER PANEL ════════ */}
1 ? (isPanning.current ? 'grabbing' : 'grab') : drawingFence ? 'crosshair' : 'default' }}
>
{/* Topbar */}
AERIAL FEED
{(loading || wsConnected) && LIVE}
{zoom > 1 && {zoom.toFixed(1)}×}
{[
{ key: 'heatmap', Icon: Thermometer, title: 'Heatmap' },
{ key: 'clustering', Icon: GitBranch, title: 'Clustering' },
{ key: 'motionVecs', Icon: Wind, title: 'Motion Vecs'},
{ key: 'drawFence', Icon: Target, title: 'Draw Zone' },
].map(({ key, Icon, title }) => (
{
e.stopPropagation();
if (key === 'drawFence') setDrawingFence(d => !d);
else toggleSetting(key);
}}>
))}
{ e.stopPropagation(); setZoom(z => Math.min(5, +(z + 0.5).toFixed(1))); }}>
{ e.stopPropagation(); viewerRef.current?.requestFullscreen?.(); }}>
{/* Loading state */}
{loading && !resultImg && (
{fileType === 'video' ? 'Initializing Telemetry...' : 'Neural Engine Processing...'}
)}
{/* Image / video feed with zoom+pan+opacity (GAP 2, 4) */}
{resultImg && (

)}
{!resultImg && preview && !loading && (

)}
{/* Empty state */}
{!resultImg && !preview && !loading && (
No Feed Detected
Upload aerial imagery or video to begin analysis
)}
{/* Fencing SVG */}
{drawingFence && viewerRef.current && (
)}
{/* Count overlay */}
{(resultImg || stats.count > 0) && (
Zone Population
{stats.count}
)}
{/* Anomaly banner */}
{anomalyActive && (
ANOMALY DETECTED — CHAOS / COUNTERFLOW
)}
{/* Predictive warning overlay */}
{predictAlert && !anomalyActive && (
PREDICTIVE ALERT — CAPACITY BREACH IMMINENT (~{predicted} subjects)
)}
{/* ════════ RIGHT INTELLIGENCE PANEL ════════ */}
{/* ════════ BOTTOM ANALYTICS PANEL ════════ */}
{/* Metric: Zone Population */}
Zone Pop.
{stats.count}
Current frame
{/* Metric: Unique Subjects */}
Unique
{stats.unique}
Tracked subjects
{/* Metric: Avg Density (GAP 3) */}
Density
{density.label}
Avg {avgCount} · Peak {peakCount}
{/* Metric: Latency / Frames */}
{fileType === 'video'
? (stats.frames || 0)
: (stats.latency > 0 ? stats.latency.toFixed(2) : '—')}
{fileType === 'video' ? 'frames processed' : 'seconds'}
{/* Chart (GAP 1: time-formatted X-axis) */}
Population Dynamics Timeline
{history.length > 0 ? (
{/* GAP 1: X-axis shows HH:MM:SS time label */}
} />
{settings.capacity > 0 && (
)}
) : (
Run an image scan or video stream to populate analytics.
)}
);
}