'use client' /** * StepAnsicht — Split-view page layout comparison. * * Left: Original scan with OCR word overlay (red) + coordinate grid * Right: Reconstructed layout with all zones + coordinate grid * * Both sides share the same coordinate system for easy visual comparison. */ import { useEffect, useRef, useState } from 'react' import { useGridEditor } from '@/components/grid-editor/useGridEditor' import type { GridZone, GridEditorCell } from '@/components/grid-editor/types' const KLAUSUR_API = '/klausur-api' interface StepAnsichtProps { sessionId: string | null onNext: () => void } function getCellColor(cell: GridEditorCell | undefined): string | null { if (!cell) return null if (cell.color_override) return cell.color_override const colored = cell.word_boxes?.find((wb) => wb.color_name && wb.color_name !== 'black') return colored?.color ?? null } export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) { const { grid, loading, error, loadGrid } = useGridEditor(sessionId) const leftRef = useRef(null) const [panelWidth, setPanelWidth] = useState(0) const [showGrid, setShowGrid] = useState(true) const [gridSpacing, setGridSpacing] = useState(100) // px in original coordinates useEffect(() => { if (sessionId) loadGrid() }, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps // Track panel width useEffect(() => { if (!leftRef.current) return const ro = new ResizeObserver(([entry]) => setPanelWidth(entry.contentRect.width)) ro.observe(leftRef.current) return () => ro.disconnect() }, []) if (loading) { return (
Lade Vorschau...
) } if (error || !grid) { return (

{error || 'Keine Grid-Daten.'}

) } const imgW = grid.image_width || 1 const imgH = grid.image_height || 1 const scale = panelWidth > 0 ? panelWidth / imgW : 0.5 const panelHeight = imgH * scale const baseFontPx = (grid as any).layout_metrics?.font_size_suggestion_px || 14 const scaledFont = Math.max(7, baseFontPx * scale * 0.85) // Collect all word boxes for OCR overlay const allWordBoxes = grid.zones.flatMap((z) => z.cells.flatMap((c) => (c.word_boxes || []).map((wb) => ({ ...wb, zone: z }))) ) return (
{/* Header */}

Ansicht — Original vs. Rekonstruktion

Links: Original mit OCR-Overlay. Rechts: Rekonstruierte Seite. Koordinatengitter zum Abgleich.

{/* Split view */}
0 ? `${panelHeight + 40}px` : '600px' }}> {/* LEFT: Original + OCR overlay */}
Original + OCR
{/* Scan image */} {sessionId && ( Original )} {/* OCR word overlay (red text) */} {allWordBoxes.map((wb, i) => (
{wb.text}
))} {/* Coordinate grid */} {showGrid && }
{/* RIGHT: Reconstruction */}
Rekonstruktion
{/* Rendered zones */} {grid.zones.map((zone) => ( ))} {/* Coordinate grid */} {showGrid && }
) } // --------------------------------------------------------------------------- // Coordinate grid overlay // --------------------------------------------------------------------------- function CoordinateGrid({ imgW, imgH, scale, spacing }: { imgW: number; imgH: number; scale: number; spacing: number }) { const lines: JSX.Element[] = [] // Vertical lines for (let x = 0; x <= imgW; x += spacing) { const px = x * scale lines.push(
{x}
) } // Horizontal lines for (let y = 0; y <= imgH; y += spacing) { const px = y * scale lines.push(
{y}
) } return <>{lines} } // --------------------------------------------------------------------------- // Zone renderer (reconstruction side) // --------------------------------------------------------------------------- function ZoneRenderer({ zone, scale, fontSize }: { zone: GridZone; scale: number; fontSize: number }) { const isBox = zone.zone_type === 'box' const boxColor = (zone as any).box_bg_hex || '#6b7280' if (!zone.cells || zone.cells.length === 0) return null const left = zone.bbox_px.x * scale const top = zone.bbox_px.y * scale const width = zone.bbox_px.w * scale const height = zone.bbox_px.h * scale const cellMap = new Map() for (const cell of zone.cells) { cellMap.set(`${cell.row_index}_${cell.col_index}`, cell) } // Column widths scaled to zone const colWidths = zone.columns.map((col) => { const w = (col.x_max_px ?? 0) - (col.x_min_px ?? 0) return Math.max(5, w * scale) }) const totalColW = colWidths.reduce((s, w) => s + w, 0) const colScale = totalColW > 0 ? width / totalColW : 1 const scaledColWidths = colWidths.map((w) => w * colScale) const numCols = zone.columns.length return (
`${w.toFixed(1)}px`).join(' ') }}> {zone.rows.map((row) => { const isSpanning = zone.cells.some((c) => c.row_index === row.index && c.col_type === 'spanning_header') const measuredH = (row.y_max_px ?? 0) - (row.y_min_px ?? 0) const rowH = Math.max(fontSize * 1.3, measuredH * scale) return (
{isSpanning ? ( zone.cells .filter((c) => c.row_index === row.index && c.col_type === 'spanning_header') .sort((a, b) => a.col_index - b.col_index) .map((cell) => { const colspan = cell.colspan || numCols const color = getCellColor(cell) return (
{cell.text}
) }) ) : ( zone.columns.map((col) => { const cell = cellMap.get(`${row.index}_${col.index}`) if (!cell) return
const color = getCellColor(cell) const isBold = col.bold || cell.is_bold || row.is_header const text = cell.text ?? '' return (
{text}
) }) )}
) })}
) }