feat(agents): Frontend Methodik-First Layout
CI / detect-changes (push) Successful in 7s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m24s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 7s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Successful in 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m24s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
User-Vorgabe: pro Slot transparent zeigen WAS wir tun:
1. Was wurde geprueft (MC-Coverage, collapsible)
2. Speedometer mit Severity-Verteilung
3. LLM-Eskalation-Log (wenn benutzt)
4. Findings sortiert HIGH->LOW, je Card:
- Methodik-Badge (MC / Regex / KB / LLM / Cross)
- Gesetzliche Basis (Norm-Block, violett)
- Befund (Zitat-Block, amber)
- Empfehlung -> 'Pflicht-Massnahme' bei HIGH,
'Best-Practice' bei MEDIUM/LOW, 'LLM-Vorschlag'
bei LLM-Quelle
5. Maszahmen-Plan (gerollupte Recommendations mit
related_finding_ids + Aufwand)
Refactor: ein File AgentTestTab.tsx (519 LOC) -> 7 Files:
_agentTypes.ts (Types + Methodik-Konstanten)
AgentSpeedometer.tsx
AgentMcCoverage.tsx
AgentFindingCard.tsx
AgentRecommendationCard.tsx
AgentSlotCard.tsx
AgentTestTab.tsx (Top-Level, schlank)
Plus Methodik-Info-Erklaerung am Tab-Anfang + Disclaimer.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
|||||||
|
'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,
|
||||||
|
} 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 || []
|
||||||
|
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>
|
||||||
|
<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} />
|
||||||
|
))}
|
||||||
|
{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)'
|
||||||
|
: 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
'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',
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_LABEL: Record<string, string> = {
|
||||||
|
ok: 'OK',
|
||||||
|
na: 'n/a',
|
||||||
|
skipped: 'übersprungen',
|
||||||
|
high: 'HIGH',
|
||||||
|
medium: 'MEDIUM',
|
||||||
|
low: 'LOW',
|
||||||
|
}
|
||||||
|
|
||||||
|
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,51 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommendation-Card: zeigt die gerollupten Maßnahmen.
|
||||||
|
* Eine Recommendation bündelt 1..N Findings mit gleicher Maßnahme.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import type { Recommendation } from './_agentTypes'
|
||||||
|
import { SEVERITY_COLOR } from './_agentTypes'
|
||||||
|
|
||||||
|
export function AgentRecommendationCard({ r }: { r: Recommendation }) {
|
||||||
|
const color = SEVERITY_COLOR[r.severity]
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="rounded p-3 space-y-1 text-sm bg-emerald-50"
|
||||||
|
style={{ borderLeft: `3px solid ${color}` }}
|
||||||
|
>
|
||||||
|
<div className="flex items-baseline gap-2 flex-wrap">
|
||||||
|
<span
|
||||||
|
className="text-[10px] font-bold px-1.5 py-0.5 rounded text-white"
|
||||||
|
style={{ background: color }}
|
||||||
|
>
|
||||||
|
{r.severity}
|
||||||
|
</span>
|
||||||
|
<span className="font-semibold text-gray-900">{r.title}</span>
|
||||||
|
<span className="text-[10px] text-gray-500 ml-auto">
|
||||||
|
{r.related_finding_ids.length} Finding(s)
|
||||||
|
{' · '}
|
||||||
|
{r.estimated_effort_hours.toFixed(1)}h geschätzt
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{r.body && r.body !== r.title && (
|
||||||
|
<div className="text-xs text-gray-700 whitespace-pre-wrap">
|
||||||
|
{r.body}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{r.related_finding_ids.length > 0 && (
|
||||||
|
<details className="text-[10px] text-gray-500">
|
||||||
|
<summary className="cursor-pointer">Aus diesen Findings abgeleitet</summary>
|
||||||
|
<ul className="mt-1 list-disc ml-4 space-y-0.5">
|
||||||
|
{r.related_finding_ids.map(id => (
|
||||||
|
<li key={id}><code>{id}</code></li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
'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)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState } 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSlotCard({
|
||||||
|
slot, output, runId,
|
||||||
|
}: {
|
||||||
|
slot: string
|
||||||
|
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 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">
|
||||||
|
<h3 className="font-semibold text-gray-900">Slot: {slot}</h3>
|
||||||
|
<span className="text-xs text-gray-500">
|
||||||
|
{output.duration_ms} ms · Konfidenz {(output.confidence * 100).toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
{wasSkipped && (
|
||||||
|
<span className="text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded">
|
||||||
|
Dokument konnte nicht geladen werden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{allGreen && (
|
||||||
|
<span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded">
|
||||||
|
Alle anwendbaren MCs erfüllt
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
className="text-xs text-blue-600 hover:underline ml-auto"
|
||||||
|
href={`/api/sdk/v1/specialist-agent/run/${runId}/artifacts`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Artefakte ↗
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Speedometer + Color-Legende für eine MC-Auswertung.
|
||||||
|
* Zeigt 5 Klassen: OK / n/a / HIGH / MEDIUM / LOW als horizontaler Balken.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
total: number
|
||||||
|
ok: number
|
||||||
|
na: number
|
||||||
|
high: number
|
||||||
|
medium: number
|
||||||
|
low: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSpeedometer({ total, ok, na, high, medium, low }: Props) {
|
||||||
|
const safeTotal = Math.max(total, 1)
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{total} Machine-Checks (MCs) durchlaufen
|
||||||
|
</div>
|
||||||
|
<div className="flex h-4 rounded overflow-hidden border">
|
||||||
|
<Bar pct={(ok / safeTotal) * 100} color="#10b981" />
|
||||||
|
<Bar pct={(na / safeTotal) * 100} color="#94a3b8" />
|
||||||
|
<Bar pct={(high / safeTotal) * 100} color="#dc2626" />
|
||||||
|
<Bar pct={(medium / safeTotal) * 100} color="#f59e0b" />
|
||||||
|
<Bar pct={(low / safeTotal) * 100} color="#3b82f6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-3 text-xs">
|
||||||
|
<Legend color="#10b981" label={`OK ${ok}`} title="Geprüft & erfüllt" />
|
||||||
|
<Legend color="#94a3b8" label={`n/a ${na}`} title="Nicht anwendbar (Branche, B2C, …)" />
|
||||||
|
<Legend color="#dc2626" label={`HIGH ${high}`} title="Pflichtangabe fehlt / hartes Risiko" />
|
||||||
|
<Legend color="#f59e0b" label={`MEDIUM ${medium}`} title="Ergänzung empfohlen" />
|
||||||
|
<Legend color="#3b82f6" label={`LOW ${low}`} title="Best-Practice-Hinweis" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Bar({ pct, color }: { pct: number; color: string }) {
|
||||||
|
return <div style={{ width: `${pct}%`, background: color }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Legend({
|
||||||
|
color, label, title,
|
||||||
|
}: { color: string; label: string; title?: string }) {
|
||||||
|
return (
|
||||||
|
<span className="inline-flex items-center gap-1" title={title}>
|
||||||
|
<span style={{ background: color }} className="w-2 h-2 inline-block rounded" />
|
||||||
|
<span>{label}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,66 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AgentTestTab — Top-Level für den 5-URL-Test eines Specialist-Agents.
|
||||||
|
* Sections:
|
||||||
|
* 1. Agent-Wähler + 5 URL-Slots + Start-Button
|
||||||
|
* 2. Methodik-Erklärung (was wir tun, warum)
|
||||||
|
* 3. Live-Event-Log
|
||||||
|
* 4. Pro Slot: SlotCard (siehe AgentSlotCard.tsx)
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
type AgentInfo = {
|
import type { AgentInfo, RunResult, SlotOutput, StreamEvent } from './_agentTypes'
|
||||||
agent_id: string
|
import { AgentSlotCard } from './AgentSlotCard'
|
||||||
agent_version: string
|
|
||||||
doc_type: string
|
|
||||||
mc_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type Finding = {
|
|
||||||
check_id: string
|
|
||||||
agent: string
|
|
||||||
agent_version: string
|
|
||||||
field_id?: string
|
|
||||||
severity: 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
|
|
||||||
title: string
|
|
||||||
norm?: string
|
|
||||||
evidence?: string
|
|
||||||
action?: string
|
|
||||||
confidence?: number
|
|
||||||
sources?: { source_type: string; source_id: string; detail?: string }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Recommendation = {
|
|
||||||
recommendation_id: string
|
|
||||||
title: string
|
|
||||||
body: string
|
|
||||||
severity: string
|
|
||||||
related_finding_ids: string[]
|
|
||||||
estimated_effort_hours: number
|
|
||||||
}
|
|
||||||
|
|
||||||
type SlotOutput = {
|
|
||||||
agent: string
|
|
||||||
agent_version: string
|
|
||||||
findings: Finding[]
|
|
||||||
recommendations: Recommendation[]
|
|
||||||
mc_total: number
|
|
||||||
mc_ok: number
|
|
||||||
mc_na: number
|
|
||||||
mc_high: number
|
|
||||||
mc_medium: number
|
|
||||||
mc_low: number
|
|
||||||
duration_ms: number
|
|
||||||
confidence: number
|
|
||||||
escalation_log: { stage: string; model: string; success: boolean; duration_ms: number }[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunResult = {
|
|
||||||
run_id: string
|
|
||||||
agent_id: string
|
|
||||||
finished: boolean
|
|
||||||
results: Record<string, SlotOutput>
|
|
||||||
vault_url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type StreamEvent = {
|
|
||||||
type: string
|
|
||||||
slot?: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'agent-test-state-v1'
|
const STORAGE_KEY = 'agent-test-state-v1'
|
||||||
const MAX_SLOTS = 5
|
const MAX_SLOTS = 5
|
||||||
@@ -83,9 +35,11 @@ export function AgentTestTab() {
|
|||||||
if (s) {
|
if (s) {
|
||||||
const parsed = JSON.parse(s)
|
const parsed = JSON.parse(s)
|
||||||
if (parsed.agentId) setAgentId(parsed.agentId)
|
if (parsed.agentId) setAgentId(parsed.agentId)
|
||||||
if (Array.isArray(parsed.urls))
|
if (Array.isArray(parsed.urls)) {
|
||||||
setUrls(parsed.urls.slice(0, MAX_SLOTS).concat(
|
const padded = [...parsed.urls.slice(0, MAX_SLOTS),
|
||||||
new Array(MAX_SLOTS).fill('')).slice(0, MAX_SLOTS))
|
...new Array(MAX_SLOTS).fill('')].slice(0, MAX_SLOTS)
|
||||||
|
setUrls(padded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch { /* noop */ }
|
} catch { /* noop */ }
|
||||||
}, [])
|
}, [])
|
||||||
@@ -118,17 +72,11 @@ export function AgentTestTab() {
|
|||||||
if (cleanUrls.length === 0) { setError('Mind. eine URL angeben.'); return }
|
if (cleanUrls.length === 0) { setError('Mind. eine URL angeben.'); return }
|
||||||
setRunning(true)
|
setRunning(true)
|
||||||
try {
|
try {
|
||||||
const r = await fetch(
|
const r = await fetch('/api/sdk/v1/specialist-agent/test/start', {
|
||||||
'/api/sdk/v1/specialist-agent/test/start',
|
method: 'POST',
|
||||||
{
|
headers: { 'Content-Type': 'application/json' },
|
||||||
method: 'POST',
|
body: JSON.stringify({ agent_id: agentId, urls: cleanUrls }),
|
||||||
headers: { 'Content-Type': 'application/json' },
|
})
|
||||||
body: JSON.stringify({
|
|
||||||
agent_id: agentId,
|
|
||||||
urls: cleanUrls,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
const j = await r.json().catch(() => ({}))
|
const j = await r.json().catch(() => ({}))
|
||||||
throw new Error(j.error || `HTTP ${r.status}`)
|
throw new Error(j.error || `HTTP ${r.status}`)
|
||||||
@@ -192,88 +140,29 @@ export function AgentTestTab() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="rounded-lg border bg-white p-4 space-y-3">
|
<InputCard
|
||||||
<h2 className="text-lg font-semibold">Agent-Test (max. {MAX_SLOTS} URLs)</h2>
|
agents={agents}
|
||||||
<p className="text-xs text-gray-500">
|
agentId={agentId}
|
||||||
Wählt einen Spezialisten-Agent und feuert ihn gegen 1-5 URLs gleichzeitig.
|
setAgentId={setAgentId}
|
||||||
Pro URL Speedometer + Findings + Empfehlungen mit Quellen-Herkunft (MC / Regex / LLM-Stufe).
|
selectedAgent={selectedAgent}
|
||||||
Keine Aussagen "rechtssicher" oder "garantiert" — alle solchen Wörter werden vor Ausgabe gelöscht.
|
urls={urls}
|
||||||
</p>
|
setUrls={setUrls}
|
||||||
<div className="flex flex-wrap gap-3 items-end">
|
running={running}
|
||||||
<div>
|
runId={runId}
|
||||||
<label className="block text-xs font-medium text-gray-600">Agent</label>
|
startTest={startTest}
|
||||||
<select value={agentId}
|
error={error}
|
||||||
onChange={e => setAgentId(e.target.value)}
|
/>
|
||||||
className="border rounded px-2 py-1 text-sm">
|
|
||||||
{agents.map(a => (
|
|
||||||
<option key={a.agent_id} value={a.agent_id}>
|
|
||||||
{a.agent_id} v{a.agent_version} ({a.mc_count} MCs)
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{selectedAgent && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
Doc-Type: <code>{selectedAgent.doc_type}</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
{urls.map((u, i) => (
|
|
||||||
<div key={i} className="flex gap-2">
|
|
||||||
<span className="text-xs font-mono text-gray-500 w-8 pt-1.5">URL{i+1}</span>
|
|
||||||
<input value={u}
|
|
||||||
onChange={e => {
|
|
||||||
const next = [...urls]; next[i] = e.target.value
|
|
||||||
setUrls(next)
|
|
||||||
}}
|
|
||||||
placeholder="https://example.com/impressum"
|
|
||||||
className="flex-1 border rounded px-2 py-1 text-sm font-mono"/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button onClick={startTest}
|
|
||||||
disabled={running}
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white text-sm px-4 py-2 rounded">
|
|
||||||
{running ? 'Laufend...' : 'Test starten'}
|
|
||||||
</button>
|
|
||||||
{runId && (
|
|
||||||
<span className="text-xs text-gray-500 self-center">
|
|
||||||
Run-ID: <code>{runId}</code>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{error && (
|
|
||||||
<div className="bg-red-50 border-l-4 border-red-400 p-2 text-sm text-red-700">
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{running && events.length > 0 && (
|
<MethodikInfo />
|
||||||
<div className="rounded-lg border bg-gray-50 p-3 max-h-48 overflow-y-auto">
|
|
||||||
<div className="text-xs font-mono space-y-1">
|
{running && events.length > 0 && <EventLog events={events} />}
|
||||||
{events.slice(-30).map((ev, i) => (
|
|
||||||
<div key={i}>
|
|
||||||
<span className="text-gray-400">[{ev.type}]</span>{' '}
|
|
||||||
{ev.slot && <span className="text-blue-600">{ev.slot}</span>}{' '}
|
|
||||||
{ev.severity && (
|
|
||||||
<span className={severityColor(ev.severity)}>
|
|
||||||
{ev.severity}
|
|
||||||
</span>
|
|
||||||
)}{' '}
|
|
||||||
{ev.title || ev.error || ev.label || ev.model || ev.url || ''}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{slotOutputs.length > 0 && (
|
{slotOutputs.length > 0 && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{slotOutputs.map(({ slot, output }) => (
|
{slotOutputs.map(({ slot, output }) => (
|
||||||
<SlotCard key={slot} slot={slot} output={output} runId={runId}/>
|
<AgentSlotCard
|
||||||
|
key={slot} slot={slot} output={output} runId={runId}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -281,174 +170,147 @@ export function AgentTestTab() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SlotCard({ slot, output, runId }: {
|
function InputCard({
|
||||||
slot: string
|
agents, agentId, setAgentId, selectedAgent, urls, setUrls,
|
||||||
output: SlotOutput
|
running, runId, startTest, error,
|
||||||
|
}: {
|
||||||
|
agents: AgentInfo[]
|
||||||
|
agentId: string
|
||||||
|
setAgentId: (s: string) => void
|
||||||
|
selectedAgent?: AgentInfo
|
||||||
|
urls: string[]
|
||||||
|
setUrls: (urls: string[]) => void
|
||||||
|
running: boolean
|
||||||
runId: string
|
runId: string
|
||||||
|
startTest: () => void
|
||||||
|
error: string
|
||||||
}) {
|
}) {
|
||||||
const [showAll, setShowAll] = useState(false)
|
|
||||||
const visibleFindings = showAll ? output.findings : output.findings.slice(0, 8)
|
|
||||||
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 allGreen = !wasSkipped && output.findings.length === 0
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border bg-white p-4 space-y-3">
|
<div className="rounded-lg border bg-white p-4 space-y-3 shadow-sm">
|
||||||
<div className="flex items-baseline gap-3">
|
<h2 className="text-lg font-semibold">Agent-Test (max. {MAX_SLOTS} URLs)</h2>
|
||||||
<h3 className="font-semibold text-gray-800">Slot: {slot}</h3>
|
<div className="flex flex-wrap gap-3 items-end">
|
||||||
<span className="text-xs text-gray-500">
|
<div>
|
||||||
{output.duration_ms} ms · confidence {(output.confidence * 100).toFixed(0)}%
|
<label className="block text-xs font-medium text-gray-600">Agent</label>
|
||||||
</span>
|
<select
|
||||||
{wasSkipped && (
|
value={agentId}
|
||||||
<span className="text-xs bg-amber-100 text-amber-800 px-2 py-0.5 rounded">
|
onChange={e => setAgentId(e.target.value)}
|
||||||
Dokument konnte nicht geladen werden (leer/zu kurz)
|
className="border rounded px-2 py-1 text-sm"
|
||||||
</span>
|
>
|
||||||
|
{agents.map(a => (
|
||||||
|
<option key={a.agent_id} value={a.agent_id}>
|
||||||
|
{a.agent_id} v{a.agent_version} ({a.mc_count} MCs)
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{selectedAgent && (
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
Doc-Type: <code>{selectedAgent.doc_type}</code>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{allGreen && (
|
|
||||||
<span className="text-xs bg-emerald-100 text-emerald-800 px-2 py-0.5 rounded">
|
|
||||||
Keine Findings — alle anwendbaren MCs OK
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<a className="text-xs text-blue-600 hover:underline ml-auto"
|
|
||||||
href={`/api/sdk/v1/specialist-agent/run/${runId}/artifacts`}
|
|
||||||
target="_blank" rel="noreferrer">
|
|
||||||
Artefakte ↗
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<Speedometer
|
<div className="space-y-1">
|
||||||
total={output.mc_total}
|
{urls.map((u, i) => (
|
||||||
ok={output.mc_ok}
|
<div key={i} className="flex gap-2">
|
||||||
na={output.mc_na}
|
<span className="text-xs font-mono text-gray-500 w-8 pt-1.5">
|
||||||
high={output.mc_high}
|
URL{i + 1}
|
||||||
medium={output.mc_medium}
|
|
||||||
low={output.mc_low}
|
|
||||||
/>
|
|
||||||
{output.escalation_log.length > 0 && (
|
|
||||||
<div className="text-xs text-gray-500">
|
|
||||||
Eskalationen:{' '}
|
|
||||||
{output.escalation_log.map((e, i) => (
|
|
||||||
<span key={i} className="mr-2">
|
|
||||||
{e.stage}/{e.model} {e.success ? '✓' : '✗'} ({e.duration_ms} ms)
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
<input
|
||||||
</div>
|
value={u}
|
||||||
)}
|
onChange={e => {
|
||||||
{output.findings.length > 0 && (
|
const next = [...urls]; next[i] = e.target.value
|
||||||
<div className="space-y-1">
|
setUrls(next)
|
||||||
<div className="text-xs font-semibold uppercase text-gray-600">
|
}}
|
||||||
Findings ({output.findings.length})
|
placeholder="https://example.com/impressum"
|
||||||
|
className="flex-1 border rounded px-2 py-1 text-sm font-mono"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{visibleFindings.map(f => (
|
|
||||||
<FindingRow key={f.check_id} f={f}/>
|
|
||||||
))}
|
|
||||||
{output.findings.length > 8 && (
|
|
||||||
<button onClick={() => setShowAll(x => !x)}
|
|
||||||
className="text-xs text-blue-600 hover:underline">
|
|
||||||
{showAll ? 'Weniger anzeigen' : `Alle ${output.findings.length} anzeigen`}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{output.recommendations.length > 0 && (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-xs font-semibold uppercase text-gray-600">
|
|
||||||
Empfehlungen ({output.recommendations.length}, gerollupt)
|
|
||||||
</div>
|
|
||||||
{output.recommendations.map(r => (
|
|
||||||
<div key={r.recommendation_id}
|
|
||||||
className="border-l-2 border-emerald-400 bg-emerald-50 p-2 text-xs">
|
|
||||||
<div className="font-semibold">{r.title}</div>
|
|
||||||
<div className="text-gray-600">{r.body}</div>
|
|
||||||
<div className="text-[10px] text-gray-500 mt-1">
|
|
||||||
{r.related_finding_ids.length} Finding(s) · ~{r.estimated_effort_hours}h
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Speedometer({ total, ok, na, high, medium, low }: {
|
|
||||||
total: number
|
|
||||||
ok: number
|
|
||||||
na: number
|
|
||||||
high: number
|
|
||||||
medium: number
|
|
||||||
low: number
|
|
||||||
}) {
|
|
||||||
const safeTotal = Math.max(total, 1)
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<div className="text-xs text-gray-500">{total} MCs geprüft</div>
|
|
||||||
<div className="flex h-4 rounded overflow-hidden border">
|
|
||||||
<Bar pct={(ok / safeTotal) * 100} color="#10b981"/>
|
|
||||||
<Bar pct={(na / safeTotal) * 100} color="#94a3b8"/>
|
|
||||||
<Bar pct={(high / safeTotal) * 100} color="#dc2626"/>
|
|
||||||
<Bar pct={(medium / safeTotal) * 100} color="#f59e0b"/>
|
|
||||||
<Bar pct={(low / safeTotal) * 100} color="#3b82f6"/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2 text-xs">
|
|
||||||
<Legend color="#10b981" label={`OK ${ok}`}/>
|
|
||||||
<Legend color="#94a3b8" label={`n/a ${na}`}/>
|
|
||||||
<Legend color="#dc2626" label={`HIGH ${high}`}/>
|
|
||||||
<Legend color="#f59e0b" label={`MEDIUM ${medium}`}/>
|
|
||||||
<Legend color="#3b82f6" label={`LOW ${low}`}/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Bar({ pct, color }: { pct: number; color: string }) {
|
|
||||||
return <div style={{ width: `${pct}%`, background: color }}/>
|
|
||||||
}
|
|
||||||
|
|
||||||
function Legend({ color, label }: { color: string; label: string }) {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-1">
|
|
||||||
<span style={{ background: color }} className="w-2 h-2 inline-block rounded"/>
|
|
||||||
<span>{label}</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function FindingRow({ f }: { f: Finding }) {
|
|
||||||
const color = severityHex(f.severity)
|
|
||||||
const sourceTags = (f.sources || [])
|
|
||||||
.map(s => s.source_type)
|
|
||||||
.filter((v, i, arr) => arr.indexOf(v) === i)
|
|
||||||
return (
|
|
||||||
<div className="p-2 border-l-2" style={{ borderColor: color }}>
|
|
||||||
<div className="flex items-baseline gap-2 text-xs">
|
|
||||||
<span style={{ color }} className="font-semibold">{f.severity}</span>
|
|
||||||
<code className="text-gray-500">{f.check_id}</code>
|
|
||||||
{sourceTags.map(t => (
|
|
||||||
<span key={t} className="px-1 bg-gray-100 rounded text-[10px]">{t}</span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm">{f.title}</div>
|
<div className="flex gap-2">
|
||||||
{f.norm && <div className="text-[11px] text-gray-500">{f.norm}</div>}
|
<button
|
||||||
{f.evidence && (
|
onClick={startTest}
|
||||||
<div className="text-[11px] italic text-gray-600 mt-1">„{f.evidence}"</div>
|
disabled={running}
|
||||||
)}
|
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white text-sm px-4 py-2 rounded"
|
||||||
{f.action && (
|
>
|
||||||
<div className="text-[11px] text-emerald-700 mt-1">
|
{running ? 'Laufend...' : 'Test starten'}
|
||||||
→ {f.action}
|
</button>
|
||||||
|
{runId && (
|
||||||
|
<span className="text-xs text-gray-500 self-center">
|
||||||
|
Run-ID: <code>{runId}</code>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border-l-4 border-red-400 p-2 text-sm text-red-700">
|
||||||
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function MethodikInfo() {
|
||||||
|
return (
|
||||||
|
<details className="rounded border bg-slate-50 px-3 py-2 text-xs text-gray-700">
|
||||||
|
<summary className="cursor-pointer font-semibold">
|
||||||
|
Methodik — wie geprüft wird
|
||||||
|
</summary>
|
||||||
|
<ol className="list-decimal ml-5 mt-2 space-y-1">
|
||||||
|
<li>
|
||||||
|
<strong>Machine-Checks (MCs)</strong> — deterministische
|
||||||
|
Pattern-Tests gegen Gesetzestext (z.B. § 5 TMG). Schnell,
|
||||||
|
reproduzierbar.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Knowledge-Base</strong> — kuratierte Patterns aus
|
||||||
|
anonymisierten Mandanten-FAQs.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>LLM-Eskalation</strong> — nur bei unklaren MCs:
|
||||||
|
erst lokales qwen2.5:7b, bei Bedarf größeres OVH-Modell.
|
||||||
|
Claude (Cloud) erst nach Anonymisierung.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cross-Doc-Vergleich</strong> — Konsistenz zwischen
|
||||||
|
DSE, Cookie-Policy, Impressum (späterer Agent).
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p className="mt-2 italic text-gray-500">
|
||||||
|
Disclaimer: keine Aussagen wie "rechtssicher" oder "konform" —
|
||||||
|
nur Findings + Empfehlungen + Herleitung. Verbotene Begriffe
|
||||||
|
werden vom Linter aus Agent-Outputs entfernt.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function EventLog({ events }: { events: StreamEvent[] }) {
|
||||||
|
return (
|
||||||
|
<div className="rounded border bg-gray-50 p-3 max-h-48 overflow-y-auto">
|
||||||
|
<div className="text-xs font-mono space-y-0.5">
|
||||||
|
{events.slice(-30).map((ev, i) => (
|
||||||
|
<div key={i}>
|
||||||
|
<span className="text-gray-400">[{ev.type}]</span>{' '}
|
||||||
|
{ev.slot && <span className="text-blue-600">{ev.slot}</span>}{' '}
|
||||||
|
{ev.severity && (
|
||||||
|
<span className={severityColor(ev.severity)}>{ev.severity}</span>
|
||||||
|
)}{' '}
|
||||||
|
{ev.title || ev.error || ev.label || ev.model || ev.url || ''}
|
||||||
|
{ev.word_count !== undefined && (
|
||||||
|
<span className="text-gray-500">
|
||||||
|
{' '}({ev.word_count} Wörter)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function severityColor(sev: string) {
|
function severityColor(sev: string) {
|
||||||
return sev === 'HIGH' ? 'text-red-600 font-semibold' :
|
return sev === 'HIGH' ? 'text-red-600 font-semibold' :
|
||||||
sev === 'MEDIUM' ? 'text-amber-600 font-semibold' :
|
sev === 'MEDIUM' ? 'text-amber-600 font-semibold' :
|
||||||
sev === 'LOW' ? 'text-blue-600' : 'text-gray-600'
|
sev === 'LOW' ? 'text-blue-600' : 'text-gray-600'
|
||||||
}
|
}
|
||||||
|
|
||||||
function severityHex(sev: string) {
|
|
||||||
return sev === 'HIGH' ? '#dc2626' :
|
|
||||||
sev === 'MEDIUM' ? '#f59e0b' :
|
|
||||||
sev === 'LOW' ? '#3b82f6' : '#94a3b8'
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Shared types for the agent-test UI.
|
||||||
|
//
|
||||||
|
// SourceType-Mapping zur Methodik-Anzeige:
|
||||||
|
// mc / regex → "Machine-Check (deterministisch)"
|
||||||
|
// kb_faq → "Knowledge-Base (kuratiert)"
|
||||||
|
// llm_local → "Lokales LLM (qwen2.5:7b)"
|
||||||
|
// llm_local_big → "Externes LLM (OVH 120b)"
|
||||||
|
// llm_cloud → "Cloud-LLM (Claude, anonymisiert)"
|
||||||
|
// cross → "Cross-Doc-Vergleich"
|
||||||
|
|
||||||
|
export type Severity = 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO'
|
||||||
|
|
||||||
|
export type SourceType =
|
||||||
|
| 'mc'
|
||||||
|
| 'regex'
|
||||||
|
| 'kb_faq'
|
||||||
|
| 'llm_local'
|
||||||
|
| 'llm_local_big'
|
||||||
|
| 'llm_cloud'
|
||||||
|
| 'cross'
|
||||||
|
|
||||||
|
export interface EvidenceSource {
|
||||||
|
source_type: SourceType
|
||||||
|
source_id: string
|
||||||
|
detail?: string
|
||||||
|
confidence?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Finding {
|
||||||
|
check_id: string
|
||||||
|
agent: string
|
||||||
|
agent_version: string
|
||||||
|
field_id?: string
|
||||||
|
severity: Severity
|
||||||
|
severity_reason?: string
|
||||||
|
title: string
|
||||||
|
norm?: string
|
||||||
|
evidence?: string
|
||||||
|
action?: string
|
||||||
|
confidence?: number
|
||||||
|
sources?: EvidenceSource[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Recommendation {
|
||||||
|
recommendation_id: string
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
severity: Severity
|
||||||
|
related_finding_ids: string[]
|
||||||
|
estimated_effort_hours: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McCoverage {
|
||||||
|
mc_id: string
|
||||||
|
status: 'ok' | 'na' | 'high' | 'medium' | 'low' | 'skipped'
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EscalationLog {
|
||||||
|
stage: SourceType
|
||||||
|
model: string
|
||||||
|
duration_ms: number
|
||||||
|
tokens_in?: number
|
||||||
|
tokens_out?: number
|
||||||
|
success: boolean
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlotOutput {
|
||||||
|
agent: string
|
||||||
|
agent_version: string
|
||||||
|
findings: Finding[]
|
||||||
|
recommendations: Recommendation[]
|
||||||
|
mc_coverage: McCoverage[]
|
||||||
|
escalation_log: EscalationLog[]
|
||||||
|
mc_total: number
|
||||||
|
mc_ok: number
|
||||||
|
mc_na: number
|
||||||
|
mc_high: number
|
||||||
|
mc_medium: number
|
||||||
|
mc_low: number
|
||||||
|
duration_ms: number
|
||||||
|
confidence: number
|
||||||
|
notes?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AgentInfo {
|
||||||
|
agent_id: string
|
||||||
|
agent_version: string
|
||||||
|
doc_type: string
|
||||||
|
mc_count: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RunResult {
|
||||||
|
run_id: string
|
||||||
|
agent_id: string
|
||||||
|
finished: boolean
|
||||||
|
results: Record<string, SlotOutput>
|
||||||
|
vault_url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StreamEvent {
|
||||||
|
type: string
|
||||||
|
slot?: string
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Methodik-Labels für die Source-Type-Badge ───────────────────────
|
||||||
|
|
||||||
|
export const METHODIK_LABEL: Record<SourceType, string> = {
|
||||||
|
mc: 'Machine-Check (deterministisch)',
|
||||||
|
regex: 'Pattern-Match (deterministisch)',
|
||||||
|
kb_faq: 'Knowledge-Base (kuratiert)',
|
||||||
|
llm_local: 'Lokales LLM (qwen2.5:7b)',
|
||||||
|
llm_local_big: 'Externes LLM (OVH 120b)',
|
||||||
|
llm_cloud: 'Cloud-LLM (anonymisiert)',
|
||||||
|
cross: 'Cross-Doc-Vergleich',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const METHODIK_SHORT: Record<SourceType, string> = {
|
||||||
|
mc: 'MC',
|
||||||
|
regex: 'Regex',
|
||||||
|
kb_faq: 'KB',
|
||||||
|
llm_local: 'LLM',
|
||||||
|
llm_local_big: 'LLM⁺',
|
||||||
|
llm_cloud: 'Claude',
|
||||||
|
cross: 'Cross',
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background/foreground colors für die Methodik-Badge.
|
||||||
|
export const METHODIK_COLOR: Record<SourceType, { bg: string; fg: string }> = {
|
||||||
|
mc: { bg: '#e0e7ff', fg: '#3730a3' },
|
||||||
|
regex: { bg: '#e0e7ff', fg: '#3730a3' },
|
||||||
|
kb_faq: { bg: '#fef3c7', fg: '#92400e' },
|
||||||
|
llm_local: { bg: '#dcfce7', fg: '#166534' },
|
||||||
|
llm_local_big: { bg: '#bbf7d0', fg: '#14532d' },
|
||||||
|
llm_cloud: { bg: '#fce7f3', fg: '#9d174d' },
|
||||||
|
cross: { bg: '#fed7aa', fg: '#9a3412' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SEVERITY_COLOR: Record<Severity, string> = {
|
||||||
|
HIGH: '#dc2626',
|
||||||
|
MEDIUM: '#f59e0b',
|
||||||
|
LOW: '#3b82f6',
|
||||||
|
INFO: '#64748b',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SEVERITY_BG: Record<Severity, string> = {
|
||||||
|
HIGH: '#fef2f2',
|
||||||
|
MEDIUM: '#fffbeb',
|
||||||
|
LOW: '#eff6ff',
|
||||||
|
INFO: '#f8fafc',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user