'use client' /** * OCR Regression Dashboard * * Shows all ground-truth sessions, runs regression tests, * displays pass/fail results with diff details, and shows history. */ import { useState, useEffect, useCallback } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' const KLAUSUR_API = '/klausur-api' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface GTSession { session_id: string name: string filename: string document_category: string | null pipeline: string | null saved_at: string | null summary: { total_zones: number total_columns: number total_rows: number total_cells: number } } interface DiffSummary { structural_changes: number cells_missing: number cells_added: number text_changes: number col_type_changes: number } interface RegressionResult { session_id: string name: string status: 'pass' | 'fail' | 'error' error?: string diff_summary?: DiffSummary reference_summary?: Record current_summary?: Record structural_diffs?: Array<{ field: string; reference: number; current: number }> cell_diffs?: Array<{ type: string; cell_id: string; reference?: string; current?: string }> } interface RegressionRun { id: string run_at: string status: string total: number passed: number failed: number errors: number duration_ms: number triggered_by: string } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function StatusBadge({ status }: { status: string }) { const cls = status === 'pass' ? 'bg-emerald-100 text-emerald-800 border-emerald-200' : status === 'fail' ? 'bg-red-100 text-red-800 border-red-200' : 'bg-amber-100 text-amber-800 border-amber-200' return ( {status === 'pass' ? 'Pass' : status === 'fail' ? 'Fail' : 'Error'} ) } function formatDate(iso: string | null) { if (!iso) return '—' return new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export default function OCRRegressionPage() { const [sessions, setSessions] = useState([]) const [results, setResults] = useState([]) const [history, setHistory] = useState([]) const [running, setRunning] = useState(false) const [overallStatus, setOverallStatus] = useState(null) const [durationMs, setDurationMs] = useState(null) const [expandedSession, setExpandedSession] = useState(null) const [tab, setTab] = useState<'current' | 'history'>('current') // Load ground-truth sessions const loadSessions = useCallback(async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/ground-truth-sessions`) if (res.ok) { const data = await res.json() setSessions(data.sessions || []) } } catch (e) { console.error('Failed to load GT sessions:', e) } }, []) // Load history const loadHistory = useCallback(async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/regression/history?limit=20`) if (res.ok) { const data = await res.json() setHistory(data.runs || []) } } catch (e) { console.error('Failed to load history:', e) } }, []) useEffect(() => { loadSessions() loadHistory() }, [loadSessions, loadHistory]) // Run all regressions const runAll = async () => { setRunning(true) setResults([]) setOverallStatus(null) setDurationMs(null) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/regression/run?triggered_by=manual`, { method: 'POST', }) if (res.ok) { const data = await res.json() setResults(data.results || []) setOverallStatus(data.status) setDurationMs(data.duration_ms) loadHistory() } } catch (e) { console.error('Regression run failed:', e) setOverallStatus('error') } finally { setRunning(false) } } const totalPass = results.filter(r => r.status === 'pass').length const totalFail = results.filter(r => r.status === 'fail').length const totalError = results.filter(r => r.status === 'error').length return (
{/* Header + Run Button */}

OCR Regression Tests

{sessions.length} Ground-Truth Session{sessions.length !== 1 ? 's' : ''}

{/* Overall Result Banner */} {overallStatus && (
{totalPass} bestanden, {totalFail} fehlgeschlagen, {totalError} Fehler
{durationMs !== null && ( {(durationMs / 1000).toFixed(1)}s )}
)} {/* Tabs */}
{/* Current Results Tab */} {tab === 'current' && (
{results.length === 0 && !running && (

Keine Ergebnisse

Klicken Sie "Alle Tests starten" um die Regression zu laufen.

)} {results.map(r => (
setExpandedSession(expandedSession === r.session_id ? null : r.session_id)} >
{r.name || r.session_id}
{r.diff_summary && ( {r.diff_summary.text_changes} Text, {r.diff_summary.structural_changes} Struktur )} {r.error && {r.error}}
{/* Expanded Details */} {expandedSession === r.session_id && r.status === 'fail' && (
{/* Structural Diffs */} {r.structural_diffs && r.structural_diffs.length > 0 && (

Strukturelle Aenderungen

{r.structural_diffs.map((d, i) => (
{d.field}: {d.reference} → {d.current}
))}
)} {/* Cell Diffs */} {r.cell_diffs && r.cell_diffs.length > 0 && (

Zellen-Aenderungen ({r.cell_diffs.length})

{r.cell_diffs.slice(0, 50).map((d, i) => (
{d.type} {' '} {d.cell_id} {d.reference && ( <> {' '}{d.reference} )} {d.current && ( <> {' '}{d.current} )}
))} {r.cell_diffs.length > 50 && (

... und {r.cell_diffs.length - 50} weitere

)}
)}
)}
))} {/* Ground Truth Sessions Overview (when no results yet) */} {results.length === 0 && sessions.length > 0 && (

Ground-Truth Sessions

{sessions.map(s => (
{s.name || s.session_id} {s.filename}
{s.summary.total_cells} Zellen, {s.summary.total_zones} Zonen {s.pipeline && {s.pipeline}}
))}
)}
)} {/* History Tab */} {tab === 'history' && (
{history.length === 0 ? (

Noch keine Laeufe aufgezeichnet.

) : ( {history.map(run => ( ))}
Datum Status Gesamt Pass Fail Dauer Trigger
{formatDate(run.run_at)} {run.total} {run.passed} {run.failed + run.errors} {(run.duration_ms / 1000).toFixed(1)}s {run.triggered_by}
)}
)}
) }