Files
breakpilot-lehrer/admin-lehrer/components/ocr-kombi/StepAnsicht.tsx
Benjamin Admin c1a903537b
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 45s
CI / test-python-klausur (push) Failing after 2m35s
CI / test-python-agent-core (push) Successful in 31s
CI / test-nodejs-website (push) Successful in 33s
Unified Grid: merge all zones into single Excel-like grid
Backend (unified_grid.py):
- build_unified_grid(): merges content + box zones into one zone
- Dominant row height from median of content row spacings
- Full-width boxes: rows integrated directly
- Partial-width boxes: extra rows inserted when box has more text
  lines than standard rows fit (e.g., 7 lines in 5-row height)
- Box-origin cells tagged with source_zone_type + box_region metadata

Backend (grid_editor_api.py):
- POST /sessions/{id}/build-unified-grid → persists as unified_grid_result
- GET /sessions/{id}/unified-grid → retrieve persisted result

Frontend:
- GridEditorCell: added source_zone_type, box_region fields
- GridTable: box-origin cells get tinted background + left border
- StepAnsicht: split-view with original image (left) + editable
  unified GridTable (right). Auto-builds on first load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:37:55 +02:00

191 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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 { useGridEditor } from '@/components/grid-editor/useGridEditor'
import { GridTable } from '@/components/grid-editor/GridTable'
import type { GridZone } from '@/components/grid-editor/types'
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<any>(null)
const [building, setBuilding] = useState(false)
const [buildError, setBuildError] = useState<string | null>(null)
const leftRef = useRef<HTMLDivElement>(null)
const [leftHeight, setLeftHeight] = useState(600)
const [showGrid, setShowGrid] = useState(false)
// 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 unified grid on mount (or build if missing)
useEffect(() => {
if (!sessionId) return
;(async () => {
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/unified-grid`)
if (res.ok) {
setUnifiedGrid(await res.json())
} else {
// Not built yet — build it
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 (
<div className="flex items-center justify-center py-16">
<div className="w-8 h-8 border-4 border-teal-500 border-t-transparent rounded-full animate-spin" />
<span className="ml-3 text-gray-500">{building ? 'Baue Unified Grid...' : 'Lade...'}</span>
</div>
)
}
return (
<div className="space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Ansicht Unified Grid</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
Alle Inhalte in einem Grid. Boxen sind integriert (farbig markiert).
{unifiedGrid && (
<span className="ml-2 font-mono text-xs">
{unifiedGrid.summary?.total_rows} Zeilen × {unifiedGrid.summary?.total_columns} Spalten
{unifiedGrid.dominant_row_h && ` · Zeilenhöhe: ${Math.round(unifiedGrid.dominant_row_h)}px`}
</span>
)}
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={buildUnified}
disabled={building}
className="px-3 py-1.5 bg-amber-600 text-white rounded-lg hover:bg-amber-700 text-xs font-medium disabled:opacity-50"
>
{building ? 'Baut...' : 'Neu aufbauen'}
</button>
<button onClick={onNext} className="px-5 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 text-sm font-medium">
Weiter
</button>
</div>
</div>
{(error || buildError) && (
<div className="p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-300 text-sm">
{error || buildError}
</div>
)}
{/* Split view */}
<div className="flex gap-2">
{/* LEFT: Original + OCR overlay */}
<div ref={leftRef} className="w-1/3 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-900 flex-shrink-0">
<div className="px-2 py-1 bg-black/60 text-white text-[10px] font-medium">Original + OCR</div>
{sessionId && (
<img
src={`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/words-overlay`}
alt="Original + OCR"
className="w-full h-auto"
/>
)}
</div>
{/* RIGHT: Unified Grid Table */}
<div className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg overflow-auto bg-white dark:bg-gray-900" style={{ maxHeight: `${Math.max(600, leftHeight)}px` }}>
<div className="px-2 py-1 bg-teal-600/80 text-white text-[10px] font-medium sticky top-0 z-20">
Unified Grid
{unifiedGrid?.is_unified && (
<span className="ml-2 opacity-70">
({unifiedGrid.summary?.total_rows}×{unifiedGrid.summary?.total_columns})
</span>
)}
</div>
{unifiedZone ? (
<GridTable
zone={unifiedZone}
selectedCell={selectedCell}
selectedCells={selectedCells}
onSelectCell={setSelectedCell}
onCellTextChange={updateCellText}
onToggleColumnBold={toggleColumnBold}
onToggleRowHeader={toggleRowHeader}
onNavigate={(cellId, dir) => {
const next = getAdjacentCell(cellId, dir)
if (next) setSelectedCell(next)
}}
onDeleteColumn={deleteColumn}
onAddColumn={addColumn}
onDeleteRow={deleteRow}
onAddRow={addRow}
onToggleCellSelection={toggleCellSelection}
onSetCellColor={setCellColor}
/>
) : (
<div className="p-8 text-center text-gray-400">
<p>Kein Unified Grid verfügbar.</p>
<button onClick={buildUnified} className="mt-2 text-teal-600 text-sm">Jetzt aufbauen</button>
</div>
)}
</div>
</div>
</div>
)
}