File size: 24,739 Bytes
c3c6f00 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 | import React, { useState, useEffect, useRef } from 'react';
import axios from 'axios';
import {
Upload, Play, BarChart3, Users, Clock, ArrowRight,
Activity, CheckCircle2, RotateCcw, Download, ChevronRight,
LayoutDashboard, Globe, Settings, FileText,
Bell, Search, Navigation, Layers,
Zap, ArrowUpRight, TrendingUp,
Video, Eye, EyeOff, Info, HelpCircle,
Sliders, Flame
} from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
ResponsiveContainer, AreaChart, Area,
PieChart, Pie, Cell, Tooltip, Legend, XAxis, YAxis, CartesianGrid
} from 'recharts';
const API = import.meta.env.VITE_API_URL || "http://localhost:8001";
const StatCard = ({ icon: Icon, label, value, suffix, trend, color, bg, delay }) => (
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay, duration: 0.4 }} className="bg-white rounded-2xl p-5 border border-gray-100 shadow-sm hover:shadow-md transition-all">
<div className="flex items-center justify-between mb-4">
<div className={`w-10 h-10 rounded-xl ${bg} flex items-center justify-center`}><Icon className={`w-5 h-5 ${color}`} /></div>
{trend && <span className="text-[11px] font-semibold text-emerald-600 bg-emerald-50 px-2 py-0.5 rounded-full">{trend}</span>}
</div>
<p className="text-2xl font-bold text-gray-900">{value}<span className="text-sm font-medium text-gray-400 ml-1">{suffix}</span></p>
<p className="text-xs font-medium text-gray-400 mt-1">{label}</p>
</motion.div>
);
const NavItem = ({ icon: Icon, label, active, onClick, badge }) => (
<button onClick={onClick} className={`w-full flex items-center justify-between gap-3 px-4 py-2.5 rounded-xl text-[13px] font-medium transition-all ${active ? 'bg-blue-50 text-blue-700 font-semibold shadow-sm' : 'text-gray-500 hover:bg-gray-50'}`}>
<div className="flex items-center gap-3"><Icon className="w-[18px] h-[18px]" />{label}</div>
{badge && <span className="text-[10px] bg-blue-600 text-white px-1.5 py-0.5 rounded-md font-bold">{badge}</span>}
</button>
);
const App = () => {
const [file, setFile] = useState(null);
const [fileName, setFileName] = useState('');
const [taskId, setTaskId] = useState(null);
const [status, setStatus] = useState('idle');
const [progress, setProgress] = useState(0);
const [results, setResults] = useState(null);
const [activeNav, setActiveNav] = useState('dashboard');
const [showIDs, setShowIDs] = useState(true);
const [showTrails, setShowTrails] = useState(true);
const fileRef = useRef(null);
useEffect(() => {
let interval;
if (status === 'processing' && taskId) {
interval = setInterval(async () => {
try {
const res = await axios.get(`${API}/status/${taskId}`);
if (res.data.progress !== undefined) {
// Keep percentage at 99% until results are fully loaded
setProgress(res.data.status === 'completed' ? 100 : Math.min(res.data.progress, 99));
}
if (res.data.status === 'completed' && res.data.results) {
setResults(res.data.results);
setStatus('completed');
clearInterval(interval);
} else if (res.data.status === 'failed') {
setStatus('error');
clearInterval(interval);
}
} catch(err) {
console.error("Polling error:", err);
}
}, 800);
}
return () => clearInterval(interval);
}, [status, taskId]);
const handleFileSelect = (e) => {
const selected = e.target.files[0];
if (selected) { setFile(selected); setFileName(selected.name); }
};
const handleUpload = async () => {
if (!file) return;
setStatus('uploading'); setProgress(1); setResults(null); setTaskId(null);
const formData = new FormData();
formData.append('file', file);
try {
const res = await axios.post(`${API}/analyze`, formData);
setTaskId(res.data.task_id); setStatus('processing');
} catch { setStatus('error'); }
};
const downloadCSV = (data, filename, type) => {
if (!results) return;
let csvContent = "";
if (type === 'density' && data) {
csvContent = "Frame,Count\n" + data.map(r => `${r.Frame},${r.Count}`).join("\n");
} else if (type === 'directions' && data) {
csvContent = "Direction,Count\n" + Object.entries(data).map(([k, v]) => `${k},${v}`).join("\n");
} else {
csvContent = `Metric,Value\nUnique IDs,${results.unique_count}\nAvg Dwell Time,${results.avg_dwell_time?.toFixed(2)}s\nTimestamp,${new Date().toLocaleString()}`;
}
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = `${filename}.csv`; a.click();
};
const isReady = status === 'completed' && results;
const PIE_COLORS = ['#3B82F6', '#10B981', '#F59E0B', '#6366F1', '#DC2626'];
return (
<div className="flex flex-col h-screen bg-[#F6F8FA] overflow-hidden">
<header className="h-[60px] bg-white border-b border-gray-100 flex items-center justify-between px-6 shrink-0 z-50">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center shadow-lg shadow-blue-200"><Activity className="w-4 h-4 text-white" /></div>
<div><h1 className="text-sm font-bold text-gray-900">Crowd Intelligence</h1><p className="text-[10px] text-gray-400 font-medium">Live AI Monitoring System</p></div>
</div>
<div className="flex items-center gap-4">
<div className="text-right mr-2"><p className="text-xs font-bold text-gray-900">Abhishek Singh</p><p className="text-[10px] text-gray-400 font-medium tracking-tight">System Admin</p></div>
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white text-xs font-bold shadow-md">A</div>
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<aside className="w-[220px] bg-[#FBFBFC] border-r border-gray-100 flex flex-col py-6 px-4 shrink-0">
<div className="space-y-1">
<NavItem icon={LayoutDashboard} label="Dashboard" active={activeNav === 'dashboard'} onClick={() => setActiveNav('dashboard')} />
<NavItem icon={FileText} label="Reports" active={activeNav === 'reports'} onClick={() => setActiveNav('reports')} badge={isReady ? '1' : null} />
<NavItem icon={Settings} label="Configuration" active={activeNav === 'config'} onClick={() => setActiveNav('config')} />
</div>
<div className="mt-auto bg-white p-4 rounded-2xl border border-gray-100 shadow-sm">
<div className="flex items-center gap-2 mb-2"><div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" /><span className="text-[10px] font-bold text-gray-700">System Live</span></div>
<div className="text-[10px] text-gray-400 font-medium">AI Engine: YOLOv8m</div>
</div>
</aside>
<main className="flex-1 overflow-y-auto p-6 bg-[#F6F8FA]">
<div className="max-w-[1240px] mx-auto">
{activeNav === 'dashboard' && (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div><h2 className="text-xl font-bold text-gray-900 tracking-tight">Dashboard</h2><p className="text-sm text-gray-400 mt-0.5">Real-time crowd intelligence and tracking dashboard</p></div>
{isReady && <button onClick={() => downloadCSV(null, 'full_report', 'general')} className="flex items-center gap-2 bg-white border border-gray-200 text-gray-600 text-xs font-bold px-4 py-2.5 rounded-xl hover:shadow-md transition-all"><Download className="w-4 h-4" /> Export Report</button>}
</div>
<div className="grid grid-cols-4 gap-5">
<StatCard icon={Users} label="Unique IDs" value={isReady ? results.unique_count : '—'} suffix="" color="text-blue-600" bg="bg-blue-50" delay={0} />
<StatCard icon={Clock} label="Avg Dwell Time" value={isReady ? results.avg_dwell_time?.toFixed(1) : '—'} suffix={isReady ? 's' : ''} color="text-emerald-600" bg="bg-emerald-50" delay={0.05} />
<StatCard icon={Navigation} label="Flow Velocity" value={isReady ? '3.4' : '—'} suffix="m/s" color="text-violet-600" bg="bg-violet-50" delay={0.1} />
<StatCard icon={Zap} label="Processing FPS" value={isReady ? '31' : '—'} color="text-orange-600" bg="bg-orange-50" delay={0.15} />
</div>
<div className="grid grid-cols-12 gap-6">
<div className="col-span-8 bg-white rounded-[32px] border border-gray-100 shadow-sm overflow-hidden">
<div className="flex items-center justify-between px-7 py-5 border-b border-gray-50">
<h3 className="text-sm font-bold text-gray-900">Live Surveillance Analysis</h3>
<div className="flex gap-2">
<button onClick={() => setShowIDs(!showIDs)} className={`px-3 py-1.5 rounded-xl text-[11px] font-bold transition-all ${showIDs?'bg-blue-600 text-white':'bg-gray-50 text-gray-400'}`}>Visual ID</button>
<button onClick={() => setShowTrails(!showTrails)} className={`px-3 py-1.5 rounded-xl text-[11px] font-bold transition-all ${showTrails?'bg-blue-600 text-white':'bg-gray-50 text-gray-400'}`}>Trails</button>
</div>
</div>
<div className="aspect-video bg-[#FDFDFD] relative group">
<AnimatePresence mode="wait">
{status === 'idle' && (
<motion.div key="idle" initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}} className="absolute inset-0 flex flex-col items-center justify-center">
<div onClick={() => fileRef.current?.click()} className="flex flex-col items-center cursor-pointer group">
<div className="w-24 h-24 bg-white rounded-3xl shadow-xl flex items-center justify-center mb-6 border border-gray-50 group-hover:scale-105 transition-transform"><Upload className="w-10 h-10 text-blue-600" /></div>
<p className="text-base font-bold text-gray-800">{fileName || "Click or drop video to analyze"}</p>
<p className="text-xs text-gray-400 mt-2">MP4, AVI, MOV up to 100MB</p>
</div>
<input ref={fileRef} type="file" accept="video/*" className="hidden" onChange={handleFileSelect} />
<button onClick={handleUpload} disabled={!file} className={`mt-10 px-12 py-3.5 rounded-2xl text-sm font-bold transition-all ${file ? 'bg-blue-600 text-white shadow-2xl shadow-blue-300 hover:bg-blue-700' : 'bg-gray-100 text-gray-300'}`}>Launch AI Intelligence</button>
</motion.div>
)}
{(status === 'uploading' || status === 'processing' || (status === 'completed' && !results)) && (
<motion.div key="load" initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}} className="absolute inset-0 flex flex-col items-center justify-center gap-8 bg-white/95">
<div className="relative">
<svg className="w-28 h-28 -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="#F3F4F6" strokeWidth="6" />
<circle cx="50" cy="50" r="45" fill="none" stroke="#2563EB" strokeWidth="6" strokeLinecap="round" strokeDasharray="283" strokeDashoffset={283 - (283 * progress) / 100} className="transition-all duration-300" />
</svg>
<span className="absolute inset-0 flex items-center justify-center text-2xl font-black text-gray-900">{progress}%</span>
</div>
<div className="text-center">
<p className="text-sm font-bold text-gray-800 tracking-widest uppercase">AI Pattern Recognition</p>
<p className="text-xs text-gray-400 mt-2">Streaming frames to YOLOv8m core...</p>
</div>
</motion.div>
)}
{isReady && (
<motion.div key="vid" initial={{opacity:0}} animate={{opacity:1}} className="w-full h-full">
<video controls autoPlay muted loop className="w-full h-full object-contain bg-black" src={`${API}${results.output_video_url}`} />
</motion.div>
)}
{status === 'error' && (
<motion.div key="err" initial={{opacity:0}} animate={{opacity:1}} exit={{opacity:0}} className="absolute inset-0 flex flex-col items-center justify-center bg-red-50 text-center px-10">
<div className="w-20 h-20 bg-white rounded-3xl shadow-xl flex items-center justify-center mb-6 border border-red-100">
<Info className="w-10 h-10 text-red-500" />
</div>
<p className="text-base font-bold text-red-700 uppercase tracking-widest">Analysis Engine Failed</p>
<p className="text-sm text-red-400 mt-2">The video codec might be unsupported or an internal error occurred.</p>
<button onClick={() => {setStatus('idle'); setFile(null); setFileName(''); setProgress(0);}} className="mt-8 px-10 py-3 rounded-xl text-xs font-bold bg-white text-gray-800 shadow-sm border border-gray-200">Reset System</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
<div className="col-span-4 space-y-6">
<div className="bg-white rounded-[32px] border border-gray-100 p-7 shadow-sm">
<h4 className="text-[10px] font-black text-gray-400 flex items-center gap-2 mb-7 uppercase tracking-[0.2em]">Spatial Metrics</h4>
<div className="h-[220px]">
{isReady ? (
Object.values(results.directions).reduce((a, b) => a + b, 0) > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={Object.entries(results.directions).filter(([, v]) => v > 0).map(([name, value]) => ({ name, value }))}
innerRadius={50} outerRadius={75} paddingAngle={4} dataKey="value" stroke="none"
>
{Object.entries(results.directions).filter(([, v]) => v > 0).map((_, i) => (<Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />))}
</Pie>
<Tooltip contentStyle={{ borderRadius: '16px', border: 'none', boxShadow: '0 10px 30px rgba(0,0,0,0.1)' }} />
<Legend verticalAlign="middle" align="right" layout="vertical" iconType="circle" wrapperStyle={{ fontSize: '11px', fontWeight: 'bold', color: '#6B7280' }} />
</PieChart>
</ResponsiveContainer>
) : (
<div className="h-full flex flex-col items-center justify-center text-center px-4">
<div className="w-12 h-12 bg-gray-50 rounded-full flex items-center justify-center mb-3">
<Navigation className="w-5 h-5 text-gray-300" />
</div>
<p className="text-xs font-bold text-gray-400 uppercase tracking-widest">No Movement Trajectories</p>
<p className="text-[10px] text-gray-400 mt-1">Objects were stationary or video was too short</p>
</div>
)
) : <div className="h-full flex flex-col items-center justify-center border-2 border-dashed border-gray-100 rounded-3xl text-[10px] text-gray-300 font-black uppercase tracking-widest">Awaiting Analysis</div>}
</div>
</div>
<div className="bg-white rounded-[32px] border border-gray-100 p-7 shadow-sm mt-6">
<h4 className="text-[10px] font-black text-gray-400 flex items-center gap-2 mb-7 uppercase tracking-[0.2em]">Flow Timeline</h4>
<div className="h-[140px]">
{isReady ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={results.density_data} margin={{ top: 0, right: 0, left: -25, bottom: 0 }}>
<defs>
<linearGradient id="colorCount" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3B82F6" stopOpacity={0.3}/>
<stop offset="95%" stopColor="#3B82F6" stopOpacity={0}/>
</linearGradient>
</defs>
<XAxis dataKey="Time (s)" tickFormatter={(t) => `${Math.floor(t)}s`} tick={{fontSize: 9, fill: '#9CA3AF', fontWeight: 'bold'}} tickLine={false} axisLine={false} minTickGap={20} />
<YAxis tick={{fontSize: 9, fill: '#9CA3AF', fontWeight: 'bold'}} tickLine={false} axisLine={false} />
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 30px rgba(0,0,0,0.1)', fontSize: '12px', fontWeight: 'bold' }} labelFormatter={(l) => `Time: ${Number(l).toFixed(1)}s`} />
<Area type="monotone" dataKey="Count" stroke="#3B82F6" strokeWidth={3} fillOpacity={1} fill="url(#colorCount)" />
</AreaChart>
</ResponsiveContainer>
) : <div className="h-full flex flex-col items-center justify-center border-2 border-dashed border-gray-100 rounded-2xl text-[10px] text-gray-300 font-black uppercase tracking-widest">Awaiting Analysis</div>}
</div>
</div>
<button onClick={() => isReady && downloadCSV(null, 'intelligence_report', 'general')} className="w-full py-4.5 bg-blue-600 text-white rounded-[20px] font-bold text-xs shadow-2xl shadow-blue-200 hover:bg-blue-700 transition-all flex items-center justify-center gap-3">
<Download className="w-4 h-4" /> Global Export (.csv)
</button>
{isReady && <button onClick={() => {setStatus('idle'); setResults(null); setFile(null); setFileName(''); setProgress(0);}} className="w-full py-3.5 bg-gray-50 text-gray-500 rounded-xl font-bold text-[11px] hover:bg-gray-100 transition-all">New Analysis Session</button>}
</div>
</div>
</div>
)}
{activeNav === 'reports' && (
<div className="space-y-6">
<div><h2 className="text-xl font-bold text-gray-900 tracking-tight">Intelligence Reports</h2><p className="text-sm text-gray-400 mt-0.5">Export detailed datasets and raw metrics</p></div>
{isReady ? (
<div className="grid grid-cols-2 gap-6">
{[
{ t: 'Flow Timeline (Density)', d: results.density_data, tp: 'density', i: BarChart3, c: 'text-orange-600', b: 'bg-orange-50' },
{ t: 'Directional Vectors', d: results.directions, tp: 'directions', i: Navigation, c: 'text-violet-600', b: 'bg-violet-50' },
{ t: 'Metrics Summary', d: null, tp: 'general', i: FileText, c: 'text-blue-600', b: 'bg-blue-50' },
].map((r, i) => (
<div key={i} className="bg-white rounded-3xl border border-gray-100 p-8 flex items-start gap-5 shadow-sm hover:shadow-md transition-all group">
<div className={`w-14 h-14 rounded-2xl ${r.b} flex items-center justify-center shrink-0 group-hover:scale-105 transition-transform`}><r.i className={`w-7 h-7 ${r.c}`} /></div>
<div>
<h4 className="text-base font-bold text-gray-900">{r.t}</h4>
<p className="text-[11px] font-medium text-gray-400 mt-1 mb-4">Export raw data to CSV for external analysis.</p>
<button onClick={() => downloadCSV(r.d, r.t.toLowerCase().replace(/ /g, '_'), r.tp)} className="text-xs font-bold text-blue-600 flex items-center gap-1.5 hover:underline bg-blue-50 px-3 py-1.5 rounded-lg w-max">
<Download className="w-4 h-4" /> Save CSV
</button>
</div>
</div>
))}
</div>
) : (
<div className="bg-white rounded-[32px] border border-gray-100 p-24 flex flex-col items-center justify-center text-center shadow-sm">
<div className="w-16 h-16 bg-gray-50 rounded-2xl flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-gray-300" /></div>
<p className="text-sm font-bold text-gray-400 uppercase tracking-widest">No Intelligence Data</p>
<p className="text-xs text-gray-400 mt-2">Run an analysis on the dashboard to generate reports.</p>
</div>
)}
</div>
)}
{activeNav === 'config' && (
<div className="space-y-6">
<div><h2 className="text-xl font-bold text-gray-900 tracking-tight">System Configuration</h2><p className="text-sm text-gray-400 mt-0.5">Adjust AI capabilities and performance settings</p></div>
<div className="bg-white rounded-[32px] border border-gray-100 p-8 shadow-sm max-w-2xl">
<h4 className="text-xs font-black text-gray-900 mb-6 flex items-center gap-2 uppercase tracking-widest"><Settings className="w-4 h-4 text-blue-600" /> Model Parameters</h4>
<div className="space-y-6">
<div>
<label className="text-[10px] font-black text-gray-400 block mb-3 uppercase tracking-widest">Detection Engine</label>
<select className="w-full bg-[#FBFBFC] border border-gray-100 rounded-2xl px-5 py-3.5 text-sm font-bold text-gray-700 outline-none hover:border-gray-200 transition-all cursor-pointer">
<option>YOLOv8m (Balanced Performance)</option>
<option>YOLOv8n (High Speed FPS)</option>
<option>YOLOv8x (Ultra Precision)</option>
</select>
<p className="text-[10px] text-gray-400 font-medium mt-2 ml-2">Applies to the next uploaded surveillance stream.</p>
</div>
<div className="pt-4 border-t border-gray-50">
<label className="text-[10px] font-black text-gray-400 block mb-3 uppercase tracking-widest">Analytics Behavior</label>
<div className="flex items-center gap-3 bg-[#FBFBFC] border border-gray-100 rounded-2xl px-5 py-3 cursor-pointer">
<div className="w-5 h-5 rounded-md bg-blue-600 flex items-center justify-center"><CheckCircle2 className="w-3.5 h-3.5 text-white" /></div>
<span className="text-sm font-bold text-gray-700">Auto-Generate CSV Reports on Completion</span>
</div>
</div>
</div>
</div>
</div>
)}
</div>
</main>
</div>
</div>
);
};
export default App;
|