From ac60597e650f07122afe4624ef9178997e8b31ab Mon Sep 17 00:00:00 2001 From: tocmo0nlord Date: Sun, 26 Apr 2026 00:55:33 +0000 Subject: [PATCH] feat: multi-file drop zone with zip support and upload status queue --- frontend/src/components/DocumentManager.jsx | 121 +++++++++++--------- 1 file changed, 70 insertions(+), 51 deletions(-) diff --git a/frontend/src/components/DocumentManager.jsx b/frontend/src/components/DocumentManager.jsx index f7a865c..2e0c742 100644 --- a/frontend/src/components/DocumentManager.jsx +++ b/frontend/src/components/DocumentManager.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect, useCallback } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import axios from 'axios' import { useDropzone } from 'react-dropzone' -import { Upload, Trash2, Eye, RefreshCw, FileText, X } from 'lucide-react' +import { Upload, Trash2, Eye, RefreshCw, FileText, FileArchive, X, CheckCircle, AlertCircle, Loader } from 'lucide-react' const STAGES = ['input', 'parsed', 'generated', 'curated', 'final'] @@ -11,13 +11,19 @@ function formatBytes(bytes) { return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } +function FileIcon({ name }) { + return name.endsWith('.zip') + ? + : +} + export default function DocumentManager({ connected }) { - const [stage, setStage] = useState('input') - const [files, setFiles] = useState([]) - const [loading, setLoading] = useState(false) - const [uploading, setUploading] = useState(false) - const [preview, setPreview] = useState(null) - const [error, setError] = useState('') + const [stage, setStage] = useState('input') + const [files, setFiles] = useState([]) + const [loading, setLoading] = useState(false) + const [uploadQueue, setUploadQueue] = useState([]) + const [preview, setPreview] = useState(null) + const [error, setError] = useState('') const fetchFiles = useCallback(async () => { if (!connected) return @@ -36,23 +42,36 @@ export default function DocumentManager({ connected }) { const onDrop = useCallback(async accepted => { if (!accepted.length || !connected) return - setUploading(true) + const entries = accepted.map(f => ({ name: f.name, status: 'uploading', error: null })) + setUploadQueue(entries) const formData = new FormData() - formData.append('file', accepted[0]) + accepted.forEach(f => formData.append('files', f)) try { - await axios.post('/api/upload', formData) + const { data } = await axios.post('/api/upload', formData) + setUploadQueue(data.results.map(r => ({ name: r.file, status: 'done', action: r.action, error: null }))) fetchFiles() } catch (err) { - setError(err.response?.data?.detail || err.message) - } finally { - setUploading(false) + const msg = err.response?.data?.detail || err.message + setUploadQueue(entries.map(e => ({ ...e, status: 'error', error: msg }))) + setError(msg) } + setTimeout(() => setUploadQueue([]), 4000) }, [connected, fetchFiles]) const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, disabled: !connected || stage !== 'input', - multiple: false, + multiple: true, + accept: { + 'application/zip': ['.zip'], + 'application/x-zip-compressed': ['.zip'], + 'text/plain': ['.txt'], + 'text/markdown': ['.md'], + 'application/pdf': ['.pdf'], + 'application/json': ['.json'], + 'text/csv': ['.csv'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + }, }) const deleteFile = async name => { @@ -66,26 +85,29 @@ export default function DocumentManager({ connected }) { setPreview({ name, content: data.content }) } + const statusIcon = item => { + if (item.status === 'uploading') return + if (item.status === 'done') return + if (item.status === 'error') return + return null + } + return (
- {/* Stage tabs */}
{STAGES.map(s => ( ))}
- {/* Upload drop zone (input stage only) */} {stage === 'input' && (
- {uploading - ?

Uploading…

- :

- {isDragActive ? 'Drop file here' : 'Drag & drop or click to upload to /input'} -

- } +

+ {isDragActive ? 'Drop files here' : 'Drag & drop files or click — multiple files and .zip archives supported'} +

+

txt · md · pdf · json · csv · docx · zip

+
+ )} + + {uploadQueue.length > 0 && ( +
+
Uploading
+
    + {uploadQueue.map((item, i) => ( +
  • + {statusIcon(item)} + {item.name} + {item.status === 'done' && {item.action}} + {item.error && {item.error}} +
  • + ))} +
)} - {/* File table */}
{files.length} file(s) -
- {error && ( -

{error}

- )} + {error &&

{error}

} {loading ? (

Loading…

@@ -138,23 +168,17 @@ export default function DocumentManager({ connected }) { {files.map(f => ( - + {f.name} {formatBytes(f.size)} {f.modified}
- -
@@ -166,22 +190,17 @@ export default function DocumentManager({ connected }) { )}
- {/* Preview modal */} {preview && (
{preview.name} - +
-
-              {preview.content}
-            
+
{preview.content}
)}
) -} +} \ No newline at end of file