2e29b611c9
Phase 1A — Haftungs-kritische Fixes: - SIL/PL-Badges als "Vorab-Einschaetzung" mit Tooltip gekennzeichnet - Coverage-Disclaimer in CE-Akte, Projekt-Uebersicht und Print-Export - Norm-Referenzen: 42 Kapitelverweise durch Themen-Deskriptoren ersetzt Phase 1B — Massnahmen-Verkabelung: - 16 neue Massnahmen (M201-M216) fuer bisher unabgedeckte Kategorien (communication_failure, hmi_error, firmware_corruption, maintenance, sensor_fault, mode_confusion) - Kategorie-Fallback im Initialize-Endpoint: ordnet Massnahmen aus der Bibliothek automatisch per HazardCategory zu (max 8 pro Kategorie) - Total: 225 → 241 Massnahmen, 0 Kategorien ohne Massnahmen Phase 1C — Explainability Engine: - MatchReason Struct in PatternMatch (type, tag, met) - Pattern Engine schreibt fuer jeden Match strukturierte Begruendungen - Frontend zeigt "Erkannt weil: Komponente X, Energie Y, Kein Ausschluss Z" Weitere Aenderungen: - BAuA/OSHA Regulatory Hints: 3 Enrich-Endpoints (per Hazard, per Measure, Batch) - Dokumente-Tab in IACE-Bibliothek (36.708 Chunks aus Qdrant) - Varianten-UX: Basis-Projekt-Summary auf Varianten-Seite - Projekt-Initialisierung: POST /initialize kettet Parse→Komponenten→Patterns→Hazards→Massnahmen→Normen - 18 pre-existing TS-Fehler gefixt, Route-Konflikt behoben - Component-Library + Measures-Library Tests aktualisiert Tests: Go alle bestanden, TS 0 Fehler, Playwright 141+ bestanden Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
7.6 KiB
TypeScript
192 lines
7.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useMemo, useState, useRef, useEffect } from 'react'
|
|
import { SearchInput, FilterDropdown, Pagination, ExternalLinkIcon } from './LibraryTable'
|
|
|
|
export interface CEDocument {
|
|
regulation_id: string
|
|
name_de: string
|
|
name_en: string
|
|
category: string
|
|
source_url: string
|
|
source_org: string
|
|
chunk_count: number
|
|
}
|
|
|
|
const PER_PAGE = 50
|
|
|
|
const CATEGORY_LABELS: Record<string, string> = {
|
|
trbs: 'TRBS — Betriebssicherheit',
|
|
trgs: 'TRGS — Gefahrstoffe',
|
|
asr: 'ASR — Arbeitsstaetten',
|
|
osha: 'OSHA — US Occupational Safety',
|
|
ce_machinery: 'EU — Maschinenrecht',
|
|
ce_machinery_guidance: 'EU — Leitfaeden',
|
|
ce_electrical: 'EU — Niederspannung',
|
|
ce_emc: 'EU — EMV',
|
|
ce_radio: 'EU — Funkanlagen',
|
|
ce_ai: 'EU — KI-Verordnung',
|
|
ce_ai_safety: 'KI-Sicherheit',
|
|
ce_software_safety: 'Software-Sicherheit',
|
|
ce_software_security: 'Software-Security',
|
|
ce_software_weaknesses: 'Software-Schwachstellen',
|
|
ce_ot_cybersecurity: 'OT-Cybersecurity',
|
|
eu_recht: 'EU-Recht',
|
|
eu_datenschutz: 'EU-Datenschutz',
|
|
guidance: 'Guidance',
|
|
}
|
|
|
|
function categoryLabel(cat: string): string {
|
|
return CATEGORY_LABELS[cat] || cat.replace(/_/g, ' ')
|
|
}
|
|
|
|
function categoryColor(cat: string): string {
|
|
if (cat.startsWith('trbs')) return 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300'
|
|
if (cat.startsWith('trgs')) return 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300'
|
|
if (cat.startsWith('asr')) return 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300'
|
|
if (cat === 'osha') return 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300'
|
|
if (cat.startsWith('ce_')) return 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300'
|
|
if (cat.startsWith('eu_')) return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300'
|
|
return 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300'
|
|
}
|
|
|
|
interface Props {
|
|
documents: CEDocument[]
|
|
loading?: boolean
|
|
}
|
|
|
|
export default function DokumenteTab({ documents, loading }: Props) {
|
|
const [search, setSearch] = useState('')
|
|
const [debounced, setDebounced] = useState('')
|
|
const [categoryFilter, setCategoryFilter] = useState('')
|
|
const [page, setPage] = useState(1)
|
|
const timer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
useEffect(() => {
|
|
timer.current = setTimeout(() => setDebounced(search), 300)
|
|
return () => { if (timer.current) clearTimeout(timer.current) }
|
|
}, [search])
|
|
|
|
useEffect(() => { setPage(1) }, [debounced, categoryFilter])
|
|
|
|
const categories = useMemo(() => {
|
|
const cats = new Set(documents.map((d) => d.category))
|
|
const opts = [{ value: '', label: 'Alle Kategorien' }]
|
|
Array.from(cats).sort().forEach((c) => opts.push({ value: c, label: categoryLabel(c) }))
|
|
return opts
|
|
}, [documents])
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = debounced.toLowerCase()
|
|
return documents.filter((d) => {
|
|
if (categoryFilter && d.category !== categoryFilter) return false
|
|
if (q) {
|
|
const hay = `${d.regulation_id} ${d.name_de} ${d.name_en} ${d.source_org}`.toLowerCase()
|
|
if (!hay.includes(q)) return false
|
|
}
|
|
return true
|
|
})
|
|
}, [documents, debounced, categoryFilter])
|
|
|
|
const totalPages = Math.ceil(filtered.length / PER_PAGE)
|
|
const pageItems = filtered.slice((page - 1) * PER_PAGE, page * PER_PAGE)
|
|
|
|
const totalChunks = filtered.reduce((sum, d) => sum + d.chunk_count, 0)
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="flex items-center justify-center py-12 gap-3">
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-purple-600" />
|
|
<span className="text-sm text-gray-500">Lade Dokumentenindex aus Qdrant...</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Stats bar */}
|
|
<div className="flex flex-wrap items-center gap-3 text-sm text-gray-600 dark:text-gray-400">
|
|
<span className="font-medium">{documents.length} Dokumente</span>
|
|
<span>|</span>
|
|
<span>{documents.reduce((s, d) => s + d.chunk_count, 0).toLocaleString()} Chunks im Vektorspeicher</span>
|
|
<span>|</span>
|
|
<span>Quellen: BAuA, OSHA, EUR-Lex, NIST, ENISA</span>
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
<div className="flex-1 min-w-[200px]">
|
|
<SearchInput value={search} onChange={setSearch} placeholder="Dokument suchen (z.B. TRBS 2111, Maschinenverordnung)..." />
|
|
</div>
|
|
<FilterDropdown label="Kategorie" value={categoryFilter} options={categories} onChange={setCategoryFilter} />
|
|
<span className="text-sm text-gray-500 dark:text-gray-400 ml-auto">
|
|
{filtered.length !== documents.length && `${filtered.length} gefiltert | `}{totalChunks.toLocaleString()} Chunks
|
|
</span>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div className="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-gray-100 dark:bg-gray-800">
|
|
<tr>
|
|
{['Kennung', 'Bezeichnung', 'Kategorie', 'Quelle', 'Chunks'].map((h) => (
|
|
<th key={h} className="px-4 py-2.5 text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{pageItems.map((d) => (
|
|
<tr
|
|
key={d.regulation_id}
|
|
className="hover:bg-purple-50/50 dark:hover:bg-purple-900/10 transition-colors even:bg-gray-50/50 dark:even:bg-gray-800/30"
|
|
>
|
|
<td className="px-4 py-2.5 text-sm font-mono text-gray-700 dark:text-gray-300 whitespace-nowrap">
|
|
{d.regulation_id}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300">
|
|
<div className="max-w-md">
|
|
<div className="truncate">{d.name_de || d.name_en || d.regulation_id}</div>
|
|
{d.source_url && (
|
|
<a
|
|
href={d.source_url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
Quelle<ExternalLinkIcon />
|
|
</a>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-2.5 whitespace-nowrap">
|
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${categoryColor(d.category)}`}>
|
|
{categoryLabel(d.category)}
|
|
</span>
|
|
</td>
|
|
<td className="px-4 py-2.5 text-sm text-gray-500 whitespace-nowrap">
|
|
{d.source_org || '-'}
|
|
</td>
|
|
<td className="px-4 py-2.5 text-sm font-mono text-gray-500 text-right whitespace-nowrap">
|
|
{d.chunk_count.toLocaleString()}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{pageItems.length === 0 && (
|
|
<tr>
|
|
<td colSpan={5} className="px-4 py-8 text-center text-sm text-gray-400">
|
|
{documents.length === 0
|
|
? 'Keine Dokumente im CE-Corpus gefunden. Qdrant-Verbindung pruefen.'
|
|
: 'Keine Dokumente fuer diesen Filter gefunden'}
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<Pagination page={page} totalPages={totalPages} onPageChange={setPage} />
|
|
</div>
|
|
)
|
|
}
|