feat(iace): benchmark distance panel (Thema 1)

Surface result.distances in the benchmark module: a DistanceComparison
panel showing agreement %, covered values (green), GT-only gaps (amber)
and engine-only extras — mirroring the RiskComparison panel.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-11 16:32:52 +02:00
parent dbb15dbb78
commit b4981ea9ab
3 changed files with 89 additions and 0 deletions
@@ -0,0 +1,69 @@
'use client'
import type { DistanceComparison as DistanceComparisonData, DistanceToken } from '../_hooks/useBenchmark'
function fmt(t: DistanceToken): string {
const v = Number.isInteger(t.value) ? t.value.toString() : t.value.toString().replace('.', ',')
return `${v} ${t.unit}`
}
function TokenList({ tokens, tone }: { tokens: DistanceToken[]; tone: 'ok' | 'gap' | 'extra' }) {
if (!tokens.length) return <span className="text-xs text-gray-400"></span>
const cls =
tone === 'ok'
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
: tone === 'gap'
? 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300'
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
return (
<div className="flex flex-wrap gap-1">
{tokens.map((t, i) => (
<span key={i} className={`inline-block rounded px-1.5 py-0.5 text-[11px] tabular-nums ${cls}`}>
{fmt(t)}
</span>
))}
</div>
)
}
/**
* Distance/speed dimension comparison: do the engine's mm/mm-s values match the
* professional's (GT)? Confidence-aware tonality — green = covered, amber = gap.
*/
export function DistanceComparison({ data }: { data?: DistanceComparisonData }) {
if (!data || data.gt_count === 0) return null
return (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 space-y-3">
<div>
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Abstands-/Geschwindigkeits-Vergleich (Fachmann vs. Tool)</h3>
<p className="text-xs text-gray-500 mt-0.5">
Deckt das Tool die konkreten mm-/mm-s-Werte des Fachmanns ab? Deterministischer Abgleich der
Maße aus den GT-Maßnahmen gegen die vom Tool vorgeschlagenen Maßnahmen.
</p>
</div>
<div className="flex items-baseline gap-3">
<span className="text-2xl font-bold text-purple-600 tabular-nums">{Math.round(data.agreement_pct)}%</span>
<span className="text-xs text-gray-500">
{data.matched_count} von {data.gt_count} Fachmann-Maßen abgedeckt
</span>
</div>
<dl className="grid grid-cols-1 sm:grid-cols-3 gap-3 text-xs">
<div>
<dt className="text-[10px] uppercase tracking-wide text-gray-400 mb-1">Abgedeckt</dt>
<dd><TokenList tokens={data.matched} tone="ok" /></dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-gray-400 mb-1">Lücken (nur Fachmann)</dt>
<dd><TokenList tokens={data.gt_only} tone="gap" /></dd>
</div>
<div>
<dt className="text-[10px] uppercase tracking-wide text-gray-400 mb-1">Nur Tool</dt>
<dd><TokenList tokens={data.engine_only} tone="extra" /></dd>
</div>
</dl>
</div>
)
}
@@ -66,6 +66,21 @@ export interface RiskAgreement {
high_confidence_pct: number high_confidence_pct: number
} }
export interface DistanceToken {
value: number
unit: string // "mm" | "mm/s"
raw: string
}
export interface DistanceComparison {
gt_count: number
matched_count: number
agreement_pct: number
matched: DistanceToken[]
gt_only: DistanceToken[]
engine_only: DistanceToken[]
}
export interface BenchmarkResult { export interface BenchmarkResult {
coverage_score: number coverage_score: number
measure_coverage: number measure_coverage: number
@@ -78,6 +93,7 @@ export interface BenchmarkResult {
risk_rank_pairs: { gt_rank: number; engine_rank: number; hazard_name: string; gt_risk_score: number }[] risk_rank_pairs: { gt_rank: number; engine_rank: number; hazard_name: string; gt_risk_score: number }[]
risk_comparison?: RiskComparisonPair[] risk_comparison?: RiskComparisonPair[]
risk_agreement?: RiskAgreement risk_agreement?: RiskAgreement
distances?: DistanceComparison
} }
interface UseBenchmarkReturn { interface UseBenchmarkReturn {
@@ -7,6 +7,7 @@ import { GTImportForm } from './_components/GTImportForm'
import { HazardComparisonTable } from './_components/HazardComparisonTable' import { HazardComparisonTable } from './_components/HazardComparisonTable'
import { CategoryBreakdown } from './_components/CategoryBreakdown' import { CategoryBreakdown } from './_components/CategoryBreakdown'
import { RiskComparison } from './_components/RiskComparison' import { RiskComparison } from './_components/RiskComparison'
import { DistanceComparison } from './_components/DistanceComparison'
export default function BenchmarkPage() { export default function BenchmarkPage() {
const { projectId } = useParams<{ projectId: string }>() const { projectId } = useParams<{ projectId: string }>()
@@ -106,6 +107,9 @@ export default function BenchmarkPage() {
{/* Risk-number comparison (tool vs professional) with traffic lights */} {/* Risk-number comparison (tool vs professional) with traffic lights */}
<RiskComparison pairs={result.risk_comparison} agreement={result.risk_agreement} /> <RiskComparison pairs={result.risk_comparison} agreement={result.risk_agreement} />
{/* Distance/speed dimension comparison */}
<DistanceComparison data={result.distances} />
{/* Hazard Comparison Table */} {/* Hazard Comparison Table */}
<HazardComparisonTable <HazardComparisonTable
matched={result.matched_pairs || []} matched={result.matched_pairs || []}