import { useState, useRef, useCallback, useEffect, type ChangeEvent } from 'react'; import { useLocation } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import Sidebar from '../../components/layout/Sidebar'; import { RotateCcw, Loader2, ShieldAlert, ShieldCheck, CheckCircle2, XCircle, HelpCircle, ScanSearch, Upload, Zap, Brain, Eye, Waves, Fingerprint, GitFork, CheckCircle, X, AlertCircle } from 'lucide-react'; import { analyzeImage, type ImageAnalysisResponse } from '../../services/imageService'; import ForensicLens from './ForensicLens'; import MetadataInspector from './MetadataInspector'; import ImageReportPanel from './ImageReportPanel'; import { useAuth } from '../../hooks/useAuth.tsx'; // ── VERDICT CONFIG: matches backend strings exactly ──────────────── const VERDICT_CONFIG = { 'AI GENERATED': { color: '#ef4444', bg: 'rgba(239,68,68,0.08)', border: 'rgba(239,68,68,0.25)', icon: XCircle, label: 'AI GENERATED', badge: 'SYNTHETIC', glow: '0 0 40px rgba(239,68,68,0.2)', }, 'UNCERTAIN': { color: '#eab308', bg: 'rgba(234,179,8,0.08)', border: 'rgba(234,179,8,0.25)', icon: HelpCircle, label: 'UNCERTAIN', badge: 'REVIEW', glow: '0 0 40px rgba(234,179,8,0.15)', }, 'LIKELY HUMAN': { color: '#22c55e', bg: 'rgba(34,197,94,0.08)', border: 'rgba(34,197,94,0.25)', icon: ShieldCheck, label: 'REAL PHOTO', badge: 'VERIFIED', glow: '0 0 40px rgba(34,197,94,0.15)', }, } as const; type VerdictKey = keyof typeof VERDICT_CONFIG; // ── MODULE CONFIG: all 8 forensic signals ──────────────────────── const MODULE_CONFIG = [ { key: 'rigid', label: 'RIGID / DINOv2', desc: 'Perturbation sensitivity', icon: Brain, color: '#00E5CC', weight: 'HIGH', }, { key: 'classifier', label: 'SigLIP / ViT', desc: 'Neural classifier ensemble', icon: Eye, color: '#00E5CC', weight: 'HIGH', }, { key: 'clip', label: 'CLIP Semantic', desc: 'Zero-shot domain gap', icon: Zap, color: '#f97316', weight: 'MED', }, { key: 'exif', label: 'EXIF Guard', desc: 'Metadata provenance', icon: Fingerprint, color: '#22c55e', weight: 'HIGH', }, { key: 'noise', label: 'PRNU Noise', desc: 'Sensor fingerprint', icon: Waves, color: '#e879f9', weight: 'MED', }, { key: 'fft', label: 'FFT Spectral', desc: '1/f² power law deviation', icon: GitFork, color: '#38bdf8', weight: 'LOW', }, { key: 'ela', label: 'ELA', desc: 'Compression uniformity', icon: ScanSearch, color: '#eab308', weight: 'LOW', }, { key: 'aug', label: 'Aug Consistency', desc: 'Classifier stability test', icon: ShieldAlert, color: '#fb7185', weight: 'MED', }, { key: 'c2pa', label: 'C2PA Provenance', desc: 'Content Credentials', icon: CheckCircle2, color: '#3b82f6', weight: 'ULTRA', }, ] as const; // ── HELPERS ──────────────────────────────────────────────────────── const scoreColor = (s: number): string => { if (s >= 0.72) return '#ef4444'; if (s >= 0.55) return '#f97316'; if (s >= 0.40) return '#eab308'; return '#22c55e'; }; const scoreLabel = (s: number): string => { if (s >= 0.72) return 'HIGH PROBABILITY'; if (s >= 0.55) return 'ELEVATED'; if (s >= 0.40) return 'UNCERTAIN'; return 'AUTHENTIC'; }; import DashboardLayout from '../../components/layout/DashboardLayout'; // ── MAIN COMPONENT ───────────────────────────────────────────────── const ImageLabPage = () => { const { token } = useAuth(); const location = useLocation(); const [isAnalyzing, setIsAnalyzing] = useState(false); const [result, setResult] = useState<{ status: string; data: ImageAnalysisResponse } | null>(null); const [previewUrl, setPreviewUrl] = useState(null); const [isDragging, setIsDragging] = useState(false); const [error, setError] = useState(null); const fileInputRef = useRef(null); useEffect(() => { const params = new URLSearchParams(location.search); const scanId = params.get('scan_id'); if (scanId && token) { const fetchScan = async () => { setIsAnalyzing(true); try { const res = await fetch(`http://127.0.0.1:8001/api/v1/dashboard/scan/${scanId}`, { headers: { Authorization: `Bearer ${token}` } }); if (res.ok) { const json = await res.json(); const scanData = json.data.full_result || json.data; setResult({ status: 'success', data: scanData }); // For historical images, we might not have the original base64 if it wasn't saved. // But we can show the heatmap if available. if (scanData.heatmap_url) setPreviewUrl(scanData.heatmap_url); } } catch (err) { console.error("Failed to load historical scan:", err); } finally { setIsAnalyzing(false); } }; fetchScan(); } }, [location.search, token]); // Auto-dismiss errors after 6s useEffect(() => { if (error) { const timer = setTimeout(() => setError(null), 6000); return () => clearTimeout(timer); } }, [error]); const data = result?.data; // ── Map verdict → config (with fallback to UNCERTAIN) ── const verdictKey = (data?.verdict ?? 'UNCERTAIN') as VerdictKey; const vCfg = VERDICT_CONFIG[verdictKey] ?? VERDICT_CONFIG['UNCERTAIN']; const processFile = useCallback(async (file: File) => { if (!file.type.startsWith('image/')) return; setIsAnalyzing(true); setResult(null); // Show preview immediately const previewReader = new FileReader(); previewReader.onloadend = () => setPreviewUrl(previewReader.result as string); previewReader.readAsDataURL(file); // Analyze const b64Reader = new FileReader(); b64Reader.onloadend = async () => { try { const dataUrl = b64Reader.result as string; if (!dataUrl?.startsWith('data:image/')) { throw new Error('Invalid image data. Please upload a valid image file.'); } const res = await analyzeImage(dataUrl, token); setResult(res); } catch (e: any) { let msg = e.message || 'An unknown anomaly occurred during signal extraction.'; if (msg === 'Failed to fetch') { msg = 'Backend Connection Lost: Is the forensic server running offline?'; } setError(msg); setResult(null); setPreviewUrl(null); } finally { setIsAnalyzing(false); } }; b64Reader.readAsDataURL(file); }, []); const handleFileChange = (e: ChangeEvent) => { const f = e.target.files?.[0]; if (f) processFile(f); e.target.value = ''; }; const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const f = e.dataTransfer.files[0]; if (f) processFile(f); }, [processFile]); const reset = () => { setResult(null); setPreviewUrl(null); setIsAnalyzing(false); }; const showWorkspace = previewUrl || isAnalyzing; return (

Image lab

Neural visual authentication suite

{showWorkspace && ( )}
{/* ── Upload Zone ── */} {!showWorkspace ? (
{/* Drop zone */}
fileInputRef.current?.click()} onDragOver={(e) => { e.preventDefault(); setIsDragging(true); }} onDragLeave={() => setIsDragging(false)} onDrop={handleDrop} >

Upload Image for Analysis

Drag & drop or click to upload · JPG, PNG, WEBP · Max 20MB

Select Image
) : ( /* ── Analysis Workspace ── */
{/* TOP ROW: FORENSIC LENS + MASTER GAUGE */}
{/* LEFT: Image Viewer (8/12) */}
{/* RIGHT: Master Verdict Gauge (4/12) */}
{isAnalyzing ? (

PROCESSING IMAGE PATTERNS...

) : data ? ( <> {/* Circular gauge */}
{Math.round(data.ai_probability * 100)}% AI PROBABILITY SCORE
{/* Verdict label */}
{(() => { const Icon = vCfg.icon; return ; })()} {vCfg.label}
{vCfg.badge} Confidence: {data.confidence.toFixed(1)}%
) : (

Awaiting Image Content

)}
{/* BOTTOM ROW: MODULE SCORECARD + REPORT + METADATA */}
{/* LEFT: Forensic Scorecard (5/12) */}
Signal analysis breakdown
{isAnalyzing ? ( [...Array(8)].map((_, i) => (
)) ) : data?.signals ? ( MODULE_CONFIG.map((mod) => { const rawScore = (data.signals as any)[mod.key] as number | undefined; const score = rawScore ?? 0; const pct = Math.round(score * 100); const col = scoreColor(score); const Icon = mod.icon; return (
{mod.label}
{mod.desc}
{rawScore !== undefined ? `${pct}%` : '—'}
{scoreLabel(score)}
); }) ) : (
Upload an image to start signal extraction
)}
{/* RIGHT: Reasoning & Metadata (7/12) */}
{/* Reasoning Panel */} {data && ( )} {/* Metadata Inspector */} {(data || isAnalyzing) && (
{isAnalyzing ? (
{[...Array(4)].map((_, i) =>
)}
) : data?.metadata ? ( ) : null}
)}
)}
{/* ── Premium Error Toast Popup ── */} {error && (

System Error

{error}

)}
); }; export default ImageLabPage;