import React, { useState, useEffect, useCallback } from 'react' import axios from 'axios' import { useDropzone } from 'react-dropzone' import { Upload, Trash2, Eye, RefreshCw, FileText, FileArchive, X, CheckCircle, AlertCircle, Loader } from 'lucide-react' const STAGES = ['input', 'parsed', 'generated', 'curated', 'final'] function formatBytes(bytes) { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` 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 [uploadQueue, setUploadQueue] = useState([]) const [preview, setPreview] = useState(null) const [error, setError] = useState('') const fetchFiles = useCallback(async () => { if (!connected) return setLoading(true); setError('') try { const { data } = await axios.get(`/api/files/${stage}`) setFiles(data.files) } catch (err) { setError(err.response?.data?.detail || err.message) } finally { setLoading(false) } }, [stage, connected]) useEffect(() => { fetchFiles() }, [fetchFiles]) const onDrop = useCallback(async accepted => { if (!accepted.length || !connected) return const entries = accepted.map(f => ({ name: f.name, status: 'uploading', error: null })) setUploadQueue(entries) const formData = new FormData() accepted.forEach(f => formData.append('files', f)) try { 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) { 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: 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 => { if (!window.confirm(`Delete "${name}"?`)) return await axios.delete(`/api/files/${stage}/${name}`) fetchFiles() } const previewFile = async name => { const { data } = await axios.get(`/api/files/${stage}/${name}/preview`) 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 (
{STAGES.map(s => ( ))}
{stage === '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}}
  • ))}
)}
{files.length} file(s)
{error &&

{error}

} {loading ? (

Loading…

) : files.length === 0 ? (

No files in /{stage}

) : ( {files.map(f => ( ))}
Name Size Modified
{f.name} {formatBytes(f.size)} {f.modified}
)}
{preview && (
{preview.name}
{preview.content}
)}
) }