'use client' import { useCallback, useEffect, useState } from 'react' import type { RowResult, RowGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types' const KLAUSUR_API = '/klausur-api' interface StepRowDetectionProps { sessionId: string | null onNext: () => void } export function StepRowDetection({ sessionId, onNext }: StepRowDetectionProps) { const [rowResult, setRowResult] = useState(null) const [detecting, setDetecting] = useState(false) const [error, setError] = useState(null) const [gtNotes, setGtNotes] = useState('') const [gtSaved, setGtSaved] = useState(false) useEffect(() => { if (!sessionId) return const fetchSession = async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`) if (res.ok) { const info = await res.json() if (info.row_result) { setRowResult(info.row_result) return } } } catch (e) { console.error('Failed to fetch session info:', e) } // No cached result — run auto runAutoDetection() } fetchSession() // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionId]) const runAutoDetection = useCallback(async () => { if (!sessionId) return setDetecting(true) setError(null) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/rows`, { method: 'POST', }) if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })) throw new Error(err.detail || 'Zeilenerkennung fehlgeschlagen') } const data: RowResult = await res.json() setRowResult(data) } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') } finally { setDetecting(false) } }, [sessionId]) const handleGroundTruth = useCallback(async (isCorrect: boolean) => { if (!sessionId) return const gt: RowGroundTruth = { is_correct: isCorrect, notes: gtNotes || undefined, } try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/rows`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(gt), }) setGtSaved(true) } catch (e) { console.error('Ground truth save failed:', e) } }, [sessionId, gtNotes]) if (!sessionId) { return (
📏

Schritt 4: Zeilenerkennung

Bitte zuerst Schritte 1-3 abschliessen.

) } const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/rows-overlay` const dewarpedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped` const rowTypeColors: Record = { header: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300', content: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300', footer: 'bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300', } return (
{/* Loading */} {detecting && (
Zeilenerkennung laeuft...
)} {/* Images: overlay vs clean */}
Mit Zeilen-Overlay
{rowResult ? ( // eslint-disable-next-line @next/next/no-img-element Zeilen-Overlay ) : (
{detecting ? 'Erkenne Zeilen...' : 'Keine Daten'}
)}
Entzerrtes Bild
{/* eslint-disable-next-line @next/next/no-img-element */} Entzerrt
{/* Row summary */} {rowResult && (

Ergebnis: {rowResult.total_rows} Zeilen erkannt

{rowResult.duration_seconds}s
{/* Type summary badges */}
{Object.entries(rowResult.summary).map(([type, count]) => ( {type}: {count} ))}
{/* Row list */}
{rowResult.rows.map((row) => (
R{row.index} {row.row_type} y={row.y} h={row.height}px {row.word_count} Woerter {row.gap_before > 0 && ( gap={row.gap_before}px )}
))}
)} {/* Controls */} {rowResult && (
{/* Ground truth */} {!gtSaved ? ( <> setGtNotes(e.target.value)} className="px-2 py-1 text-xs border rounded dark:bg-gray-700 dark:border-gray-600 w-48" /> ) : ( Ground Truth gespeichert )}
)} {error && (
{error}
)}
) }