import { useState, useEffect, useCallback } from 'react'; import { Topbar } from '../../components/layout/Topbar'; import { Upload, Search, Download, Trash2, RefreshCw, AlertTriangle } from 'lucide-react'; import { UploadModal } from './UploadModal'; interface Doc { id: string; name: string; status: 'ok' | 'warn' | 'risk' | 'info'; uploadedAt: string; chunks: number; type: string; sizeBytes: number; summary?: string; version?: string; } const STATUS_FILTERS = ['All', 'Ready', 'Processing', 'Failed', 'Pending']; const STATUS_LABEL: Record = { ok: 'Ready', warn: 'Processing', risk: 'Failed', info: 'Pending' }; const STATUS_MAP: Record = { All: 'All', Ready: 'ok', Processing: 'warn', Failed: 'risk', Pending: 'info' }; function backendStatus(s: string): Doc['status'] { if (s === 'indexed') return 'ok'; if (s === 'failed') return 'risk'; if (s === 'parsed') return 'warn'; return 'info'; } function formatSize(bytes: number): string { if (!bytes) return '—'; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; } // ── Confirm dialog ───────────────────────────────────────────────────────── function ConfirmDialog({ message, onConfirm, onCancel }: { message: string; onConfirm: () => void; onCancel: () => void; }) { return (
e.stopPropagation()} >
Confirm deletion

{message}

); } export function DocsPage() { const [search, setSearch] = useState(''); const [statusF, setStatusF] = useState('All'); const [typeF, setTypeF] = useState('All types'); const [selected, setSelected] = useState>(new Set()); const [docs, setDocs] = useState([]); const [loading, setLoading] = useState(true); const [showUpload, setShowUpload] = useState(false); const [refreshKey, setRefreshKey] = useState(0); const [retrying, setRetrying] = useState>(new Set()); const [deleting, setDeleting] = useState>(new Set()); const [confirmDelete, setConfirmDelete] = useState<{ ids: string[]; names: string[] } | null>(null); // Dynamic type options derived from actual docs const typeOpts = ['All types', ...Array.from(new Set(docs.map(d => d.type).filter(t => t && t !== '—')))]; const fetchDocs = useCallback(() => { setLoading(true); fetch('/api/v1/documents/management-list') .then(r => r.json()) .then(d => { if (!Array.isArray(d?.documents)) { setLoading(false); return; } setDocs(d.documents.map((item: Record) => ({ id: item.doc_id as string, name: item.doc_name as string, status: backendStatus(item.status as string), uploadedAt: ((item.updated_at as string) ?? '').slice(0, 10), chunks: (item.chunk_count as number) ?? 0, type: (item.regulation_type as string) || '—', sizeBytes: (item.size_bytes as number) ?? 0, summary: item.summary as string | undefined, version: item.version as string | undefined, }))); setLoading(false); }) .catch(() => setLoading(false)); }, []); useEffect(() => { fetchDocs(); }, [fetchDocs, refreshKey]); // ── Filtering ──────────────────────────────────────────────────────────── const filtered = docs.filter(d => { const matchSearch = !search || d.name.toLowerCase().includes(search.toLowerCase()); const matchStatus = statusF === 'All' || d.status === STATUS_MAP[statusF]; const matchType = typeF === 'All types' || d.type === typeF; return matchSearch && matchStatus && matchType; }); // ── Selection helpers ──────────────────────────────────────────────────── function toggleAll() { if (selected.size === filtered.length) setSelected(new Set()); else setSelected(new Set(filtered.map(d => d.id))); } function toggleOne(id: string) { const s = new Set(selected); s.has(id) ? s.delete(id) : s.add(id); setSelected(s); } // ── Download ───────────────────────────────────────────────────────────── function downloadDoc(id: string, name: string) { const a = document.createElement('a'); a.href = `/api/v1/documents/download/${id}`; a.download = name; a.click(); } // ── Retry (re-process failed doc) ──────────────────────────────────────── async function retryDoc(id: string) { setRetrying(r => new Set([...r, id])); try { await fetch(`/api/v1/documents/${id}/retry`, { method: 'POST' }); setTimeout(() => { setRetrying(r => { const s = new Set(r); s.delete(id); return s; }); setRefreshKey(k => k + 1); }, 1500); } catch { setRetrying(r => { const s = new Set(r); s.delete(id); return s; }); } } // ── Delete (single or batch) ───────────────────────────────────────────── function askDelete(ids: string[]) { const names = ids.map(id => docs.find(d => d.id === id)?.name ?? id); setConfirmDelete({ ids, names }); } async function confirmDeleteDocs() { if (!confirmDelete) return; const { ids } = confirmDelete; setConfirmDelete(null); setDeleting(new Set(ids)); await Promise.allSettled( ids.map(id => fetch(`/api/v1/documents/${id}`, { method: 'DELETE' })) ); setDeleting(new Set()); setSelected(s => { const n = new Set(s); ids.forEach(id => n.delete(id)); return n; }); setRefreshKey(k => k + 1); } return (
setSearch(e.target.value)} />
} />
{STATUS_FILTERS.map(f => ( ))}
{/* Batch action bar */} {selected.size > 0 && (
{selected.size} document{selected.size > 1 ? 's' : ''} selected
)} {/* Table */}
0} onChange={toggleAll} /> Document name Status Uploaded Chunks Size Type Actions
{loading ? (
Loading documents…
) : filtered.length === 0 ? (
{docs.length === 0 ? 'No documents yet. Upload a document to get started.' : 'No documents match the current filters.'}
) : ( filtered.map(d => { const isDeleting = deleting.has(d.id); const isRetrying = retrying.has(d.id); return (
toggleOne(d.id)} disabled={isDeleting} /> {d.name} {d.version && v{d.version}} {STATUS_LABEL[d.status]} {d.uploadedAt} {d.chunks || '—'} {formatSize(d.sizeBytes)} {d.type} {/* Download */} {/* Retry for failed */} {d.status === 'risk' && ( )} {/* Delete */}
); }) )}
{/* Footer count */} {!loading && (
{filtered.length} of {docs.length} document{docs.length !== 1 ? 's' : ''} {selected.size > 0 && ` · ${selected.size} selected`}
)}
{/* Confirm delete dialog */} {confirmDelete && ( setConfirmDelete(null)} /> )} {showUpload && ( setShowUpload(false)} onComplete={() => setRefreshKey(k => k + 1)} /> )}
); }