import React, { useState, useCallback } from 'react'; import { Upload, FileText, Image, FileCode, Loader2, CheckCircle, XCircle, AlertCircle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { Badge } from '@/components/ui/badge'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; interface UploadedFile { id: string; file: File; status: 'pending' | 'uploading' | 'processing' | 'completed' | 'failed'; progress: number; documentId?: number; error?: string; } interface Document { id: number; title: string; fileName: string; fileSize: number; mimeType: string; processingStatus: string; createdAt: string; } export default function DocumentUpload() { const [files, setFiles] = useState([]); const [isDragging, setIsDragging] = useState(false); const [uploadTitle, setUploadTitle] = useState(''); const [uploadSource, setUploadSource] = useState(''); const [documents, setDocuments] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const acceptedTypes = [ 'application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'text/plain', 'text/markdown', 'application/json', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ]; const getFileIcon = (mimeType: string) => { if (mimeType.startsWith('image/')) return ; if (mimeType === 'application/pdf') return ; if (mimeType.includes('text') || mimeType.includes('json')) return ; return ; }; const getStatusIcon = (status: string) => { switch (status) { case 'completed': return ; case 'failed': return ; case 'processing': case 'uploading': return ; default: return ; } }; 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 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 droppedFiles = Array.from(e.dataTransfer.files); handleFiles(droppedFiles); }, []); const handleFileSelect = (e: React.ChangeEvent) => { if (e.target.files) { const selectedFiles = Array.from(e.target.files); handleFiles(selectedFiles); } }; const handleFiles = (newFiles: File[]) => { const validFiles = newFiles.filter(file => { if (!acceptedTypes.includes(file.type)) { alert(`File type ${file.type} is not supported`); return false; } if (file.size > 50 * 1024 * 1024) { // 50MB limit alert(`File ${file.name} is too large. Maximum size is 50MB`); return false; } return true; }); const uploadFiles: UploadedFile[] = validFiles.map(file => ({ id: `${Date.now()}-${Math.random()}`, file, status: 'pending', progress: 0 })); setFiles(prev => [...prev, ...uploadFiles]); }; const uploadFiles = async () => { const pendingFiles = files.filter(f => f.status === 'pending'); if (pendingFiles.length === 0) return; for (const uploadFile of pendingFiles) { try { // Update status to uploading setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'uploading', progress: 0 } : f )); const formData = new FormData(); formData.append('files', uploadFile.file); if (uploadTitle) formData.append('title', uploadTitle); if (uploadSource) formData.append('source', uploadSource); const response = await fetch('/api/documents/upload', { method: 'POST', body: formData, }); const result = await response.json(); if (!response.ok) { // Handle disabled uploads gracefully if (response.status === 503) { throw new Error(result.message || 'File uploads are disabled in this environment'); } throw new Error(result.message || `Upload failed: ${response.statusText}`); } if (result.success && result.documents?.length > 0) { const document = result.documents[0]; // Update file status to processing setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'processing', progress: 100, documentId: document.id } : f )); // Add to documents list setDocuments(prev => [document, ...prev]); // If file requires processing, start processing if (document.processingStatus === 'pending') { await processDocument(document.id, uploadFile.id); } else { // Mark as completed if no processing needed setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'completed' } : f )); } } else { throw new Error(result.message || 'Upload failed'); } } catch (error) { console.error('Upload error:', error); setFiles(prev => prev.map(f => f.id === uploadFile.id ? { ...f, status: 'failed', error: error instanceof Error ? error.message : 'Upload failed' } : f )); } } }; const processDocument = async (documentId: number, fileId: string) => { try { const response = await fetch(`/api/documents/process/${documentId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ operations: ['extract_text', 'generate_embedding'] }) }); const result = await response.json(); if (result.success) { // Update file status to completed setFiles(prev => prev.map(f => f.id === fileId ? { ...f, status: 'completed' } : f )); // Update document in list setDocuments(prev => prev.map(doc => doc.id === documentId ? { ...doc, processingStatus: 'completed' } : doc )); } else { throw new Error(result.message || 'Processing failed'); } } catch (error) { console.error('Processing error:', error); setFiles(prev => prev.map(f => f.id === fileId ? { ...f, status: 'failed', error: error instanceof Error ? error.message : 'Processing failed' } : f )); } }; const batchProcessDocuments = async () => { const completedDocuments = documents.filter(doc => doc.processingStatus === 'completed'); if (completedDocuments.length === 0) { alert('No processed documents available for batch operations'); return; } setIsProcessing(true); try { // Build vector index const response = await fetch('/api/documents/index/build', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentIds: completedDocuments.map(doc => doc.id), indexName: 'main_index' }) }); const result = await response.json(); if (result.success) { alert(`Vector index built successfully with ${result.documentCount} documents`); } else { throw new Error(result.message || 'Index building failed'); } } catch (error) { console.error('Batch processing error:', error); alert(`Batch processing failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsProcessing(false); } }; const removeFile = (fileId: string) => { setFiles(prev => prev.filter(f => f.id !== fileId)); }; const clearCompleted = () => { setFiles(prev => prev.filter(f => f.status !== 'completed' && f.status !== 'failed')); }; return (
{/* Upload Area */} Document Upload {/* Optional metadata */}
setUploadTitle(e.target.value)} />
setUploadSource(e.target.value)} />
{/* Drag and drop area */}

Drop files here or click to browse

Supports PDF, images, text files, Word documents (max 50MB each)

{/* File list */} {files.length > 0 && (

Files ({files.length})

{files.map((file) => (
{getFileIcon(file.file.type)}

{file.file.name}

{formatFileSize(file.file.size)}

{file.status} {getStatusIcon(file.status)} {(file.status === 'uploading' || file.status === 'processing') && (
)} {(file.status === 'pending' || file.status === 'failed') && ( )}
))}
)} {/* Error alerts */} {files.some(f => f.error) && ( Some files failed to upload. Check individual file status above. )}
{/* Document Management */} {documents.length > 0 && ( Uploaded Documents ({documents.length})
{documents.map((doc) => (
{getFileIcon(doc.mimeType)}

{doc.title}

{doc.fileName} • {formatFileSize(doc.fileSize)}

{doc.processingStatus} {getStatusIcon(doc.processingStatus)}
))}
)}
); }