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:
2026-06-03 17:45:14 +08:00
parent 7cd7a10bea
commit 65ba1b214d
4 changed files with 140 additions and 424 deletions

View File

@@ -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>
);
}