'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 && (

)}
{/* 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.
)}
)
}