/** * FileUpload Component * * Drag-and-drop file upload for CSV/XLSX files. * Validates file type and size (max 10MB). */ import { useCallback, useState, useEffect } from 'react'; import type { FileMetadata } from '../types'; import { LoadingSpinner } from './LoadingSpinner'; interface FileUploadProps { onFileUploaded: (metadata: FileMetadata) => void; sessionId: string; isUploading: boolean; onUploadStart: () => void; onUploadEnd: () => void; onError: (error: string) => void; initialFile?: FileMetadata | null; } const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const ALLOWED_EXTENSIONS = ['.csv', '.xlsx', '.xls']; export function FileUpload({ onFileUploaded, sessionId, isUploading, onUploadStart, onUploadEnd, onError, initialFile = null, }: FileUploadProps) { const [isDragging, setIsDragging] = useState(false); const [uploadedFile, setUploadedFile] = useState(initialFile); // Sync uploadedFile state when initialFile prop changes (e.g., after async restoration) useEffect(() => { setUploadedFile(initialFile); }, [initialFile]); const validateFile = (file: File): string | null => { // Check file extension const extension = '.' + file.name.split('.').pop()?.toLowerCase(); if (!ALLOWED_EXTENSIONS.includes(extension)) { return 'Invalid file type. Please upload a CSV or XLSX file.'; } // Check file size if (file.size > MAX_FILE_SIZE) { return 'File too large. Maximum size is 10MB.'; } return null; }; const uploadFile = async (file: File) => { const validationError = validateFile(file); if (validationError) { onError(validationError); return; } onUploadStart(); try { const formData = new FormData(); formData.append('file', file); formData.append('session_id', sessionId); const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const response = await fetch(`${API_URL}/api/upload`, { method: 'POST', body: formData, }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || 'Upload failed'); } const data = await response.json(); const metadata: FileMetadata = { fileId: data.file_id, fileUrl: data.file_url, filename: data.filename, columns: data.columns, rowCount: data.row_count, }; setUploadedFile(metadata); onFileUploaded(metadata); } catch (error) { onError(error instanceof Error ? error.message : 'Upload failed'); } finally { onUploadEnd(); } }; const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) { uploadFile(file); } }, [sessionId] ); const handleFileSelect = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { uploadFile(file); } }, [sessionId] ); const handleRemoveFile = () => { setUploadedFile(null); }; // Show uploaded file info if (uploadedFile) { return (

Uploaded Data

{uploadedFile.filename}

{uploadedFile.rowCount.toLocaleString()} rows •{' '} {uploadedFile.columns.length} columns

{/* Column preview */}

Columns

{uploadedFile.columns.slice(0, 8).map((col) => ( {col} ))} {uploadedFile.columns.length > 8 && ( +{uploadedFile.columns.length - 8} more )}
); } // Show upload area return (

Upload Data

{isUploading ? ( ) : ( <> )}
); } export default FileUpload;