| import { useRef, useState, useCallback } from 'react'; |
| import { motion, AnimatePresence } from 'framer-motion'; |
| import { Upload, FileImage, FileVideo, AlertCircle, Sparkles } from 'lucide-react'; |
| import CameraCapture from './CameraCapture'; |
| import styles from './UploadZone.module.css'; |
|
|
| const MAX_SIZE_MB = 200; |
| const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/tiff', 'image/avif']; |
| const ACCEPTED_VIDEO_TYPES = ['video/mp4', 'video/webm', 'video/quicktime', 'video/x-msvideo', 'video/avi']; |
|
|
| export default function UploadZone({ onFileSelect, error }) { |
| const inputRef = useRef(null); |
| const [isDragging, setIsDragging] = useState(false); |
| const [dragType, setDragType] = useState(null); |
| const [validationError, setValidationError] = useState(null); |
|
|
| const validateFile = useCallback((file) => { |
| if (!file) return 'No file selected'; |
| const isImage = ACCEPTED_IMAGE_TYPES.includes(file.type) || file.type.startsWith('image/'); |
| const isVideo = ACCEPTED_VIDEO_TYPES.includes(file.type) || file.type.startsWith('video/'); |
| if (!isImage && !isVideo) return 'Unsupported file format. Please use an image or video file.'; |
| if (file.size > MAX_SIZE_MB * 1024 * 1024) return `File is too large. Maximum size is ${MAX_SIZE_MB} MB.`; |
| return null; |
| }, []); |
|
|
| const handleDragOver = useCallback((event) => { |
| event.preventDefault(); |
| const file = event.dataTransfer?.items?.[0]; |
| if (!file) return; |
| setIsDragging(true); |
| if (file.type.startsWith('image/')) setDragType('image'); |
| else if (file.type.startsWith('video/')) setDragType('video'); |
| else setDragType('invalid'); |
| }, []); |
|
|
| const handleDragLeave = useCallback((event) => { |
| event.preventDefault(); |
| setIsDragging(false); |
| setDragType(null); |
| }, []); |
|
|
| const handleDrop = useCallback((event) => { |
| event.preventDefault(); |
| setIsDragging(false); |
| setDragType(null); |
| const file = event.dataTransfer?.files?.[0]; |
| if (!file) return; |
| const validation = validateFile(file); |
| if (validation) { |
| setValidationError(validation); |
| return; |
| } |
| setValidationError(null); |
| onFileSelect(file); |
| }, [validateFile, onFileSelect]); |
|
|
| const handleInputChange = useCallback((event) => { |
| const file = event.target.files?.[0]; |
| if (!file) return; |
| const validation = validateFile(file); |
| if (validation) { |
| setValidationError(validation); |
| return; |
| } |
| setValidationError(null); |
| onFileSelect(file); |
| }, [validateFile, onFileSelect]); |
|
|
| const handleClick = () => { |
| setValidationError(null); |
| inputRef.current?.click(); |
| }; |
|
|
| return ( |
| <div className={styles.wrapper}> |
| <motion.div |
| className={styles.hero} |
| initial={{ opacity: 0, y: 20 }} |
| animate={{ opacity: 1, y: 0 }} |
| transition={{ duration: 0.5 }} |
| > |
| <div className={styles.heroBadge}> |
| <Sparkles size={13} /> |
| <span>Forensic-Grade AI Detection 路 Explainable Results</span> |
| </div> |
| <h1 className={styles.heroTitle}> |
| Is this media <span className={styles.heroAccent}>AI-generated?</span> |
| </h1> |
| <p className={styles.heroSub}> |
| Upload an image or video for multi-model forensic analysis. UAIDE detects deepfakes, |
| GAN artifacts, and AI-generated content using Grad-CAM heatmaps and frequency analysis. |
| </p> |
| </motion.div> |
| |
| <motion.div |
| className={`${styles.dropzone} ${isDragging ? styles.dragging : ''} ${dragType === 'invalid' ? styles.invalid : ''}`} |
| onDragOver={handleDragOver} |
| onDragLeave={handleDragLeave} |
| onDrop={handleDrop} |
| onClick={handleClick} |
| role="button" |
| tabIndex={0} |
| aria-label="Upload media file for analysis" |
| onKeyDown={(event) => event.key === 'Enter' || event.key === ' ' ? handleClick() : null} |
| initial={{ opacity: 0, scale: 0.97 }} |
| animate={{ opacity: 1, scale: 1 }} |
| transition={{ duration: 0.4, delay: 0.15 }} |
| whileHover={{ scale: 1.005 }} |
| > |
| <input |
| ref={inputRef} |
| type="file" |
| accept="image/*,video/*" |
| onChange={handleInputChange} |
| className={styles.hiddenInput} |
| aria-hidden="true" |
| /> |
| |
| <AnimatePresence mode="wait"> |
| {isDragging ? ( |
| <motion.div |
| key="dragging" |
| className={styles.dropContent} |
| initial={{ opacity: 0, scale: 0.9 }} |
| animate={{ opacity: 1, scale: 1 }} |
| exit={{ opacity: 0, scale: 0.9 }} |
| transition={{ duration: 0.15 }} |
| > |
| {dragType === 'invalid' ? ( |
| <> |
| <div className={`${styles.iconRing} ${styles.iconRingError}`}> |
| <AlertCircle size={32} /> |
| </div> |
| <p className={styles.dropLabel} style={{ color: 'var(--ai-generated)' }}>Unsupported format</p> |
| <p className={styles.dropHint}>Drop images or video files only</p> |
| </> |
| ) : ( |
| <> |
| <div className={`${styles.iconRing} ${styles.iconRingActive}`}> |
| {dragType === 'video' ? <FileVideo size={32} /> : <FileImage size={32} />} |
| </div> |
| <p className={styles.dropLabel}>Release to analyse</p> |
| <p className={styles.dropHint}>{dragType === 'video' ? 'Video file detected' : 'Image file detected'}</p> |
| </> |
| )} |
| </motion.div> |
| ) : ( |
| <motion.div key="idle" className={styles.dropContent} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}> |
| <div className={styles.iconGroup}> |
| <div className={styles.iconRing}> |
| <Upload size={28} /> |
| </div> |
| </div> |
| <p className={styles.dropLabel}>Drag & drop your media here</p> |
| <p className={styles.dropHint}>or <span className={styles.browseLink}>browse files</span></p> |
| <div className={styles.supportedTypes}> |
| <div className={styles.typeChip}> |
| <FileImage size={12} /> |
| <span>Images</span> |
| <span className={styles.typeFormats}>JPEG 路 PNG 路 WebP 路 GIF 路 TIFF 路 AVIF</span> |
| </div> |
| <div className={styles.typeDivider} /> |
| <div className={styles.typeChip}> |
| <FileVideo size={12} /> |
| <span>Videos</span> |
| <span className={styles.typeFormats}>MP4 路 WebM 路 MOV 路 AVI</span> |
| </div> |
| </div> |
| <p className={styles.sizeNote}>Max file size: {MAX_SIZE_MB} MB</p> |
| </motion.div> |
| )} |
| </AnimatePresence> |
| </motion.div> |
|
|
| <CameraCapture onCapture={onFileSelect} /> |
|
|
| <AnimatePresence> |
| {(validationError || error) && ( |
| <motion.div className={styles.errorBanner} initial={{ opacity: 0, y: -8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }}> |
| <AlertCircle size={15} /> |
| <span>{validationError || error}</span> |
| </motion.div> |
| )} |
| </AnimatePresence> |
|
|
| <motion.div className={styles.capabilities} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.35 }}> |
| {['GAN Detection', 'Diffusion Model ID', 'Grad-CAM Heatmap', 'FFT Analysis', 'Temporal Analysis', 'Deepfake Detection', 'Live Capture'].map((cap) => ( |
| <span key={cap} className={styles.capPill}>{cap}</span> |
| ))} |
| </motion.div> |
| </div> |
| ); |
| } |
|
|