From 8bb90d73e5e83145ff552f7d256a39711dd6735c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 13 May 2026 01:02:33 +0200 Subject: [PATCH] feat(iace): benchmark system + erklaerteil + dedup-fix - Erklaerteil-Template fuer Risikobeurteilungen (risk_assessment_template.go) in PDF-Export, Markdown-Export und Frontend ReportPrintView eingebaut - Ground Truth Benchmark-System: Datenmodell, Fuzzy-Matching-Engine, 3 API Endpoints (import-gt, benchmark, benchmark/summary) - Frontend Benchmark-Tab mit Score-Cards, Kategorie-Breakdown, Hazard-Vergleichstabelle (Zugeordnet/Fehlend/Extra), Business Impact - Erster Benchmark: 13.3% Coverage (Baseline) gegen 60 GT-Eintraege - Dedup-Fix: seenCat[cat] -> seenCatZone[cat+zone] erlaubt mehrere Gefaehrdungen pro Kategorie an verschiedenen Gefahrenstellen - Komponenten-spezifische Hazard-Namen und Zone-basierte Zuordnung Co-Authored-By: Claude Opus 4.6 (1M context) --- .../_components/CategoryBreakdown.tsx | 46 + .../benchmark/_components/GTImportForm.tsx | 121 + .../_components/HazardComparisonTable.tsx | 158 + .../benchmark/_hooks/useBenchmark.ts | 115 + .../sdk/iace/[projectId]/benchmark/page.tsx | 160 + .../tech-file/_components/ReportPrintView.tsx | 53 +- admin-compliance/app/sdk/iace/layout.tsx | 1 + .../api/handlers/iace_handler_benchmark.go | 162 ++ .../api/handlers/iace_handler_init.go | 35 +- .../api/handlers/iace_handler_init_helpers.go | 9 + ai-compliance-sdk/internal/app/routes.go | 3 + .../internal/iace/benchmark_matcher.go | 365 +++ .../internal/iace/benchmark_types.go | 135 + .../internal/iace/document_export.go | 9 + .../internal/iace/document_export_pdf.go | 27 + .../internal/iace/risk_assessment_template.go | 53 + .../internal/iace/store_projects.go | 12 + .../iace/testdata/ground_truth_bremse.json | 2570 +++++++++++++++++ 18 files changed, 4029 insertions(+), 5 deletions(-) create mode 100644 admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/CategoryBreakdown.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/GTImportForm.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/HazardComparisonTable.tsx create mode 100644 admin-compliance/app/sdk/iace/[projectId]/benchmark/_hooks/useBenchmark.ts create mode 100644 admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx create mode 100644 ai-compliance-sdk/internal/api/handlers/iace_handler_benchmark.go create mode 100644 ai-compliance-sdk/internal/iace/benchmark_matcher.go create mode 100644 ai-compliance-sdk/internal/iace/benchmark_types.go create mode 100644 ai-compliance-sdk/internal/iace/risk_assessment_template.go create mode 100644 ai-compliance-sdk/internal/iace/testdata/ground_truth_bremse.json diff --git a/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/CategoryBreakdown.tsx b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/CategoryBreakdown.tsx new file mode 100644 index 0000000..c72c3ae --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/CategoryBreakdown.tsx @@ -0,0 +1,46 @@ +'use client' + +import React from 'react' +import type { CategoryScore } from '../_hooks/useBenchmark' + +interface Props { breakdown: CategoryScore[] } + +const CATEGORY_LABELS: Record = { + 'mechanische gefaehrdungen': 'Mechanisch', + 'elektrische gefaehrdungen': 'Elektrisch', + 'thermische gefaehrdungen': 'Thermisch', + 'laerm': 'Laerm', + 'vibration': 'Vibration', + 'strahlung': 'Strahlung', + 'materialien und substanzen': 'Materialien/Substanzen', + 'ergonomische gefaehrdungen': 'Ergonomie', + 'einsatzumgebung': 'Einsatzumgebung', +} + +export function CategoryBreakdown({ breakdown }: Props) { + if (!breakdown || breakdown.length === 0) return null + + return ( +
+

Coverage nach Gefaehrdungsgruppe

+
+ {breakdown.map((cat) => { + const label = CATEGORY_LABELS[cat.category] || cat.category + const pct = Math.round(cat.coverage * 100) + const barColor = pct >= 80 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500' + return ( +
+
+ {label} + {cat.match_count}/{cat.gt_count} ({pct}%) +
+
+
+
+
+ ) + })} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/GTImportForm.tsx b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/GTImportForm.tsx new file mode 100644 index 0000000..ac69519 --- /dev/null +++ b/admin-compliance/app/sdk/iace/[projectId]/benchmark/_components/GTImportForm.tsx @@ -0,0 +1,121 @@ +'use client' + +import React, { useState, useRef } from 'react' +import type { GroundTruthEntry } from '../_hooks/useBenchmark' + +interface Props { + onImport: (gt: { entries: GroundTruthEntry[]; source_file?: string; description?: string }) => Promise + loading: boolean +} + +export function GTImportForm({ onImport, loading }: Props) { + const [jsonText, setJsonText] = useState('') + const [parseError, setParseError] = useState(null) + const [preview, setPreview] = useState<{ count: number; groups: Record } | null>(null) + const fileRef = useRef(null) + + function tryParse(text: string) { + setJsonText(text) + setParseError(null) + setPreview(null) + if (!text.trim()) return + + try { + const parsed = JSON.parse(text) + const entries: GroundTruthEntry[] = parsed.entries || parsed + if (!Array.isArray(entries) || entries.length === 0) { + setParseError('JSON muss ein Array "entries" enthalten') + return + } + // Validate first entry has required fields + const first = entries[0] + if (!first.hazard_type && !first.hazard_group) { + setParseError('Eintraege muessen hazard_type oder hazard_group enthalten') + return + } + // Build preview + const groups: Record = {} + for (const e of entries) { + const g = e.hazard_group || 'Unbekannt' + groups[g] = (groups[g] || 0) + 1 + } + setPreview({ count: entries.length, groups }) + } catch (err) { + setParseError('Ungueltiges JSON: ' + (err instanceof Error ? err.message : String(err))) + } + } + + async function handleImport() { + if (!jsonText.trim()) return + try { + const parsed = JSON.parse(jsonText) + const gt = parsed.entries ? parsed : { entries: parsed } + await onImport(gt) + setJsonText('') + setPreview(null) + } catch (err) { + setParseError(err instanceof Error ? err.message : 'Import fehlgeschlagen') + } + } + + function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = (ev) => { + const text = ev.target?.result as string + tryParse(text) + } + reader.readAsText(file) + } + + return ( +
+

Ground Truth importieren

+

+ JSON-Datei mit der professionellen Risikobeurteilung einfuegen oder hochladen. +

+ +
+ + +
+ +