feat: ImageLayoutEditor, arrow-key nav, multi-select bold, wider columns
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 32s
CI / test-go-edu-search (push) Successful in 25s
CI / test-python-klausur (push) Failing after 1m52s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 18s

- New ImageLayoutEditor: SVG overlay on original scan with draggable
  column dividers, horizontal guidelines (margins/header/footer),
  double-click to add columns, x-button to delete
- GridTable: MIN_COL_WIDTH 40→80px for better readability
- Arrow up/down keys navigate between rows in the grid editor
- Ctrl+Click for multi-cell selection, Ctrl+B to toggle bold on selection
- getAdjacentCell works for cells that don't exist yet (new rows/cols)
- deleteColumn now merges x-boundaries correctly
- Session restore fix: grid_editor_result/structure_result in session GET
- Footer row 3-state cycle, auto-create cells for empty footer rows
- Grid save/build/GT-mark now advance current_step=11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-24 07:45:39 +01:00
parent 4e668660a7
commit 65f4ce1947
12 changed files with 1422 additions and 90 deletions

View File

@@ -8,20 +8,22 @@
* the GT marking flow in the parent page.
*/
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react'
import { useGridEditor } from '@/components/grid-editor/useGridEditor'
import type { GridZone } from '@/components/grid-editor/types'
import type { GridZone, LayoutDividers } from '@/components/grid-editor/types'
import { GridToolbar } from '@/components/grid-editor/GridToolbar'
import { GridTable } from '@/components/grid-editor/GridTable'
import { ImageLayoutEditor } from '@/components/grid-editor/ImageLayoutEditor'
const KLAUSUR_API = '/klausur-api'
interface StepGridReviewProps {
sessionId: string | null
onNext?: () => void
saveRef?: MutableRefObject<(() => Promise<void>) | null>
}
export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
export function StepGridReview({ sessionId, onNext, saveRef }: StepGridReviewProps) {
const {
grid,
loading,
@@ -29,6 +31,7 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
error,
dirty,
selectedCell,
selectedCells,
setSelectedCell,
buildGrid,
loadGrid,
@@ -43,12 +46,31 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
getAdjacentCell,
deleteColumn,
addColumn,
deleteRow,
addRow,
commitUndoPoint,
updateColumnDivider,
updateLayoutHorizontals,
splitColumnAt,
toggleCellSelection,
clearCellSelection,
toggleSelectedBold,
} = useGridEditor(sessionId)
const [showImage, setShowImage] = useState(true)
const [zoom, setZoom] = useState(100)
const [acceptedRows, setAcceptedRows] = useState<Set<string>>(new Set())
// Expose save function to parent via ref (for GT marking auto-save)
useEffect(() => {
if (saveRef) {
saveRef.current = async () => {
if (dirty) await saveGrid()
}
return () => { saveRef.current = null }
}
}, [saveRef, dirty, saveGrid])
// Load grid on mount
useEffect(() => {
if (sessionId) loadGrid()
@@ -71,11 +93,18 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
} 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])
}, [undo, redo, saveGrid, selectedCells, toggleSelectedBold, clearCellSelection])
const handleNavigate = useCallback(
(cellId: string, direction: 'up' | 'down' | 'left' | 'right') => {
@@ -195,6 +224,11 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
{grid.summary.total_zones} Zone(n), {grid.summary.total_columns} Spalten,{' '}
{grid.summary.total_rows} Zeilen, {grid.summary.total_cells} Zellen
</span>
{grid.dictionary_detection?.is_dictionary && (
<span className="px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
Woerterbuch ({Math.round(grid.dictionary_detection.confidence * 100)}%)
</span>
)}
{lowConfCells.length > 0 && (
<span className="px-2 py-0.5 rounded-full bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
{lowConfCells.length} niedrige Konfidenz
@@ -249,47 +283,21 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
className={showImage ? 'grid grid-cols-2 gap-3' : ''}
style={{ minHeight: '55vh' }}
>
{/* Left: Original Image */}
{/* Left: Original Image with Layout Editor */}
{showImage && (
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col">
<div className="flex items-center justify-between px-3 py-2 border-b border-gray-100 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50">
<span className="text-xs font-medium text-gray-600 dark:text-gray-400">
Original Scan (zugeschnitten)
</span>
<div className="flex items-center gap-2">
<button
onClick={() => setZoom((z) => Math.max(50, z - 25))}
className="px-2 py-0.5 text-xs bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
>
-
</button>
<span className="text-xs text-gray-500 dark:text-gray-400 w-10 text-center">
{zoom}%
</span>
<button
onClick={() => setZoom((z) => Math.min(300, z + 25))}
className="px-2 py-0.5 text-xs bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
>
+
</button>
<button
onClick={() => setZoom(100)}
className="px-2 py-0.5 text-xs bg-gray-200 dark:bg-gray-700 rounded hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300"
>
Fit
</button>
</div>
</div>
<div className="flex-1 overflow-auto p-2">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imageUrl}
alt="Original scan"
style={{ width: `${zoom}%`, maxWidth: 'none' }}
className="block"
/>
</div>
</div>
<ImageLayoutEditor
imageUrl={imageUrl}
zones={grid.zones}
imageWidth={grid.image_width}
layoutDividers={grid.layout_dividers}
zoom={zoom}
onZoomChange={setZoom}
onColumnDividerMove={updateColumnDivider}
onHorizontalsChange={updateLayoutHorizontals}
onCommitUndo={commitUndoPoint}
onSplitColumnAt={splitColumnAt}
onDeleteColumn={deleteColumn}
/>
)}
{/* Right: Grid with row-accept buttons */}
@@ -370,13 +378,17 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
zone={zone}
layoutMetrics={grid.layout_metrics}
selectedCell={selectedCell}
selectedCells={selectedCells}
onSelectCell={setSelectedCell}
onToggleCellSelection={toggleCellSelection}
onCellTextChange={updateCellText}
onToggleColumnBold={toggleColumnBold}
onToggleRowHeader={toggleRowHeader}
onNavigate={handleNavigate}
onDeleteColumn={deleteColumn}
onAddColumn={addColumn}
onDeleteRow={deleteRow}
onAddRow={addRow}
/>
</div>
))}
@@ -388,11 +400,34 @@ export function StepGridReview({ sessionId, onNext }: StepGridReviewProps) {
</div>
</div>
{/* Multi-select toolbar */}
{selectedCells.size > 0 && (
<div className="flex items-center gap-3 px-3 py-2 bg-teal-50 dark:bg-teal-900/20 border border-teal-200 dark:border-teal-800 rounded-lg text-xs">
<span className="text-teal-700 dark:text-teal-300 font-medium">
{selectedCells.size} Zellen markiert
</span>
<button
onClick={toggleSelectedBold}
className="px-2.5 py-1 bg-teal-600 text-white rounded hover:bg-teal-700 transition-colors font-medium"
>
B Fett umschalten
</button>
<button
onClick={clearCellSelection}
className="px-2 py-1 text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-200 transition-colors"
>
Auswahl aufheben (Esc)
</button>
</div>
)}
{/* Tips + Next */}
<div className="flex items-center justify-between">
<div className="text-[11px] text-gray-400 dark:text-gray-500 flex items-center gap-4">
<span>Tab: naechste Zelle</span>
<span>Enter: Zeile runter</span>
<span>Pfeiltasten: Navigation</span>
<span>Ctrl+Klick: Mehrfachauswahl</span>
<span>Ctrl+B: Fett</span>
<span>Ctrl+Z/Y: Undo/Redo</span>
<span>Ctrl+S: Speichern</span>
</div>