feat(agent): strukturierte Ergebnis-Tabs — Impressum (Phase 1)

Der Compliance-Check legt zusätzlich einen strukturierten v3-AgentOutput
pro Thema in result.agent_outputs ab (additiv; B18-HTML + Firehose-Mail
bleiben unangetastet). Frontend: standardisiertes Ergebnis-Tab statt
Firehose — Impressum-Tab (AgentResultTab) + "Alle Checks (roh)" (ChecklistView).

- backend: _agent_outputs.py ruft den registrierten v3-ImpressumAgent,
  gewired in _orchestrator nach B18, surfaced via _phase_f_persist.
- frontend: AgentResultView (aus AgentSlotCard extrahiert, DRY),
  AgentResultTab, ComplianceResultTabs; ComplianceCheckTab 490->391 Zeilen.
- Tests: backend 2 passed, frontend 2 passed; tsc 0 neue Fehler; check-loc 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-10 18:32:06 +02:00
parent 3aa49f9553
commit e21984e0ad
11 changed files with 622 additions and 192 deletions
@@ -1,26 +1,16 @@
'use client'
/**
* SlotCard — ein Slot im Agent-Test mit Sections:
* 1. Header (Slot-Name, duration, Vault-Link)
* 2. Was wurde geprüft (MC-Coverage, collapsible)
* 3. Speedometer
* 4. Eskalationslog (wenn vorhanden)
* 5. Findings (sortiert HIGH → LOW)
* 6. Recommendations (gerollupt)
* AgentSlotCard — ein Slot im Agent-Test: Slot-Header (Name, Dauer,
* Konfidenz, Status-Badges, Artefakt-Link) + der geteilte
* AgentResultView (Coverage/Speedometer/Findings/Maßnahmen).
*/
import React, { useState } from 'react'
import React from 'react'
import type { SlotOutput, Severity } from './_agentTypes'
import { AgentFindingCard } from './AgentFindingCard'
import { AgentMcCoverage } from './AgentMcCoverage'
import { AgentRecommendationCard } from './AgentRecommendationCard'
import { AgentSpeedometer } from './AgentSpeedometer'
const SEV_ORDER: Record<Severity, number> = {
HIGH: 0, MEDIUM: 1, LOW: 2, INFO: 3,
}
import type { SlotOutput } from './_agentTypes'
import { isOutputSkipped } from './_agentTypes'
import { AgentResultView } from './AgentResultView'
export function AgentSlotCard({
slot, output, runId,
@@ -29,15 +19,8 @@ export function AgentSlotCard({
output: SlotOutput
runId: string
}) {
const [showAll, setShowAll] = useState(false)
const wasSkipped = output.mc_total > 0 &&
output.mc_ok === 0 && output.mc_na === 0 &&
output.mc_high === 0 && output.mc_medium === 0 && output.mc_low === 0
const wasSkipped = isOutputSkipped(output)
const allGreen = !wasSkipped && output.findings.length === 0
const sortedFindings = [...output.findings].sort(
(a, b) => SEV_ORDER[a.severity] - SEV_ORDER[b.severity],
)
const visible = showAll ? sortedFindings : sortedFindings.slice(0, 12)
return (
<div className="rounded-lg border bg-white p-4 space-y-3 shadow-sm">
<div className="flex items-baseline gap-3 flex-wrap">
@@ -65,72 +48,7 @@ export function AgentSlotCard({
</a>
</div>
{output.notes && (
<div className="text-xs text-amber-700 bg-amber-50 px-2 py-1 rounded">
Hinweis: {output.notes}
</div>
)}
<AgentMcCoverage coverage={output.mc_coverage} />
<AgentSpeedometer
total={output.mc_total}
ok={output.mc_ok}
na={output.mc_na}
high={output.mc_high}
medium={output.mc_medium}
low={output.mc_low}
/>
{output.escalation_log.length > 0 && (
<div className="text-xs text-gray-600 border-l-2 border-violet-400 pl-2 space-y-0.5">
<div className="font-semibold text-violet-700">
LLM-Eskalation eingesetzt:
</div>
{output.escalation_log.map((e, i) => (
<div key={i}>
{e.stage} <code className="text-violet-700">{e.model}</code>{' '}
· {e.duration_ms} ms{' '}
{e.tokens_in ? `· ${e.tokens_in}${e.tokens_out} tok` : ''}{' '}
{e.success ? '✓' : `${e.error || ''}`}
</div>
))}
</div>
)}
{sortedFindings.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-semibold uppercase text-gray-700">
Findings ({sortedFindings.length}) nach Schwere sortiert
</div>
<div className="space-y-2">
{visible.map(f => (
<AgentFindingCard key={f.check_id} f={f} />
))}
</div>
{sortedFindings.length > 12 && (
<button
onClick={() => setShowAll(x => !x)}
className="text-xs text-blue-600 hover:underline"
>
{showAll ? 'Weniger anzeigen' : `Alle ${sortedFindings.length} anzeigen`}
</button>
)}
</div>
)}
{output.recommendations.length > 0 && (
<div className="space-y-2">
<div className="text-xs font-semibold uppercase text-gray-700">
Maßnahmen-Plan ({output.recommendations.length} konsolidiert)
</div>
<div className="space-y-2">
{output.recommendations.map(r => (
<AgentRecommendationCard key={r.recommendation_id} r={r} />
))}
</div>
</div>
)}
<AgentResultView output={output} />
</div>
)
}