'use client' import { useCallback, useEffect, useRef, useState } from 'react' import { useGridEditor } from '@/components/grid-editor/useGridEditor' import { GridTable } from '@/components/grid-editor/GridTable' import { ImageLayoutEditor } from '@/components/grid-editor/ImageLayoutEditor' import type { GridZone } from '@/components/grid-editor/types' const KLAUSUR_API = '/klausur-api' interface StepGroundTruthProps { sessionId: string | null isGroundTruth: boolean onMarked: () => void gridSaveRef: React.MutableRefObject<(() => Promise) | null> } /** * Step 12: Ground Truth marking. * * Shows the full Grid-Review view (original image + table) so the user * can verify the final result before marking as Ground Truth reference. */ export function StepGroundTruth({ sessionId, isGroundTruth, onMarked, gridSaveRef }: StepGroundTruthProps) { const { grid, loading, saving, error, dirty, selectedCell, selectedCells, setSelectedCell, loadGrid, saveGrid, updateCellText, toggleColumnBold, toggleRowHeader, undo, redo, canUndo, canRedo, getAdjacentCell, deleteColumn, addColumn, deleteRow, addRow, toggleCellSelection, clearCellSelection, toggleSelectedBold, setCellColor, } = useGridEditor(sessionId) const [showImage, setShowImage] = useState(true) const [zoom, setZoom] = useState(100) const [markSaving, setMarkSaving] = useState(false) const [message, setMessage] = useState('') // Expose save function via ref useEffect(() => { if (gridSaveRef) { gridSaveRef.current = async () => { if (dirty) await saveGrid() } return () => { gridSaveRef.current = null } } }, [gridSaveRef, dirty, saveGrid]) // Load grid on mount useEffect(() => { if (sessionId) loadGrid() }, [sessionId, loadGrid]) // Keyboard shortcuts useEffect(() => { const handler = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo() } else if ((e.metaKey || e.ctrlKey) && e.key === 'z' && e.shiftKey) { e.preventDefault(); redo() } else if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); saveGrid() } else if ((e.metaKey || e.ctrlKey) && e.key === 'b') { e.preventDefault() if (selectedCells.size > 0) toggleSelectedBold() } else if (e.key === 'Escape') { clearCellSelection() } } window.addEventListener('keydown', handler) return () => window.removeEventListener('keydown', handler) }, [undo, redo, saveGrid, selectedCells, toggleSelectedBold, clearCellSelection]) const handleNavigate = useCallback( (cellId: string, direction: 'up' | 'down' | 'left' | 'right') => { const target = getAdjacentCell(cellId, direction) if (target) { setSelectedCell(target) setTimeout(() => { const el = document.getElementById(`cell-${target}`) if (el) { el.focus() if (el instanceof HTMLInputElement) el.select() } }, 0) } }, [getAdjacentCell, setSelectedCell], ) const handleMark = async () => { if (!sessionId) return setMarkSaving(true) setMessage('') try { if (dirty) await saveGrid() const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=kombi`, { method: 'POST' }, ) if (!res.ok) { const body = await res.text().catch(() => '') throw new Error(`Ground Truth fehlgeschlagen (${res.status}): ${body}`) } const data = await res.json() setMessage(`Ground Truth gespeichert (${data.cells_saved} Zellen)`) onMarked() } catch (e) { setMessage(e instanceof Error ? e.message : String(e)) } finally { setMarkSaving(false) } } if (!sessionId) { return
Keine Session ausgewaehlt.
} if (loading) { return (
Grid wird geladen...
) } if (error) { return (

Fehler: {error}

) } if (!grid || !grid.zones.length) { return
Kein Grid vorhanden.
} const imageUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped` return (
{/* GT Header Bar */}

Ground Truth {isGroundTruth && (bereits markiert)}

Pruefen Sie das Ergebnis und markieren Sie es als Referenz fuer Regressionstests.

{dirty && ( )}
{message && (
{message}
)} {/* Stats */}
{grid.summary.total_zones} Zone(n), {grid.summary.total_columns} Spalten,{' '} {grid.summary.total_rows} Zeilen, {grid.summary.total_cells} Zellen
{/* Split View: Image left + Grid right */}
{showImage && ( {}} onHorizontalsChange={() => {}} onCommitUndo={() => {}} onSplitColumnAt={() => {}} onDeleteColumn={() => {}} /> )}
{(() => { const groups: GridZone[][] = [] for (const zone of grid.zones) { const prev = groups[groups.length - 1] if (prev && zone.vsplit_group != null && prev[0].vsplit_group === zone.vsplit_group) { prev.push(zone) } else { groups.push([zone]) } } return groups.map((group) => (
1 ? 'flex gap-2' : ''}`}> {group.map((zone) => (
1 ? 'flex-1 min-w-0' : ''} bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700`} >
))}
)) })()}
{/* Keyboard tips */}
Tab: naechste Zelle Ctrl+Z/Y: Undo/Redo Ctrl+S: Speichern
) }