Files
breakpilot-compliance/admin-compliance/app/sdk/iace/library/_components/DokumenteTab.tsx
T
Benjamin Admin 2e29b611c9 feat(iace): Phase 1 — Haftungs-Fixes, Massnahmen-Verkabelung, Explainability Engine
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>
2026-05-09 21:32:23 +02:00

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