import React, { useState, useCallback, useEffect } from 'react'; import { Box, Card, CardContent, Typography, Button, LinearProgress, Chip, List, ListItem, ListItemIcon, ListItemText, ListItemSecondaryAction, IconButton, Alert, Paper, alpha, useTheme, Divider, } from '@mui/material'; import { CloudUpload as UploadIcon, InsertDriveFile as FileIcon, CheckCircle as CheckIcon, Error as ErrorIcon, Delete as DeleteIcon, Refresh as RefreshIcon, } from '@mui/icons-material'; import { useDropzone, FileRejection, DropzoneOptions } from 'react-dropzone'; import { useNavigate } from 'react-router-dom'; import api from '../../services/api'; import { useNotifications } from '../../contexts/NotificationContext'; import LabelSelector from '../Labels/LabelSelector'; import { type LabelData } from '../Labels/Label'; import LanguageSelector from '../LanguageSelector'; interface UploadedDocument { id: string; original_filename: string; filename: string; file_size: number; mime_type: string; created_at: string; } interface FileItem { file: File; id: string; status: 'pending' | 'uploading' | 'success' | 'error'; progress: number; error: string | null; documentId?: string; } interface UploadZoneProps { onUploadComplete?: (document: UploadedDocument) => void; } type FileStatus = 'pending' | 'uploading' | 'success' | 'error'; const UploadZone: React.FC = ({ onUploadComplete }) => { const theme = useTheme(); const navigate = useNavigate(); const { addBatchNotification } = useNotifications(); const [files, setFiles] = useState([]); const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); const [selectedLabels, setSelectedLabels] = useState([]); const [availableLabels, setAvailableLabels] = useState([]); const [labelsLoading, setLabelsLoading] = useState(false); const [selectedLanguages, setSelectedLanguages] = useState(['eng']); const [primaryLanguage, setPrimaryLanguage] = useState('eng'); useEffect(() => { fetchLabels(); }, []); const fetchLabels = async () => { try { setLabelsLoading(true); const response = await api.get('/labels?include_counts=false'); if (response.status === 200 && Array.isArray(response.data)) { setAvailableLabels(response.data); } else { console.error('Failed to fetch labels:', response); } } catch (error) { console.error('Failed to fetch labels:', error); } finally { setLabelsLoading(false); } }; const handleCreateLabel = async (labelData: Omit) => { try { const response = await api.post('/labels', labelData); const newLabel = response.data; setAvailableLabels(prev => [...prev, newLabel]); return newLabel; } catch (error) { console.error('Failed to create label:', error); throw error; } }; const handleLanguagesChange = (languages: string[], primary?: string) => { setSelectedLanguages(languages); if (primary) { setPrimaryLanguage(primary); } else if (languages.length > 0) { setPrimaryLanguage(languages[0]); } }; const onDrop = useCallback((acceptedFiles: File[], rejectedFiles: FileRejection[]) => { setError(''); // Handle rejected files if (rejectedFiles.length > 0) { const errors = rejectedFiles.map(file => `${file.file.name}: ${file.errors.map(e => e.message).join(', ')}` ); setError(`Some files were rejected: ${errors.join('; ')}`); } // Add accepted files to the list const newFiles: FileItem[] = acceptedFiles.map(file => ({ file, id: Math.random().toString(36).substr(2, 9), status: 'pending' as FileStatus, progress: 0, error: null, })); setFiles(prev => [...prev, ...newFiles]); }, []); const dropzoneOptions: DropzoneOptions = { onDrop, accept: { 'application/pdf': ['.pdf'], 'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff'], 'text/*': ['.txt', '.rtf'], 'application/msword': ['.doc'], 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], }, maxSize: 50 * 1024 * 1024, // 50MB multiple: true, }; const { getRootProps, getInputProps, isDragActive } = useDropzone(dropzoneOptions); const removeFile = (fileId: string): void => { setFiles(prev => prev.filter(f => f.id !== fileId)); }; const uploadFile = async (fileItem: FileItem): Promise => { const formData = new FormData(); formData.append('file', fileItem.file); // Add selected labels to the form data if (selectedLabels.length > 0) { const labelIds = selectedLabels.map(label => label.id); formData.append('label_ids', JSON.stringify(labelIds)); } // Add selected languages to the form data if (selectedLanguages.length > 0) { selectedLanguages.forEach((lang, index) => { formData.append(`ocr_languages[${index}]`, lang); }); } try { setFiles(prev => prev.map(f => f.id === fileItem.id ? { ...f, status: 'uploading' as FileStatus, progress: 0 } : f )); const response = await api.post('/documents', formData, { headers: { 'Content-Type': 'multipart/form-data', }, onUploadProgress: (progressEvent) => { if (progressEvent.total) { const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total); setFiles(prev => prev.map(f => f.id === fileItem.id ? { ...f, progress } : f )); } }, }); setFiles(prev => prev.map(f => f.id === fileItem.id ? { ...f, status: 'success' as FileStatus, progress: 100, documentId: response.data.id } : f )); if (onUploadComplete) { onUploadComplete(response.data); } } catch (error: any) { setFiles(prev => prev.map(f => f.id === fileItem.id ? { ...f, status: 'error' as FileStatus, error: error.response?.data?.message || 'Upload failed', progress: 0, } : f )); } }; const uploadAllFiles = async (): Promise => { setUploading(true); setError(''); const pendingFiles = files.filter(f => f.status === 'pending' || f.status === 'error'); const results: { name: string; success: boolean }[] = []; try { await Promise.allSettled(pendingFiles.map(async (file) => { try { await uploadFile(file); results.push({ name: file.file.name, success: true }); } catch (error) { results.push({ name: file.file.name, success: false }); } })); // Trigger notification based on results const hasFailures = results.some(r => !r.success); const hasSuccesses = results.some(r => r.success); if (!hasFailures) { addBatchNotification('success', 'upload', results); } else if (!hasSuccesses) { addBatchNotification('error', 'upload', results); } else { addBatchNotification('warning', 'upload', results); } } catch (error) { setError('Upload failed. Please try again.'); } finally { setUploading(false); } }; const retryUpload = (fileItem: FileItem): void => { uploadFile(fileItem); }; const clearCompleted = (): void => { setFiles(prev => prev.filter(f => f.status !== 'success')); }; const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }; const getStatusColor = (status: FileStatus): string => { switch (status) { case 'success': return theme.palette.success.main; case 'error': return theme.palette.error.main; case 'uploading': return theme.palette.primary.main; default: return theme.palette.text.secondary; } }; const getStatusIcon = (status: FileStatus): React.ReactElement => { switch (status) { case 'success': return ; case 'error': return ; case 'uploading': return ; default: return ; } }; const handleFileClick = (fileItem: FileItem) => { if (fileItem.status === 'success' && fileItem.documentId) { navigate(`/documents/${fileItem.documentId}`); } }; return ( {/* Upload Drop Zone */} {isDragActive ? 'Drop files here' : 'Drag & drop files here'} or click to browse your computer Maximum file size: 50MB per file {/* Error Alert */} {error && ( {error} )} {/* Language Selection */} 🌐 OCR Language Settings Select languages for optimal OCR text recognition div': { width: '100%' } }}> {/* Label Selection */} 📋 Label Assignment Select labels to automatically assign to all uploaded documents {selectedLabels.length > 0 && ( These labels will be applied to all uploaded documents )} {/* File List */} {files.length > 0 && ( Files ({files.length}) {files.map((fileItem, index) => ( handleFileClick(fileItem)} > {getStatusIcon(fileItem.status)} {fileItem.file.name} } secondary={ {formatFileSize(fileItem.file.size)} {fileItem.status === 'uploading' && ( {fileItem.progress}% )} {fileItem.error && ( {fileItem.error} )} } /> {fileItem.status === 'error' && ( { e.stopPropagation(); retryUpload(fileItem); }} sx={{ color: 'primary.main' }} > )} { e.stopPropagation(); removeFile(fileItem.id); }} disabled={fileItem.status === 'uploading'} > ))} )} ); }; export default UploadZone;