Spaces:
Sleeping
Sleeping
| import { motion } from 'framer-motion'; | |
| import { Loader2, FileImage, FileVideo, Cpu, Activity, BarChart3, Eye, CheckCircle2 } from 'lucide-react'; | |
| import styles from './AnalysisProgress.module.css'; | |
| const STAGE_ICONS = { | |
| upload: FileImage, | |
| preprocess: Cpu, | |
| inference: Activity, | |
| gradcam: Eye, | |
| fft: BarChart3, | |
| temporal: FileVideo, | |
| verdict: CheckCircle2, | |
| }; | |
| const SKELETONS = Array.from({ length: 3 }); | |
| export default function AnalysisProgress({ phase, currentStage, progress, file }) { | |
| const isVideo = file?.type?.startsWith('video/'); | |
| const StageIcon = currentStage ? (STAGE_ICONS[currentStage.id] || Cpu) : Cpu; | |
| return ( | |
| <div className={styles.container}> | |
| <motion.div | |
| className={styles.card} | |
| initial={{ opacity: 0, y: 24 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ duration: 0.4 }} | |
| > | |
| {/* Header */} | |
| <div className={styles.header}> | |
| <div className={styles.headerLeft}> | |
| <div className={styles.spinnerWrap}> | |
| <Loader2 size={18} className={styles.spinner} /> | |
| </div> | |
| <div> | |
| <p className={styles.headerTitle}>Forensic Analysis in Progress</p> | |
| <p className={styles.headerSub}>{file?.name}</p> | |
| </div> | |
| </div> | |
| <div className={styles.progressBadge}> | |
| <span className="font-mono">{progress}%</span> | |
| </div> | |
| </div> | |
| {/* Progress bar */} | |
| <div className={styles.progressTrack}> | |
| <motion.div | |
| className={styles.progressBar} | |
| initial={{ width: 0 }} | |
| animate={{ width: `${progress}%` }} | |
| transition={{ ease: 'easeOut', duration: 0.4 }} | |
| /> | |
| </div> | |
| {/* Current stage */} | |
| {currentStage && ( | |
| <motion.div | |
| className={styles.stage} | |
| key={currentStage.id} | |
| initial={{ opacity: 0, x: -8 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| transition={{ duration: 0.2 }} | |
| > | |
| <div className={styles.stageIcon}> | |
| <StageIcon size={14} /> | |
| </div> | |
| <span>{currentStage.label}</span> | |
| </motion.div> | |
| )} | |
| {/* Skeleton preview */} | |
| <div className={styles.skeletonGrid}> | |
| {/* Media skeleton */} | |
| <div className={styles.skeletonMediaWrap}> | |
| <div className={styles.skeletonMedia}> | |
| <div className={styles.shimmer} /> | |
| </div> | |
| <div className={styles.skeletonLines}> | |
| <div className={`${styles.skeletonLine} ${styles.skeletonLineLong}`}><div className={styles.shimmer} /></div> | |
| <div className={`${styles.skeletonLine} ${styles.skeletonLineMed}`}><div className={styles.shimmer} /></div> | |
| <div className={`${styles.skeletonLine} ${styles.skeletonLineShort}`}><div className={styles.shimmer} /></div> | |
| </div> | |
| </div> | |
| {/* Verdict skeleton */} | |
| <div className={styles.skeletonVerdictWrap}> | |
| <div className={styles.skeletonScoreCircle}><div className={styles.shimmer} /></div> | |
| {SKELETONS.map((_, i) => ( | |
| <div key={i} className={styles.skeletonBlock}><div className={styles.shimmer} /></div> | |
| ))} | |
| </div> | |
| </div> | |
| {/* Models running */} | |
| <div className={styles.modelsRunning}> | |
| <p className={styles.modelsLabel}>Models running</p> | |
| <div className={styles.modelsList}> | |
| {['EfficientNet-B4', 'Xception', 'ViT Forensic', ...(isVideo ? ['Temporal CNN'] : [])].map((m, i) => ( | |
| <div key={m} className={styles.modelItem}> | |
| <motion.div | |
| className={styles.modelDot} | |
| animate={{ opacity: [0.3, 1, 0.3] }} | |
| transition={{ duration: 1.4, repeat: Infinity, delay: i * 0.3 }} | |
| /> | |
| <span className="font-mono">{m}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </motion.div> | |
| </div> | |
| ); | |
| } | |