/** * FileUploader component - Drag-and-drop file upload with progress */ import React, { useCallback, useState, useRef } from 'react'; import { Box, Typography, LinearProgress, Button, Alert, Paper, } from '@mui/material'; import { CloudUpload as UploadIcon, Description as FileIcon, Close as CloseIcon, } from '@mui/icons-material'; interface FileUploaderProps { onUpload: (file: File, taskType: string) => Promise; onProgress?: (progress: number) => void; disabled?: boolean; } interface UploadState { file: File | null; progress: number; status: 'idle' | 'uploading' | 'success' | 'error'; error: string | null; } const ACCEPTED_TYPES = ['.csv', '.json', '.xlsx', '.parquet']; const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB const FileUploader: React.FC = ({ onUpload, onProgress, disabled = false, }) => { const [uploadState, setUploadState] = useState({ file: null, progress: 0, status: 'idle', error: null, }); const [isDragging, setIsDragging] = useState(false); const [taskType, setTaskType] = useState<'easy' | 'medium' | 'hard'>('easy'); const fileInputRef = useRef(null); const validateFile = (file: File): string | null => { const ext = file.name.slice(file.name.lastIndexOf('.')).toLowerCase(); if (!ACCEPTED_TYPES.includes(ext)) { return `Invalid file type. Accepted: ${ACCEPTED_TYPES.join(', ')}`; } if (file.size > MAX_FILE_SIZE) { return 'File size exceeds 100MB limit'; } return null; }; const handleFile = useCallback((file: File) => { const error = validateFile(file); if (error) { setUploadState({ file: null, progress: 0, status: 'error', error, }); return; } setUploadState({ file, progress: 0, status: 'idle', error: null, }); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); if (disabled) return; const file = e.dataTransfer.files[0]; if (file) { handleFile(file); } }, [disabled, handleFile] ); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleInputChange = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { handleFile(file); } }, [handleFile] ); const handleUpload = async () => { if (!uploadState.file) return; setUploadState((prev) => ({ ...prev, status: 'uploading', progress: 0, error: null })); try { // Simulate progress for demo (in real app, use XMLHttpRequest upload events) const progressInterval = setInterval(() => { setUploadState((prev) => { if (prev.progress < 90) { const newProgress = prev.progress + Math.random() * 15; onProgress?.(Math.min(newProgress, 90)); return { ...prev, progress: Math.min(newProgress, 90) }; } return prev; }); }, 200); await onUpload(uploadState.file, taskType); clearInterval(progressInterval); setUploadState((prev) => ({ ...prev, progress: 100, status: 'success' })); onProgress?.(100); } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Upload failed'; setUploadState((prev) => ({ ...prev, status: 'error', error: errorMessage, progress: 0, })); } }; const handleCancel = () => { setUploadState({ file: null, progress: 0, status: 'idle', error: null, }); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const formatFileSize = (bytes: number): string => { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; }; return ( {/* Drop Zone */} {isDragging ? 'Drop file here' : 'Drag & drop your file here'} or click to browse Supports: CSV, JSON, Excel, Parquet (max 100MB) {/* Selected File */} {uploadState.file && ( {uploadState.file.name} {formatFileSize(uploadState.file.size)} {uploadState.status === 'idle' && ( )} )} {/* Error Message */} {uploadState.error && ( {uploadState.error} )} {/* Progress Bar */} {uploadState.status === 'uploading' && ( Uploading... {Math.round(uploadState.progress)}% )} {/* Success Message */} {uploadState.status === 'success' && ( File uploaded successfully! )} {/* Task Selection & Upload Button */} {uploadState.file && uploadState.status !== 'uploading' && ( Task Difficulty: {(['easy', 'medium', 'hard'] as const).map((type) => ( ))} )} {/* Task Description */} {uploadState.file && uploadState.status !== 'uploading' && ( {taskType.charAt(0).toUpperCase() + taskType.slice(1)} task:{' '} {taskType === 'easy' && 'Handle missing values, duplicates, basic formatting'} {taskType === 'medium' && 'Easy + Outliers, category standardization'} {taskType === 'hard' && 'Medium + Complex patterns, custom rules, edge cases'} )} ); }; export default FileUploader;