refactor(admin): split document-crawler page.tsx into colocated components
Break 839-line page.tsx into _types.ts, _components/SourcesTab.tsx, JobsTab.tsx, DocumentsTab.tsx, ReportTab.tsx, and ComplianceRing.tsx. page.tsx is now 56 LOC (wiring only). No behavior changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { CrawlDocument, api, CLASSIFICATION_LABELS, ALL_CLASSIFICATIONS } from '../_types'
|
||||
|
||||
export function DocumentsTab() {
|
||||
const [docs, setDocs] = useState<CrawlDocument[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [filterClass, setFilterClass] = useState('')
|
||||
const [archiving, setArchiving] = useState<Record<string, boolean>>({})
|
||||
|
||||
const loadDocs = useCallback(async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const params = filterClass ? `?classification=${filterClass}` : ''
|
||||
const data = await api(`documents${params}`)
|
||||
setDocs(data?.documents || [])
|
||||
setTotal(data?.total || 0)
|
||||
} catch { /* ignore */ }
|
||||
setLoading(false)
|
||||
}, [filterClass])
|
||||
|
||||
useEffect(() => { loadDocs() }, [loadDocs])
|
||||
|
||||
const handleReclassify = async (docId: string, newClass: string) => {
|
||||
await api(`documents/${docId}/classify`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ classification: newClass }),
|
||||
})
|
||||
loadDocs()
|
||||
}
|
||||
|
||||
const handleArchive = async (docId: string) => {
|
||||
setArchiving(prev => ({ ...prev, [docId]: true }))
|
||||
try {
|
||||
await api(`documents/${docId}/archive`, { method: 'POST' })
|
||||
loadDocs()
|
||||
} catch { /* ignore */ }
|
||||
setArchiving(prev => ({ ...prev, [docId]: false }))
|
||||
}
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">{total} Dokumente</h2>
|
||||
<select
|
||||
value={filterClass}
|
||||
onChange={e => setFilterClass(e.target.value)}
|
||||
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Alle Kategorien</option>
|
||||
{ALL_CLASSIFICATIONS.map(c => (
|
||||
<option key={c} value={c}>{CLASSIFICATION_LABELS[c]?.label || c}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-gray-500">Laden...</div>
|
||||
) : docs.length === 0 ? (
|
||||
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
||||
Keine Dokumente gefunden. Starten Sie zuerst einen Crawl-Job.
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-gray-50 text-gray-600">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-3 font-medium">Datei</th>
|
||||
<th className="text-left px-4 py-3 font-medium">Kategorie</th>
|
||||
<th className="text-center px-4 py-3 font-medium">Konfidenz</th>
|
||||
<th className="text-right px-4 py-3 font-medium">Groesse</th>
|
||||
<th className="text-center px-4 py-3 font-medium">Archiv</th>
|
||||
<th className="text-right px-4 py-3 font-medium">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
{docs.map(doc => {
|
||||
const cls = CLASSIFICATION_LABELS[doc.classification || ''] || CLASSIFICATION_LABELS['Sonstiges']
|
||||
return (
|
||||
<tr key={doc.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium text-gray-900 truncate max-w-xs">{doc.file_name}</div>
|
||||
<div className="text-xs text-gray-400">{doc.source_name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<select
|
||||
value={doc.classification || 'Sonstiges'}
|
||||
onChange={e => handleReclassify(doc.id, e.target.value)}
|
||||
className={`px-2 py-1 text-xs font-medium rounded border-0 ${cls.color}`}
|
||||
>
|
||||
{ALL_CLASSIFICATIONS.map(c => (
|
||||
<option key={c} value={c}>{CLASSIFICATION_LABELS[c].label}</option>
|
||||
))}
|
||||
</select>
|
||||
{doc.classification_corrected && (
|
||||
<span className="ml-1 text-xs text-orange-500" title="Manuell korrigiert">*</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{doc.classification_confidence != null && (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-purple-500 rounded-full"
|
||||
style={{ width: `${doc.classification_confidence * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
{(doc.classification_confidence * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right text-gray-500">{formatSize(doc.file_size_bytes)}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{doc.archived ? (
|
||||
<span className="text-green-600 text-xs font-medium" title={doc.ipfs_cid || ''}>IPFS</span>
|
||||
) : (
|
||||
<span className="text-gray-400 text-xs">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
{!doc.archived && (
|
||||
<button
|
||||
onClick={() => handleArchive(doc.id)}
|
||||
disabled={archiving[doc.id]}
|
||||
className="text-xs text-purple-600 hover:text-purple-800 disabled:opacity-50"
|
||||
>
|
||||
{archiving[doc.id] ? 'Archiviert...' : 'Archivieren'}
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user