'use client' /** * StepAnsicht — Unified Grid View. * * Left: Original scan with OCR word overlay * Right: Unified grid (single zone, boxes integrated) rendered via GridTable */ import { useCallback, useEffect, useRef, useState } from 'react' import dynamic from 'next/dynamic' import { useGridEditor } from '@/components/grid-editor/useGridEditor' import { GridTable } from '@/components/grid-editor/GridTable' import type { GridZone } from '@/components/grid-editor/types' // Lazy-load SpreadsheetView (Fortune Sheet, SSR-incompatible) const SpreadsheetView = dynamic( () => import('./SpreadsheetView').then((m) => m.SpreadsheetView), { ssr: false, loading: () =>
Spreadsheet wird geladen...
}, ) const KLAUSUR_API = '/klausur-api' interface StepAnsichtProps { sessionId: string | null onNext: () => void } export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) { const gridEditor = useGridEditor(sessionId) const { loading, error, selectedCell, setSelectedCell, updateCellText, toggleColumnBold, toggleRowHeader, getAdjacentCell, deleteColumn, addColumn, deleteRow, addRow, commitUndoPoint, selectedCells, toggleCellSelection, clearCellSelection, toggleSelectedBold, setCellColor, saveGrid, saving, dirty, undo, redo, canUndo, canRedo, } = gridEditor const [unifiedGrid, setUnifiedGrid] = useState(null) const [building, setBuilding] = useState(false) const [buildError, setBuildError] = useState(null) const leftRef = useRef(null) const [leftHeight, setLeftHeight] = useState(600) const [viewMode, setViewMode] = useState<'spreadsheet' | 'grid'>('spreadsheet') // Build unified grid const buildUnified = useCallback(async () => { if (!sessionId) return setBuilding(true) setBuildError(null) try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/build-unified-grid`, { method: 'POST' }, ) if (!res.ok) { const d = await res.json().catch(() => ({})) throw new Error(d.detail || `HTTP ${res.status}`) } const data = await res.json() setUnifiedGrid(data) } catch (e) { setBuildError(e instanceof Error ? e.message : String(e)) } finally { setBuilding(false) } }, [sessionId]) // Load both grids on mount useEffect(() => { if (!sessionId) return // Load multi-zone grid (for spreadsheet mode) gridEditor.loadGrid() // Load unified grid (for grid mode) ;(async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/unified-grid`) if (res.ok) { setUnifiedGrid(await res.json()) } else { buildUnified() } } catch { buildUnified() } })() }, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps // Track left panel height for sync useEffect(() => { if (!leftRef.current) return const ro = new ResizeObserver(([e]) => setLeftHeight(e.contentRect.height)) ro.observe(leftRef.current) return () => ro.disconnect() }, []) const unifiedZone: GridZone | null = unifiedGrid?.zones?.[0] ?? null if (loading || building) { return (
{building ? 'Baue Unified Grid...' : 'Lade...'}
) } return (
{/* Header */}

Ansicht — Unified Grid

Alle Inhalte in einem Grid. Boxen sind integriert (farbig markiert). {unifiedGrid && ( {unifiedGrid.summary?.total_rows} Zeilen × {unifiedGrid.summary?.total_columns} Spalten {unifiedGrid.dominant_row_h && ` · Zeilenhöhe: ${Math.round(unifiedGrid.dominant_row_h)}px`} )}

{(error || buildError) && (
{error || buildError}
)} {/* Split view */}
{/* LEFT: Original + OCR overlay */}
Original + OCR
{sessionId && ( Original + OCR )}
{/* RIGHT: Spreadsheet or Grid view */}
{viewMode === 'spreadsheet' && (unifiedGrid || gridEditor.grid) ? ( ) : viewMode === 'grid' && unifiedZone ? (
Grid View ({unifiedGrid?.summary?.total_rows}×{unifiedGrid?.summary?.total_columns})
{ const next = getAdjacentCell(cellId, dir) if (next) setSelectedCell(next) }} onDeleteColumn={deleteColumn} onAddColumn={addColumn} onDeleteRow={deleteRow} onAddRow={addRow} onToggleCellSelection={toggleCellSelection} onSetCellColor={setCellColor} />
) : (

Kein Unified Grid verfügbar.

)}
) }