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' && (
)}
{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}
) : (
| Name |
Size |
Modified |
|
{files.map(f => (
|
{f.name}
|
{formatBytes(f.size)} |
{f.modified} |
|
))}
)}
{preview && (
{preview.name}
{preview.content}
)}
)
}