feat: implement Document Management page with filterable table and batch actions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,141 @@
|
||||
export function DocsPage() {
|
||||
return <div className="page-content"><p>Documents</p></div>;
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Topbar } from '../../components/layout/Topbar';
|
||||
import { Upload, Search } from 'lucide-react';
|
||||
|
||||
interface Doc {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'ok' | 'warn' | 'risk' | 'info';
|
||||
uploadedAt: string;
|
||||
chunks: number;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const STATUS_FILTERS = ['All', 'Ready', 'Embedding', 'Failed', 'Pending'];
|
||||
const TYPE_OPTS = ['All types', 'EU Regulation', 'ISO Standard', 'National Draft', 'Internal Policy'];
|
||||
|
||||
const MOCK_DOCS: Doc[] = [
|
||||
{ id: '1', name: 'EU AI Act — Full text (EN)', status: 'ok', uploadedAt: '2025-11-10', chunks: 842, type: 'EU Regulation' },
|
||||
{ id: '2', name: 'MIIT Draft 2025-08 (ZH)', status: 'ok', uploadedAt: '2025-11-01', chunks: 320, type: 'National Draft' },
|
||||
{ id: '3', name: 'ISO/SAE 21434:2021', status: 'ok', uploadedAt: '2025-10-15', chunks: 614, type: 'ISO Standard' },
|
||||
{ id: '4', name: 'Vehicle AI Safety Manual v3.2', status: 'ok', uploadedAt: '2025-10-08', chunks: 198, type: 'Internal Policy' },
|
||||
{ id: '5', name: 'ADAS System Requirements', status: 'warn', uploadedAt: '2025-09-22', chunks: 0, type: 'Internal Policy' },
|
||||
{ id: '6', name: 'UNECE R155 Corrigendum', status: 'info', uploadedAt: '2025-09-12', chunks: 87, type: 'EU Regulation' },
|
||||
{ id: '7', name: 'GB/T 42118-2022', status: 'risk', uploadedAt: '2025-08-30', chunks: 0, type: 'National Draft' },
|
||||
];
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = { ok: 'Ready', warn: 'Embedding', risk: 'Failed', info: 'Pending' };
|
||||
const STATUS_MAP: Record<string, string> = { All: 'All', Ready: 'ok', Embedding: 'warn', Failed: 'risk', Pending: 'info' };
|
||||
|
||||
export function DocsPage() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [statusF, setStatusF] = useState('All');
|
||||
const [typeF, setTypeF] = useState('All types');
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [docs, setDocs] = useState<Doc[]>(MOCK_DOCS);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/v1/documents')
|
||||
.then(r => r.json())
|
||||
.then(d => { if (Array.isArray(d?.documents)) setDocs(d.documents); })
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="docs-page">
|
||||
<Topbar
|
||||
title="Document Management"
|
||||
actions={
|
||||
<>
|
||||
<div className="search-box">
|
||||
<Search size={13} />
|
||||
<input
|
||||
placeholder="Search documents..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn sm primary"><Upload size={13} />Upload document</button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div className="page-content">
|
||||
<div className="docs-controls">
|
||||
<div className="chip-group">
|
||||
{STATUS_FILTERS.map(f => (
|
||||
<button
|
||||
key={f}
|
||||
className={`chip${statusF === f ? ' active' : ''}`}
|
||||
onClick={() => setStatusF(f)}
|
||||
>{f}</button>
|
||||
))}
|
||||
</div>
|
||||
<select className="select-input" value={typeF} onChange={e => setTypeF(e.target.value)}>
|
||||
{TYPE_OPTS.map(o => <option key={o}>{o}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{selected.size > 0 && (
|
||||
<div className="batch-bar">
|
||||
<span>{selected.size} document{selected.size > 1 ? 's' : ''} selected</span>
|
||||
<button className="btn sm">Analyze selected</button>
|
||||
<button className="btn sm risk-btn">Delete selected</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="docs-table">
|
||||
<div className="table-header">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.size === filtered.length && filtered.length > 0}
|
||||
onChange={toggleAll}
|
||||
/>
|
||||
<span>Document name</span>
|
||||
<span>Status</span>
|
||||
<span>Uploaded</span>
|
||||
<span>Chunks</span>
|
||||
<span>Type</span>
|
||||
<span>Actions</span>
|
||||
</div>
|
||||
{filtered.map(d => (
|
||||
<div key={d.id} className={`table-row${selected.has(d.id) ? ' row-selected' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(d.id)}
|
||||
onChange={() => toggleOne(d.id)}
|
||||
/>
|
||||
<span className="doc-name-cell">{d.name}</span>
|
||||
<span><span className={`status ${d.status}`}>{STATUS_LABEL[d.status]}</span></span>
|
||||
<span className="cell-mono">{d.uploadedAt}</span>
|
||||
<span className="cell-mono">{d.chunks || '—'}</span>
|
||||
<span className="cell-muted">{d.type}</span>
|
||||
<span className="row-actions">
|
||||
<button className="text-link">Inspect</button>
|
||||
<button className="text-link">Analyze</button>
|
||||
{d.status === 'risk' && <button className="text-link danger-link">Resolve</button>}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user