| import { useState } from 'react'; |
| import { motion } from 'framer-motion'; |
| import { ArrowLeft, Download, RotateCcw, Loader2, CheckCircle2 } from 'lucide-react'; |
| import MediaPanel from './MediaPanel'; |
| import VerdictCard from './VerdictCard'; |
| import TemporalTimeline from './TemporalTimeline'; |
| import TechnicalMetadata from './TechnicalMetadata'; |
| import AssessmentPanels from './AssessmentPanels'; |
| import { downloadReport } from '../utils/generateReport'; |
| import styles from './ForensicDashboard.module.css'; |
|
|
| export default function ForensicDashboard({ result, previewUrl, onReset }) { |
| const isVideo = result.type === 'video'; |
| const [exportState, setExportState] = useState('idle'); |
|
|
| const handleExport = async () => { |
| if (exportState !== 'idle') return; |
| setExportState('loading'); |
| |
| await new Promise(r => setTimeout(r, 80)); |
| try { |
| downloadReport(result, previewUrl); |
| } catch (e) { |
| console.error('Export failed:', e); |
| } |
| setExportState('done'); |
| setTimeout(() => setExportState('idle'), 2800); |
| }; |
|
|
| return ( |
| <motion.div |
| className={styles.dashboard} |
| initial={{ opacity: 0 }} |
| animate={{ opacity: 1 }} |
| transition={{ duration: 0.4 }} |
| > |
| {/* Top bar */} |
| <div className={styles.topBar}> |
| <div className={styles.topBarLeft}> |
| <button className={styles.backBtn} onClick={onReset}> |
| <ArrowLeft size={15} /> |
| <span>New Analysis</span> |
| </button> |
| <div className={styles.breadcrumb}> |
| <span>Dashboard</span> |
| <span className={styles.breadcrumbSep}>/</span> |
| <span className={styles.breadcrumbFile}>{result.filename}</span> |
| </div> |
| </div> |
| <div className={styles.topBarRight}> |
| <button |
| className={`${styles.actionBtn} ${styles.actionBtnExport} ${exportState === 'done' ? styles.exportDone : ''}`} |
| onClick={handleExport} |
| disabled={exportState === 'loading'} |
| title="Export full forensic report as HTML" |
| > |
| {exportState === 'loading' ? ( |
| <> |
| <Loader2 size={14} className={styles.spinIcon} /> |
| <span>Generating…</span> |
| </> |
| ) : exportState === 'done' ? ( |
| <> |
| <CheckCircle2 size={14} /> |
| <span>Downloaded!</span> |
| </> |
| ) : ( |
| <> |
| <Download size={14} /> |
| <span>Export Report</span> |
| </> |
| )} |
| </button> |
|
|
| <button className={`${styles.actionBtn} ${styles.actionBtnPrimary}`} onClick={onReset}> |
| <RotateCcw size={14} /> |
| <span>Analyse Again</span> |
| </button> |
| </div> |
| </div> |
|
|
| {} |
| <div className={styles.splitLayout}> |
| {} |
| <div className={styles.leftCol}> |
| <MediaPanel result={result} previewUrl={previewUrl} /> |
|
|
| {isVideo && result.timeline && ( |
| <motion.div |
| initial={{ opacity: 0, y: 16 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ delay: 0.3 }} |
| > |
| <TemporalTimeline |
| timeline={result.timeline} |
| duration={result.duration?.replace('00:00:', '') || '23'} |
| totalFrames={result.totalFrames} |
| /> |
| </motion.div> |
| )} |
|
|
| {(result.neurosymbolic || result.ethical) && ( |
| <motion.div |
| initial={{ opacity: 0, y: 16 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ delay: 0.35 }} |
| > |
| <AssessmentPanels result={result} /> |
| </motion.div> |
| )} |
|
|
| <motion.div |
| initial={{ opacity: 0, y: 16 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ delay: 0.4 }} |
| > |
| <TechnicalMetadata result={result} /> |
| </motion.div> |
| </div> |
|
|
| {} |
| <div className={styles.rightCol}> |
| <motion.div |
| initial={{ opacity: 0, x: 20 }} |
| animate={{ opacity: 1, x: 0 }} |
| transition={{ delay: 0.15 }} |
| > |
| <VerdictCard result={result} /> |
| </motion.div> |
| </div> |
| </div> |
| </motion.div> |
| ); |
| } |
|
|