Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/AgentFindingCard.tsx
T
Benjamin Admin 3f23a64d5f feat(agent): Impressum-Tab auf Haupt-Engine + Profil/§36-Fixes
Ergebnis-Tab rendert jetzt result.results (Haupt-Doc-Check) statt des
abweichenden v3-Agenten — BMW korrekt statt False Positives:
- DocResultView: ein Dokument als Pflichtangaben-Tabelle (Label + gefundener
  Text + 3-Tier-Status), KEINE MC-IDs. ComplianceResultTabs speist Tabs aus
  result.results; ChecklistView-Bausteine exportiert + wiederverwendet.
- profile_extractor: Firmenname/Rechtsform = fruehester Treffer + ausge-
  schriebene Formen (Aktiengesellschaft) -> BMW AG statt "juris GmbH".
- 36 VSBG (MC-010): reines b2c -> POSSIBLY_APPLICABLE (Pruef-Hinweis) statt
  MEDIUM-FAIL; hart nur bei ecommerce. possibly_hint pro MC.
- McCoverage traegt label + found (Snippet); mc_possibly-Aggregat.
- AgentFindingCard/Methodik: interne check_id/mc_id nicht mehr angezeigt.

Tests: test_four_status (16) + Frontend-Vitest gruen; CI-Suite 206, v3/GT
unveraendert. Nur eigene Dateien (geteilter Tree).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 23:44:01 +02:00

151 lines
4.5 KiB
TypeScript

'use client'
/**
* Strukturierte Finding-Anzeige.
* Layout:
* [Severity-Badge] [Methodik-Badge(s)]
* [Titel]
* ┌ Gesetzliche Basis / Norm ─────────┐
* │ § 5 Abs. 1 Nr. 1 TMG │
* └────────────────────────────────────┘
* ┌ Befund / Wörtlich ───────────────┐
* │ "Vorstand: …" │
* └────────────────────────────────────┘
* ┌ Empfehlung / Best Practice ──────┐
* │ → Konkrete Maßnahme │
* └────────────────────────────────────┘
*/
import React from 'react'
import type { Finding, SourceType } from './_agentTypes'
import {
METHODIK_COLOR,
METHODIK_LABEL,
METHODIK_SHORT,
SEVERITY_BG,
SEVERITY_COLOR,
STATUS_LABEL,
STATUS_STYLE,
} from './_agentTypes'
export function AgentFindingCard({ f }: { f: Finding }) {
const sev = f.severity
const color = SEVERITY_COLOR[sev]
const bg = SEVERITY_BG[sev]
const sources = f.sources || []
// Verdikt-Pill nur für Nicht-FAIL-Status (Applicability/Unknown) —
// macht klar: kein Verstoß, sondern Hinweis/unbestimmt.
const statusLabel = f.status ? STATUS_LABEL[f.status] : undefined
const statusStyle = f.status ? STATUS_STYLE[f.status] : undefined
return (
<div
className="rounded border-l-4 p-3 space-y-2"
style={{ borderLeftColor: color, background: bg }}
>
<div className="flex items-center flex-wrap gap-2">
<span
className="text-xs font-bold px-2 py-0.5 rounded text-white"
style={{ background: color }}
>
{sev}
</span>
{statusLabel && statusStyle && (
<span
className="text-[10px] font-semibold px-1.5 py-0.5 rounded"
style={{ background: statusStyle.bg, color: statusStyle.fg }}
>
{statusLabel}
</span>
)}
{sources.map((s, i) => (
<MethodikBadge key={i} src={s.source_type} />
))}
{f.confidence !== undefined && (
<span className="text-[10px] text-gray-500 ml-auto">
Konfidenz {(f.confidence * 100).toFixed(0)}%
</span>
)}
</div>
<div className="text-sm font-medium text-gray-900">{f.title}</div>
{f.norm && (
<Block label="Gesetzliche Basis" tone="purple">
{f.norm}
</Block>
)}
{f.evidence && (
<Block label="Befund" tone="amber">
<span className="italic">{f.evidence}"</span>
</Block>
)}
{f.action && (
<Block
label={
sources.some(s =>
s.source_type === 'llm_local' ||
s.source_type === 'llm_local_big' ||
s.source_type === 'llm_cloud'
)
? 'Empfehlung (LLM-Vorschlag)'
: f.status === 'insufficient_evidence' ||
f.status === 'possibly_applicable'
? 'Prüf-Hinweis'
: sev === 'HIGH'
? 'Pflicht-Maßnahme'
: 'Best-Practice-Empfehlung'
}
tone="green"
>
{f.action}
</Block>
)}
</div>
)
}
function MethodikBadge({
src, sourceId,
}: { src: SourceType; sourceId?: string }) {
const { bg, fg } = METHODIK_COLOR[src] || { bg: '#e5e7eb', fg: '#374151' }
const title = `${METHODIK_LABEL[src]}${sourceId ? ` · ${sourceId}` : ''}`
return (
<span
title={title}
className="text-[10px] px-1.5 py-0.5 rounded font-mono"
style={{ background: bg, color: fg }}
>
{METHODIK_SHORT[src]}
</span>
)
}
function Block({
label, tone, children,
}: {
label: string
tone: 'purple' | 'amber' | 'green'
children: React.ReactNode
}) {
const toneMap = {
purple: { border: '#a78bfa', bg: '#f5f3ff', label: '#5b21b6' },
amber: { border: '#fbbf24', bg: '#fffbeb', label: '#92400e' },
green: { border: '#34d399', bg: '#ecfdf5', label: '#065f46' },
} as const
const t = toneMap[tone]
return (
<div
className="rounded px-2 py-1.5 text-xs"
style={{ background: t.bg, borderLeft: `3px solid ${t.border}` }}
>
<div className="font-semibold mb-0.5" style={{ color: t.label }}>
{label}
</div>
<div className="text-gray-800">{children}</div>
</div>
)
}