2026-06-03 17:45:14 +08:00
|
|
|
import { useState, useEffect } from 'react';
|
|
|
|
|
import { Topbar } from '../../components/layout/Topbar';
|
|
|
|
|
import { Upload, Search } from 'lucide-react';
|
2026-06-04 15:43:44 +08:00
|
|
|
import { UploadModal } from './UploadModal';
|
2026-06-03 17:45:14 +08:00
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
];
|
|
|
|
|
|
2026-06-04 15:43:44 +08:00
|
|
|
const STATUS_LABEL: Record<string, string> = { ok: 'Ready', warn: 'Processing', risk: 'Failed', info: 'Pending' };
|
2026-06-03 17:45:14 +08:00
|
|
|
const STATUS_MAP: Record<string, string> = { All: 'All', Ready: 'ok', Embedding: 'warn', Failed: 'risk', Pending: 'info' };
|
|
|
|
|
|
2026-06-04 15:43:44 +08:00
|
|
|
// Map backend DocumentStatus enum values to frontend display status
|
|
|
|
|
function backendStatus(s: string): Doc['status'] {
|
|
|
|
|
if (s === 'indexed') return 'ok';
|
|
|
|
|
if (s === 'failed') return 'risk';
|
|
|
|
|
if (s === 'parsed') return 'warn'; // chunked, awaiting embedding
|
|
|
|
|
return 'info'; // pending / stored
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 17:16:00 +08:00
|
|
|
export function DocsPage() {
|
2026-06-03 17:45:14 +08:00
|
|
|
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);
|
2026-06-04 15:43:44 +08:00
|
|
|
const [showUpload, setShowUpload] = useState(false);
|
2026-06-03 17:45:14 +08:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2026-06-04 15:43:44 +08:00
|
|
|
fetch('/api/v1/documents/management-list')
|
2026-06-03 17:45:14 +08:00
|
|
|
.then(r => r.json())
|
2026-06-04 15:43:44 +08:00
|
|
|
.then(d => {
|
|
|
|
|
if (!Array.isArray(d?.documents)) return;
|
|
|
|
|
setDocs(d.documents.map((item: Record<string, unknown>) => ({
|
|
|
|
|
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) || '—',
|
|
|
|
|
})));
|
|
|
|
|
})
|
2026-06-03 17:45:14 +08:00
|
|
|
.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>
|
2026-06-04 15:43:44 +08:00
|
|
|
<button className="btn sm primary" onClick={() => setShowUpload(true)}>
|
|
|
|
|
<Upload size={13} />Upload document
|
|
|
|
|
</button>
|
2026-06-03 17:45:14 +08:00
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<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>
|
2026-06-04 15:43:44 +08:00
|
|
|
|
|
|
|
|
{showUpload && <UploadModal onClose={() => setShowUpload(false)} />}
|
2026-06-03 17:45:14 +08:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-05-14 15:07:34 +08:00
|
|
|
}
|