3b7ab4cbd7
Matches below 50% are now split: - GT entries → "Fehlend" tab (not matched by engine) - Engine entries → "Engine Findings" tab (additional findings) Only matches >= 50% shown in "Zugeordnet" tab. Coverage score now counts only real matches (>= 50%). "Extra" tab renamed to "Engine Findings" for clarity. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState } from 'react'
|
|
import { useParams } from 'next/navigation'
|
|
import { useBenchmark } from './_hooks/useBenchmark'
|
|
import { GTImportForm } from './_components/GTImportForm'
|
|
import { HazardComparisonTable } from './_components/HazardComparisonTable'
|
|
import { CategoryBreakdown } from './_components/CategoryBreakdown'
|
|
|
|
export default function BenchmarkPage() {
|
|
const { projectId } = useParams<{ projectId: string }>()
|
|
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
|
|
const [gtProjectId, setGtProjectId] = useState('')
|
|
|
|
// Only count matches >= 50% as real coverage
|
|
const realMatchCount = result ? (result.matched_pairs?.filter(m => m.match_score >= 0.5).length || 0) : 0
|
|
const coveragePct = result ? Math.round(realMatchCount * 100 / Math.max(result.total_gt, 1)) : 0
|
|
const measurePct = result ? Math.round(result.measure_coverage * 100) : 0
|
|
|
|
return (
|
|
<div className="space-y-6 max-w-[1200px]">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Ground Truth Benchmark</h1>
|
|
<p className="text-sm text-gray-500 mt-1">
|
|
Vergleich der Engine-Ergebnisse mit einer professionellen Risikobeurteilung
|
|
</p>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{/* GT Import or Cross-Project Reference */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
<GTImportForm onImport={importGT} loading={loading} />
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Benchmark ausfuehren</h3>
|
|
<p className="text-xs text-gray-500 mb-3">
|
|
GT aus diesem Projekt verwenden, oder eine Projekt-ID mit importierter GT angeben.
|
|
</p>
|
|
|
|
<div className="space-y-2">
|
|
<input
|
|
type="text"
|
|
value={gtProjectId}
|
|
onChange={(e) => setGtProjectId(e.target.value)}
|
|
placeholder="GT-Projekt-ID (optional — leer = dieses Projekt)"
|
|
className="w-full text-xs border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-gray-50 dark:bg-gray-900"
|
|
/>
|
|
<button
|
|
onClick={() => runBenchmark(gtProjectId || undefined)}
|
|
disabled={loading}
|
|
className="w-full px-4 py-2 text-sm font-medium bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 text-white rounded-md transition-colors"
|
|
>
|
|
{loading ? 'Vergleiche...' : 'Benchmark starten'}
|
|
</button>
|
|
</div>
|
|
|
|
{gtLoaded && !result && (
|
|
<div className="mt-3 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-blue-600">
|
|
{gtEntryCount} GT-Eintraege geladen. Klicke "Benchmark starten".
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Results */}
|
|
{result && (
|
|
<>
|
|
{/* Score Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<ScoreCard
|
|
label="Hazard Coverage"
|
|
value={`${coveragePct}%`}
|
|
sub={`${realMatchCount} / ${result.total_gt} erkannt (>= 50% Match)`}
|
|
color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
|
|
/>
|
|
<ScoreCard
|
|
label="Massnahmen-Coverage"
|
|
value={`${measurePct}%`}
|
|
sub="der zugeordneten Gefaehrdungen"
|
|
color={measurePct >= 80 ? 'green' : measurePct >= 50 ? 'yellow' : 'red'}
|
|
/>
|
|
<ScoreCard
|
|
label="GT Eintraege"
|
|
value={String(result.total_gt)}
|
|
sub="professionelle Beurteilung"
|
|
color="gray"
|
|
/>
|
|
<ScoreCard
|
|
label="Engine Eintraege"
|
|
value={String(result.total_engine)}
|
|
sub={`${result.extra_in_engine?.length || 0} zusaetzlich`}
|
|
color="gray"
|
|
/>
|
|
</div>
|
|
|
|
{/* Category Breakdown */}
|
|
<CategoryBreakdown breakdown={result.category_breakdown || []} />
|
|
|
|
{/* Hazard Comparison Table */}
|
|
<HazardComparisonTable
|
|
matched={result.matched_pairs || []}
|
|
missing={result.missing_from_engine || []}
|
|
extra={result.extra_in_engine || []}
|
|
/>
|
|
|
|
{/* Business Impact */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Business Impact</h3>
|
|
<div className="grid grid-cols-3 gap-4 text-center">
|
|
<div>
|
|
<div className="text-2xl font-bold text-gray-900 dark:text-white">2,5 Tage</div>
|
|
<div className="text-xs text-gray-500">Manueller Aufwand</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold text-purple-600">
|
|
{(coveragePct / 100 * 2.5).toFixed(1)} Tage
|
|
</div>
|
|
<div className="text-xs text-gray-500">Eingespart bei {coveragePct}% Coverage</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-2xl font-bold text-green-600">
|
|
{Math.round(coveragePct / 100 * 2.5 * 8 * 100)} EUR
|
|
</div>
|
|
<div className="text-xs text-gray-500">Einsparung (100 EUR/h)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ScoreCard({ label, value, sub, color }: {
|
|
label: string; value: string; sub: string
|
|
color: 'green' | 'yellow' | 'red' | 'gray'
|
|
}) {
|
|
const colors = {
|
|
green: 'border-green-200 dark:border-green-800',
|
|
yellow: 'border-yellow-200 dark:border-yellow-800',
|
|
red: 'border-red-200 dark:border-red-800',
|
|
gray: 'border-gray-200 dark:border-gray-700',
|
|
}
|
|
const textColors = {
|
|
green: 'text-green-600', yellow: 'text-yellow-600',
|
|
red: 'text-red-600', gray: 'text-gray-900 dark:text-white',
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white dark:bg-gray-800 rounded-lg border-2 ${colors[color]} p-4 text-center`}>
|
|
<div className={`text-2xl font-bold ${textColors[color]}`}>{value}</div>
|
|
<div className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1">{label}</div>
|
|
<div className="text-[10px] text-gray-400 mt-0.5">{sub}</div>
|
|
</div>
|
|
)
|
|
}
|