| 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> |
| ); |
| } |
|
|