Files
breakpilot-lehrer/admin-lehrer/components/ocr-kombi/StepAnsicht.tsx
Benjamin Admin d4353d76fb
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 36s
CI / test-go-edu-search (push) Successful in 36s
CI / test-python-klausur (push) Failing after 2m21s
CI / test-python-agent-core (push) Successful in 31s
CI / test-nodejs-website (push) Successful in 31s
SpreadsheetView: multi-sheet tabs instead of unified single sheet
Each zone becomes its own Excel sheet tab with independent column widths:
- Sheet "Vokabeln": main content zone with EN/DE/example columns
- Sheet "Pounds and euros": Box 1 with its own 4-column layout
- Sheet "German leihen": Box 2 with single column for flowing text

This solves the column-width conflict: boxes have different column
widths optimized for their content, which is impossible in a single
unified sheet (Excel limitation: column width is per-column, not per-cell).

Sheet tabs visible at bottom (showSheetTabs: true).
Box sheets get colored tab (from box_bg_hex).
First sheet active by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 00:51:21 +02:00

213 lines
8.2 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 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: () => <div className="py-8 text-center text-sm text-gray-400">Spreadsheet wird geladen...</div> },
)
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 [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 (
<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">
<div className="flex rounded-lg overflow-hidden border border-gray-300 dark:border-gray-600">
<button
onClick={() => setViewMode('spreadsheet')}
className={`px-3 py-1.5 text-xs font-medium ${viewMode === 'spreadsheet' ? 'bg-teal-600 text-white' : 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
>
Spreadsheet
</button>
<button
onClick={() => setViewMode('grid')}
className={`px-3 py-1.5 text-xs font-medium ${viewMode === 'grid' ? 'bg-teal-600 text-white' : 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-300'}`}
>
Grid
</button>
</div>
<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: Spreadsheet or Grid view */}
<div className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-900" style={{ maxHeight: `${Math.max(700, leftHeight)}px` }}>
{viewMode === 'spreadsheet' && (unifiedGrid || gridEditor.grid) ? (
<SpreadsheetView gridData={gridEditor.grid} height={Math.max(650, leftHeight - 10)} />
) : viewMode === 'grid' && unifiedZone ? (
<div className="overflow-auto h-full">
<div className="px-2 py-1 bg-teal-600/80 text-white text-[10px] font-medium sticky top-0 z-20">
Grid View ({unifiedGrid?.summary?.total_rows}×{unifiedGrid?.summary?.total_columns})
</div>
<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>
) : (
<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>
)
}