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>
This commit is contained in:
@@ -58,9 +58,8 @@ export function AgentFindingCard({ f }: { f: Finding }) {
|
||||
{statusLabel}
|
||||
</span>
|
||||
)}
|
||||
<code className="text-[11px] text-gray-500">{f.check_id}</code>
|
||||
{sources.map((s, i) => (
|
||||
<MethodikBadge key={i} src={s.source_type} sourceId={s.source_id} />
|
||||
<MethodikBadge key={i} src={s.source_type} />
|
||||
))}
|
||||
{f.confidence !== undefined && (
|
||||
<span className="text-[10px] text-gray-500 ml-auto">
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* "Was wurde geprüft" — listet alle MCs eines Agents mit ihrem Status.
|
||||
* Standardmäßig collapsed; zeigt sofort, was Methodik des Agents war.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import type { McCoverage } from './_agentTypes'
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
ok: '#10b981',
|
||||
na: '#94a3b8',
|
||||
skipped: '#cbd5e1',
|
||||
high: '#dc2626',
|
||||
medium: '#f59e0b',
|
||||
low: '#3b82f6',
|
||||
insufficient_evidence: '#64748b',
|
||||
possibly_applicable: '#ca8a04',
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
ok: 'OK',
|
||||
na: 'n/a',
|
||||
skipped: 'übersprungen',
|
||||
high: 'HIGH',
|
||||
medium: 'MEDIUM',
|
||||
low: 'LOW',
|
||||
insufficient_evidence: 'unklar',
|
||||
possibly_applicable: 'evtl. relevant',
|
||||
}
|
||||
|
||||
export function AgentMcCoverage({ coverage }: { coverage: McCoverage[] }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
if (!coverage?.length) return null
|
||||
return (
|
||||
<div className="border rounded bg-slate-50">
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
className="w-full text-left px-3 py-2 text-xs font-semibold uppercase text-gray-700 flex justify-between items-center"
|
||||
>
|
||||
<span>Was wurde geprüft? ({coverage.length} MCs)</span>
|
||||
<span className="text-gray-400">{open ? '▾' : '▸'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="border-t bg-white p-2 space-y-0.5 max-h-60 overflow-y-auto">
|
||||
{coverage.map(c => (
|
||||
<div key={c.mc_id} className="flex items-center gap-2 text-xs">
|
||||
<span
|
||||
className="w-2 h-2 rounded-full inline-block"
|
||||
style={{ background: STATUS_COLOR[c.status] || '#cbd5e1' }}
|
||||
/>
|
||||
<code className="text-gray-500">{c.mc_id}</code>
|
||||
<span className="text-gray-700">
|
||||
{STATUS_LABEL[c.status] || c.status}
|
||||
</span>
|
||||
{c.reason && (
|
||||
<span className="text-gray-400 italic">— {c.reason}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* AgentPflichtTable — die geprüften Pflichtangaben als menschliche Tabelle:
|
||||
* Status-Icon + Feldname + tatsächlich gefundener Text. Ersetzt die alte
|
||||
* MC-ID-Liste.
|
||||
*
|
||||
* WICHTIG: zeigt NIE die mc_id (Reverse-Engineering-Schutz der MC-Bibliothek)
|
||||
* — nur das menschliche `label`. Generisch für jeden Agenten verwendbar.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import type { McCoverage } from './_agentTypes'
|
||||
|
||||
const DISP: Record<string, { icon: string; text: string; color: string }> = {
|
||||
ok: { icon: '✓', text: 'vorhanden', color: '#16a34a' },
|
||||
high: { icon: '✗', text: 'fehlt', color: '#dc2626' },
|
||||
medium: { icon: '✗', text: 'fehlt', color: '#d97706' },
|
||||
low: { icon: '✗', text: 'fehlt', color: '#2563eb' },
|
||||
possibly_applicable: { icon: '?', text: 'zu prüfen', color: '#ca8a04' },
|
||||
insufficient_evidence: { icon: '?', text: 'unklar', color: '#64748b' },
|
||||
na: { icon: '–', text: 'nicht anwendbar', color: '#94a3b8' },
|
||||
skipped: { icon: '–', text: 'nicht geprüft', color: '#cbd5e1' },
|
||||
}
|
||||
|
||||
// Reihenfolge: Probleme zuerst, dann erfüllt, dann n/a.
|
||||
const RANK: Record<string, number> = {
|
||||
high: 0, medium: 1, low: 2, possibly_applicable: 3,
|
||||
insufficient_evidence: 4, ok: 5, na: 6, skipped: 7,
|
||||
}
|
||||
|
||||
export function AgentPflichtTable({ coverage }: { coverage: McCoverage[] }) {
|
||||
if (!coverage?.length) return null
|
||||
const rows = [...coverage].sort(
|
||||
(a, b) => (RANK[a.status] ?? 9) - (RANK[b.status] ?? 9),
|
||||
)
|
||||
const count = (s: string) => coverage.filter(c => c.status === s).length
|
||||
const ok = count('ok')
|
||||
const fehlt = count('high') + count('medium') + count('low')
|
||||
const pruefen = count('possibly_applicable') + count('insufficient_evidence')
|
||||
const na = count('na') + count('skipped')
|
||||
|
||||
return (
|
||||
<div className="border rounded overflow-hidden">
|
||||
<div className="px-3 py-2 text-xs font-semibold uppercase text-gray-700 border-b bg-slate-50">
|
||||
Pflichtangaben — <span className="text-green-700">{ok} vorhanden</span>
|
||||
{fehlt > 0 && <> · <span className="text-red-600">{fehlt} fehlt</span></>}
|
||||
{pruefen > 0 && (
|
||||
<> · <span className="text-yellow-700">{pruefen} zu prüfen</span></>
|
||||
)}
|
||||
{na > 0 && <> · <span className="text-gray-400">{na} n/a</span></>}
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{rows.map((c, i) => {
|
||||
const d = DISP[c.status] || DISP.skipped
|
||||
return (
|
||||
<div key={i} className="flex items-start gap-2 px-3 py-1.5 text-xs">
|
||||
<span
|
||||
className="font-bold w-4 text-center shrink-0"
|
||||
style={{ color: d.color }}
|
||||
aria-label={d.text}
|
||||
>
|
||||
{d.icon}
|
||||
</span>
|
||||
<span className="font-medium text-gray-800 w-52 shrink-0">
|
||||
{c.label || 'Angabe'}
|
||||
</span>
|
||||
<span className="text-gray-500 flex-1 min-w-0 break-words">
|
||||
{c.status === 'ok' ? (
|
||||
<span className="italic">{c.found || 'vorhanden'}</span>
|
||||
) : (
|
||||
<span style={{ color: d.color }}>{d.text}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import React, { useState } from 'react'
|
||||
|
||||
import type { Severity, SlotOutput } from './_agentTypes'
|
||||
import { AgentFindingCard } from './AgentFindingCard'
|
||||
import { AgentMcCoverage } from './AgentMcCoverage'
|
||||
import { AgentPflichtTable } from './AgentPflichtTable'
|
||||
import { AgentRecommendationCard } from './AgentRecommendationCard'
|
||||
import { AgentSpeedometer } from './AgentSpeedometer'
|
||||
|
||||
@@ -42,7 +42,7 @@ export function AgentResultView({ output }: { output: SlotOutput }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AgentMcCoverage coverage={output.mc_coverage} />
|
||||
<AgentPflichtTable coverage={output.mc_coverage} />
|
||||
|
||||
<AgentSpeedometer
|
||||
total={output.mc_total}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
interface CheckItem {
|
||||
export interface CheckItem {
|
||||
id: string
|
||||
label: string
|
||||
passed: boolean
|
||||
@@ -14,7 +14,7 @@ interface CheckItem {
|
||||
hint?: string
|
||||
}
|
||||
|
||||
interface DocResult {
|
||||
export interface DocResult {
|
||||
label: string
|
||||
url: string
|
||||
doc_type: string
|
||||
@@ -27,14 +27,14 @@ interface DocResult {
|
||||
scenario?: string // regenerate | fix | import | skip
|
||||
}
|
||||
|
||||
const SCENARIO_LABELS: Record<string, { label: string; color: string; bg: string }> = {
|
||||
export const SCENARIO_LABELS: Record<string, { label: string; color: string; bg: string }> = {
|
||||
regenerate: { label: 'Neugenerierung', color: 'text-red-700', bg: 'bg-red-100' },
|
||||
fix: { label: 'Korrekturen', color: 'text-amber-700', bg: 'bg-amber-100' },
|
||||
import: { label: 'Konform', color: 'text-green-700', bg: 'bg-green-100' },
|
||||
missing: { label: 'Fehlt', color: 'text-gray-600', bg: 'bg-gray-100' },
|
||||
}
|
||||
|
||||
const DOC_TYPE_LABELS: Record<string, string> = {
|
||||
export const DOC_TYPE_LABELS: Record<string, string> = {
|
||||
dse: 'DSI', agb: 'AGB', impressum: 'Impressum',
|
||||
cookie: 'Cookie', widerruf: 'Widerruf', other: 'Sonstiges',
|
||||
social_media: 'Social Media', dsfa: 'DSFA', joint_controller: 'Art. 26',
|
||||
@@ -46,7 +46,7 @@ interface GroupedCheck {
|
||||
children: CheckItem[]
|
||||
}
|
||||
|
||||
function groupChecks(checks: CheckItem[]): GroupedCheck[] {
|
||||
export function groupChecks(checks: CheckItem[]): GroupedCheck[] {
|
||||
const l1 = checks.filter(c => (c.level ?? 1) === 1)
|
||||
return l1.map(c => ({
|
||||
check: c,
|
||||
@@ -54,7 +54,7 @@ function groupChecks(checks: CheckItem[]): GroupedCheck[] {
|
||||
}))
|
||||
}
|
||||
|
||||
function CheckIcon({ passed, skipped, isInfo }: { passed: boolean; skipped?: boolean; isInfo?: boolean }) {
|
||||
export function CheckIcon({ passed, skipped, isInfo }: { passed: boolean; skipped?: boolean; isInfo?: boolean }) {
|
||||
if (skipped) {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-gray-300 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -10,21 +10,16 @@
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
import type { SlotOutput } from './_agentTypes'
|
||||
import { AgentResultTab } from './AgentResultTab'
|
||||
import { ChecklistView } from './ChecklistView'
|
||||
import { ChecklistView, DOC_TYPE_LABELS, type DocResult } from './ChecklistView'
|
||||
import { DocResultView } from './DocResultView'
|
||||
import { MigrationPanel } from './MigrationPanel'
|
||||
|
||||
const TOPIC_LABELS: Record<string, string> = {
|
||||
impressum: 'Impressum',
|
||||
cookie: 'Cookie-Banner',
|
||||
}
|
||||
|
||||
export function ComplianceResultTabs({ results }: { results: any }) {
|
||||
const agentOutputs: Record<string, SlotOutput> = results.agent_outputs || {}
|
||||
const topicKeys = Object.keys(agentOutputs)
|
||||
const tabs = [...topicKeys, 'raw']
|
||||
const [active, setActive] = useState<string>(tabs[0])
|
||||
// Themen-Tabs aus der HAUPT-Engine (result.results) — nicht aus dem
|
||||
// v3-Agent. Jedes Dokument = ein Tab mit der genauen Pflichtangaben-Tabelle.
|
||||
const docs: DocResult[] = results.results || []
|
||||
const tabs = docs.map((_: DocResult, i: number) => String(i)).concat('raw')
|
||||
const [active, setActive] = useState<string>(tabs[0] ?? 'raw')
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-4">
|
||||
@@ -112,26 +107,30 @@ export function ComplianceResultTabs({ results }: { results: any }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab-Leiste — Themen-Agenten + Roh-Checkliste */}
|
||||
{/* Tab-Leiste — ein Tab je Dokument (Haupt-Engine) + Übersicht */}
|
||||
<div className="flex gap-1 border-b border-gray-200 flex-wrap">
|
||||
{tabs.map(t => {
|
||||
const count = t !== 'raw' ? (agentOutputs[t]?.findings?.length ?? 0) : 0
|
||||
const tabClass = `px-3 py-1.5 text-sm font-medium border-b-2 -mb-px transition-colors flex items-center gap-1.5 ${
|
||||
active === t
|
||||
? 'border-purple-500 text-purple-700'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`
|
||||
if (t === 'raw') {
|
||||
return (
|
||||
<button key={t} onClick={() => setActive(t)} className={tabClass}>
|
||||
Alle Checks
|
||||
</button>
|
||||
)
|
||||
}
|
||||
const doc = docs[Number(t)]
|
||||
const dot = doc.error ? 'bg-gray-300'
|
||||
: doc.scenario === 'import' ? 'bg-green-500'
|
||||
: doc.scenario === 'fix' ? 'bg-amber-500'
|
||||
: doc.scenario === 'regenerate' ? 'bg-red-500' : 'bg-gray-400'
|
||||
return (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => setActive(t)}
|
||||
className={`px-3 py-1.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
||||
active === t
|
||||
? 'border-purple-500 text-purple-700'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
{t === 'raw' ? 'Alle Checks (roh)' : (TOPIC_LABELS[t] || t)}
|
||||
{count > 0 && (
|
||||
<span className="ml-1.5 text-xs bg-gray-100 text-gray-600 rounded-full px-1.5">
|
||||
{count}
|
||||
</span>
|
||||
)}
|
||||
<button key={t} onClick={() => setActive(t)} className={tabClass}>
|
||||
<span className={`w-2 h-2 rounded-full ${dot}`} />
|
||||
{DOC_TYPE_LABELS[doc.doc_type] || doc.doc_type}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
@@ -140,11 +139,8 @@ export function ComplianceResultTabs({ results }: { results: any }) {
|
||||
{/* Tab-Inhalt */}
|
||||
{active === 'raw' ? (
|
||||
<ChecklistView results={results.results} />
|
||||
) : agentOutputs[active] ? (
|
||||
<AgentResultTab
|
||||
topicLabel={TOPIC_LABELS[active] || active}
|
||||
output={agentOutputs[active]}
|
||||
/>
|
||||
) : docs[Number(active)] ? (
|
||||
<DocResultView doc={docs[Number(active)]} />
|
||||
) : null}
|
||||
|
||||
{/* Check-Footer (themenübergreifend) */}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DocResultView — EIN Dokument-Prüfergebnis der HAUPT-Engine als saubere,
|
||||
* immer-offene Pflichtangaben-Tabelle: Verdikt + Gruppen + extrahierte Texte
|
||||
* (matched_text) pro Prüfpunkt.
|
||||
*
|
||||
* Quelle = result.results[doc] (die genaue Haupt-Doc-Check-Engine), NICHT
|
||||
* der v3-Agent. Zeigt menschliche Labels + gefundene Snippets, keine internen
|
||||
* IDs. Wiederverwendet die Render-Bausteine aus ChecklistView.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {
|
||||
CheckIcon,
|
||||
type DocResult,
|
||||
groupChecks,
|
||||
SCENARIO_LABELS,
|
||||
} from './ChecklistView'
|
||||
|
||||
function Snippet({ text }: { text: string }) {
|
||||
return (
|
||||
<div className="text-xs text-gray-500 mt-0.5 font-mono break-words">
|
||||
„…{text}…"
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScoreBar({ label, pct, blue }: { label: string; pct: number; blue?: boolean }) {
|
||||
const color = blue
|
||||
? pct >= 80 ? 'bg-blue-400' : 'bg-blue-300'
|
||||
: pct === 100 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500'
|
||||
return (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-[10px] text-gray-400">{label}</span>
|
||||
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className={`h-full rounded-full ${color}`} style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
<span className="text-gray-600 w-9 text-right">{pct}%</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function DocResultView({ doc }: { doc: DocResult }) {
|
||||
if (doc.error) {
|
||||
return (
|
||||
<div className="text-sm text-amber-700 bg-amber-50 rounded p-3">
|
||||
{doc.error}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const grouped = groupChecks(doc.checks)
|
||||
const l1 = doc.checks.filter(c => (c.level ?? 1) === 1)
|
||||
const l1Score = l1.filter(c => c.severity !== 'INFO')
|
||||
const l1Passed = l1Score.filter(c => c.passed).length
|
||||
const l2 = doc.checks.filter(c => (c.level ?? 1) === 2 && !c.skipped)
|
||||
const l2Passed = l2.filter(c => c.passed).length
|
||||
const sc = doc.scenario ? SCENARIO_LABELS[doc.scenario] : null
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Verdikt-Kopf */}
|
||||
<div className="flex items-center flex-wrap gap-3 border rounded-lg px-4 py-3 bg-slate-50">
|
||||
{sc && (
|
||||
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full ${sc.bg} ${sc.color}`}>
|
||||
{sc.label}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-sm text-gray-700">
|
||||
{l1Passed}/{l1Score.length} Pflichtangaben
|
||||
{l2.length > 0 && <>, {l2Passed}/{l2.length} Detailprüfungen</>}
|
||||
</span>
|
||||
<div className="flex gap-3 ml-auto">
|
||||
<ScoreBar label="Pflicht" pct={doc.completeness_pct} />
|
||||
{l2.length > 0 && (
|
||||
<ScoreBar label="Detail" pct={doc.correctness_pct ?? 0} blue />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pflichtangaben-Tabelle */}
|
||||
<div className="border rounded-lg divide-y divide-gray-100">
|
||||
{grouped.map(g => {
|
||||
const l1Info = g.check.severity === 'INFO' && !g.check.passed
|
||||
return (
|
||||
<div key={g.check.id} className="px-4 py-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckIcon passed={g.check.passed} isInfo={l1Info} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-sm ${
|
||||
g.check.passed ? 'text-gray-800'
|
||||
: l1Info ? 'text-gray-500' : 'text-red-700 font-medium'
|
||||
}`}>
|
||||
{g.check.label}
|
||||
</div>
|
||||
{g.check.passed && g.check.matched_text && g.children.length === 0 && (
|
||||
<Snippet text={g.check.matched_text} />
|
||||
)}
|
||||
{!g.check.passed && g.check.hint && (
|
||||
<div className={`text-xs mt-0.5 ${l1Info ? 'text-gray-400' : 'text-red-600/80'}`}>
|
||||
{g.check.hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{g.children.length > 0 && (
|
||||
<div className="ml-6 mt-1 space-y-1 border-l-2 border-gray-200 pl-3">
|
||||
{g.children.map(ch => {
|
||||
const chInfo = ch.severity === 'INFO' && !ch.passed && !ch.skipped
|
||||
return (
|
||||
<div key={ch.id} className="flex items-start gap-2">
|
||||
<CheckIcon passed={ch.passed} skipped={ch.skipped} isInfo={chInfo} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className={`text-xs ${
|
||||
ch.skipped ? 'text-gray-400 italic'
|
||||
: ch.passed ? 'text-gray-600'
|
||||
: chInfo ? 'text-gray-400' : 'text-red-600 font-medium'
|
||||
}`}>
|
||||
{ch.label}{ch.skipped && ' (übersprungen)'}
|
||||
</div>
|
||||
{ch.passed && ch.matched_text && <Snippet text={ch.matched_text} />}
|
||||
{!ch.passed && !ch.skipped && ch.hint && (
|
||||
<div className={`text-xs mt-0.5 ${chInfo ? 'text-gray-400' : 'text-red-500/80'}`}>
|
||||
{ch.hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{doc.word_count > 0 && (
|
||||
<div className="text-xs text-gray-400">{doc.word_count} Wörter analysiert</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
|
||||
import { AgentPflichtTable } from '../AgentPflichtTable'
|
||||
import type { McCoverage } from '../_agentTypes'
|
||||
|
||||
const COV: McCoverage[] = [
|
||||
{ mc_id: 'IMP-MC-002', status: 'ok', label: 'Email-Adresse',
|
||||
found: 'kundenbetreuung@bmw.de' },
|
||||
{ mc_id: 'IMP-MC-010', status: 'possibly_applicable',
|
||||
label: 'Verbraucher-Streitbeilegung-Hinweis' },
|
||||
{ mc_id: 'IMP-MC-009', status: 'na', label: 'Verantwortlicher § 18 MStV' },
|
||||
]
|
||||
|
||||
describe('AgentPflichtTable', () => {
|
||||
it('zeigt Label + gefundenen Wert, aber KEINE mc_id', () => {
|
||||
render(<AgentPflichtTable coverage={COV} />)
|
||||
expect(screen.getByText('Email-Adresse')).toBeInTheDocument()
|
||||
expect(screen.getByText('kundenbetreuung@bmw.de')).toBeInTheDocument()
|
||||
// Reverse-Engineering-Schutz: mc_id darf NICHT erscheinen.
|
||||
expect(screen.queryByText(/IMP-MC-/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('Verdikt-Header zählt die Status', () => {
|
||||
render(<AgentPflichtTable coverage={COV} />)
|
||||
expect(screen.getByText(/1 vorhanden/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/1 zu prüfen/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -69,27 +69,32 @@ describe('AgentResultTab', () => {
|
||||
})
|
||||
|
||||
const DOC_RESULT = {
|
||||
label: 'Impressum-Rohdaten', url: 'https://example.com/impressum',
|
||||
doc_type: 'impressum', word_count: 50, completeness_pct: 80,
|
||||
correctness_pct: 90, checks: [], findings_count: 2, error: '',
|
||||
label: 'Impressum', url: 'https://example.com/impressum',
|
||||
doc_type: 'impressum', word_count: 50, completeness_pct: 100,
|
||||
correctness_pct: 100, findings_count: 0, error: '', scenario: 'import',
|
||||
checks: [
|
||||
{ id: 'name', label: 'Name des Anbieters', passed: true, severity: 'HIGH',
|
||||
matched_text: 'Bayerische Motoren Werke Aktiengesellschaft', level: 1 },
|
||||
{ id: 'email', label: 'E-Mail-Adresse', passed: true, severity: 'HIGH',
|
||||
matched_text: 'kundenbetreuung@bmw.de', level: 1 },
|
||||
],
|
||||
}
|
||||
|
||||
describe('ComplianceResultTabs', () => {
|
||||
it('zeigt den Impressum-Tab zuerst und wechselt auf die Roh-Checkliste', () => {
|
||||
const result = {
|
||||
agent_outputs: { impressum: IMPRESSUM_OUTPUT },
|
||||
results: [DOC_RESULT],
|
||||
}
|
||||
it('rendert das Dokument-Tab der Haupt-Engine mit extrahierten Texten', () => {
|
||||
// Themen-Tabs kommen aus result.results (Haupt-Engine), NICHT agent_outputs.
|
||||
const result = { results: [DOC_RESULT] }
|
||||
render(<ComplianceResultTabs results={result} />)
|
||||
// beide Tabs vorhanden
|
||||
// Dokument-Tab + Übersicht
|
||||
expect(screen.getByRole('button', { name: /Impressum/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /Alle Checks \(roh\)/ })).toBeInTheDocument()
|
||||
// Impressum-Tab aktiv → Finding sichtbar
|
||||
expect(screen.getByText('Pflichtangabe fehlt: Email-Adresse')).toBeInTheDocument()
|
||||
// Wechsel auf die Roh-Checkliste
|
||||
fireEvent.click(screen.getByRole('button', { name: /Alle Checks \(roh\)/ }))
|
||||
// Impressum-Finding ist weg (umgeschaltet), Komponente intakt
|
||||
expect(screen.queryByText('Pflichtangabe fehlt: Email-Adresse')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /Alle Checks \(roh\)/ })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /Alle Checks/ })).toBeInTheDocument()
|
||||
// DocResultView: menschliches Label + gefundener Text sichtbar
|
||||
expect(screen.getByText('Name des Anbieters')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Bayerische Motoren Werke/)).toBeInTheDocument()
|
||||
// Wechsel auf die Übersicht
|
||||
fireEvent.click(screen.getByRole('button', { name: /Alle Checks/ }))
|
||||
expect(
|
||||
screen.getByText(/Dokumenten-Pruefung/),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -63,8 +63,10 @@ export interface Recommendation {
|
||||
export interface McCoverage {
|
||||
mc_id: string
|
||||
status: 'ok' | 'na' | 'high' | 'medium' | 'low' | 'skipped' |
|
||||
'insufficient_evidence'
|
||||
'insufficient_evidence' | 'possibly_applicable'
|
||||
reason?: string
|
||||
label?: string // menschlicher Feldname (KEINE mc_id im Frontend zeigen)
|
||||
found?: string // gefundener Text/Wert bei status=ok
|
||||
}
|
||||
|
||||
export interface EscalationLog {
|
||||
|
||||
Reference in New Issue
Block a user