/** * StudioDropZone - Smart file drop zone for Studio * * Handles both model files (.sim, .prt, .fem) and context files (.pdf, .md, .txt) */ import React, { useState, useCallback, useRef } from 'react'; import { Upload, X, Loader2, AlertCircle, CheckCircle, File } from 'lucide-react'; import { intakeApi } from '../../api/intake'; interface StudioDropZoneProps { draftId: string; type: 'model' | 'context'; files: string[]; onUploadComplete: () => void; } interface FileStatus { file: File; status: 'pending' | 'uploading' | 'success' | 'error'; message?: string; } const MODEL_EXTENSIONS = ['.prt', '.sim', '.fem', '.afem']; const CONTEXT_EXTENSIONS = ['.md', '.txt', '.pdf', '.json', '.csv', '.docx']; export const StudioDropZone: React.FC = ({ draftId, type, files, onUploadComplete, }) => { const [isDragging, setIsDragging] = useState(false); const [pendingFiles, setPendingFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); const validExtensions = type === 'model' ? MODEL_EXTENSIONS : CONTEXT_EXTENSIONS; const validateFile = (file: File): { valid: boolean; reason?: string } => { const ext = '.' + file.name.split('.').pop()?.toLowerCase(); if (!validExtensions.includes(ext)) { return { valid: false, reason: `Invalid type: ${ext}` }; } if (file.size > 500 * 1024 * 1024) { return { valid: false, reason: 'File too large (max 500MB)' }; } return { valid: true }; }; const handleDragEnter = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const addFiles = useCallback((newFiles: File[]) => { const validFiles: FileStatus[] = []; for (const file of newFiles) { if (pendingFiles.some(f => f.file.name === file.name)) { continue; } const validation = validateFile(file); validFiles.push({ file, status: validation.valid ? 'pending' : 'error', message: validation.reason, }); } setPendingFiles(prev => [...prev, ...validFiles]); }, [pendingFiles, validExtensions]); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); addFiles(Array.from(e.dataTransfer.files)); }, [addFiles]); const handleFileSelect = useCallback((e: React.ChangeEvent) => { addFiles(Array.from(e.target.files || [])); e.target.value = ''; }, [addFiles]); const removeFile = (index: number) => { setPendingFiles(prev => prev.filter((_, i) => i !== index)); }; const uploadFiles = async () => { const toUpload = pendingFiles.filter(f => f.status === 'pending'); if (toUpload.length === 0) return; setIsUploading(true); try { const uploadFn = type === 'model' ? intakeApi.uploadFiles : intakeApi.uploadContextFiles; const response = await uploadFn(draftId, toUpload.map(f => f.file)); const results = new Map( response.uploaded_files.map(f => [f.name, f.status === 'uploaded']) ); setPendingFiles(prev => prev.map(f => { if (f.status !== 'pending') return f; const success = results.get(f.file.name); return { ...f, status: success ? 'success' : 'error', message: success ? undefined : 'Upload failed', }; })); setTimeout(() => { setPendingFiles(prev => prev.filter(f => f.status !== 'success')); onUploadComplete(); }, 1000); } catch (err) { setPendingFiles(prev => prev.map(f => f.status === 'pending' ? { ...f, status: 'error', message: 'Upload failed' } : f )); } finally { setIsUploading(false); } }; // Auto-upload when files are added React.useEffect(() => { const pending = pendingFiles.filter(f => f.status === 'pending'); if (pending.length > 0 && !isUploading) { uploadFiles(); } }, [pendingFiles, isUploading]); return (
{/* Drop Zone */}
fileInputRef.current?.click()} className={` relative border-2 border-dashed rounded-lg p-4 cursor-pointer transition-all duration-200 text-center ${isDragging ? 'border-primary-400 bg-primary-400/5' : 'border-dark-600 hover:border-primary-400/50 hover:bg-white/5' } `} >

{isDragging ? 'Drop files here' : 'Drop or click to add'}

{validExtensions.join(', ')}

{/* Existing Files */} {files.length > 0 && (
{files.map((name, i) => (
{name}
))}
)} {/* Pending Files */} {pendingFiles.length > 0 && (
{pendingFiles.map((f, i) => (
{f.status === 'pending' && } {f.status === 'uploading' && } {f.status === 'success' && } {f.status === 'error' && } {f.file.name} {f.message && ( ({f.message}) )} {f.status === 'pending' && ( )}
))}
)}
); }; export default StudioDropZone;