Refactor: extract _build_grid_core into grid_build_core.py + clean StepAnsicht
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) Failing after 19s
CI / test-go-edu-search (push) Failing after 23s
CI / test-python-klausur (push) Failing after 10s
CI / test-python-agent-core (push) Failing after 9s
CI / test-nodejs-website (push) Failing after 26s

grid_editor_api.py: 2411 → 474 lines
- Extracted _build_grid_core() (1892 lines) into grid_build_core.py
- API file now only contains endpoints (build, save, get, gutter, box, unified)

StepAnsicht.tsx: 212 → 112 lines
- Removed useGridEditor imports (not needed for read-only spreadsheet)
- Removed unified grid fetch/build (not used with multi-sheet approach)
- Removed Spreadsheet/Grid toggle (only spreadsheet mode now)
- Simple: fetch grid-editor data → pass to SpreadsheetView

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-15 08:54:55 +02:00
parent d4353d76fb
commit 17f0fdb2ed
3 changed files with 1982 additions and 2078 deletions

View File

@@ -1,19 +1,15 @@
'use client' 'use client'
/** /**
* StepAnsicht — Unified Grid View. * StepAnsicht — Excel-like Spreadsheet View.
* *
* Left: Original scan with OCR word overlay * Left: Original scan with OCR word overlay
* Right: Unified grid (single zone, boxes integrated) rendered via GridTable * Right: Fortune Sheet spreadsheet with multi-sheet tabs per zone
*/ */
import { useCallback, useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic' 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( const SpreadsheetView = dynamic(
() => import('./SpreadsheetView').then((m) => m.SpreadsheetView), () => import('./SpreadsheetView').then((m) => m.SpreadsheetView),
{ ssr: false, loading: () => <div className="py-8 text-center text-sm text-gray-400">Spreadsheet wird geladen...</div> }, { ssr: false, loading: () => <div className="py-8 text-center text-sm text-gray-400">Spreadsheet wird geladen...</div> },
@@ -27,67 +23,29 @@ interface StepAnsichtProps {
} }
export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) { export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) {
const gridEditor = useGridEditor(sessionId) const [gridData, setGridData] = useState<any>(null)
const { const [loading, setLoading] = useState(true)
loading, error, selectedCell, setSelectedCell, const [error, setError] = useState<string | null>(null)
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 leftRef = useRef<HTMLDivElement>(null)
const [leftHeight, setLeftHeight] = useState(600) const [leftHeight, setLeftHeight] = useState(600)
const [viewMode, setViewMode] = useState<'spreadsheet' | 'grid'>('spreadsheet')
// Build unified grid // Load grid data on mount
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(() => { useEffect(() => {
if (!sessionId) return if (!sessionId) return
// Load multi-zone grid (for spreadsheet mode)
gridEditor.loadGrid()
// Load unified grid (for grid mode)
;(async () => { ;(async () => {
try { try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/unified-grid`) const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/grid-editor`)
if (res.ok) { if (!res.ok) throw new Error(`HTTP ${res.status}`)
setUnifiedGrid(await res.json()) setGridData(await res.json())
} else { } catch (e) {
buildUnified() setError(e instanceof Error ? e.message : 'Fehler beim Laden')
} } finally {
} catch { setLoading(false)
buildUnified()
} }
})() })()
}, [sessionId]) // eslint-disable-line react-hooks/exhaustive-deps }, [sessionId])
// Track left panel height for sync // Track left panel height
useEffect(() => { useEffect(() => {
if (!leftRef.current) return if (!leftRef.current) return
const ro = new ResizeObserver(([e]) => setLeftHeight(e.contentRect.height)) const ro = new ResizeObserver(([e]) => setLeftHeight(e.contentRect.height))
@@ -95,13 +53,20 @@ export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) {
return () => ro.disconnect() return () => ro.disconnect()
}, []) }, [])
const unifiedZone: GridZone | null = unifiedGrid?.zones?.[0] ?? null if (loading) {
if (loading || building) {
return ( return (
<div className="flex items-center justify-center py-16"> <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" /> <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> <span className="ml-3 text-gray-500">Lade Spreadsheet...</span>
</div>
)
}
if (error || !gridData) {
return (
<div className="p-8 text-center">
<p className="text-red-500 mb-4">{error || 'Keine Grid-Daten.'}</p>
<button onClick={onNext} className="px-5 py-2 bg-teal-600 text-white rounded-lg">Weiter </button>
</div> </div>
) )
} }
@@ -111,51 +76,16 @@ export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">Ansicht Unified Grid</h3> <h3 className="text-lg font-semibold text-gray-900 dark:text-white">Ansicht Spreadsheet</h3>
<p className="text-sm text-gray-500 dark:text-gray-400"> <p className="text-sm text-gray-500 dark:text-gray-400">
Alle Inhalte in einem Grid. Boxen sind integriert (farbig markiert). Jede Zone als eigenes Sheet-Tab. Spaltenbreiten pro Sheet optimiert.
{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> </p>
</div> </div>
<div className="flex items-center gap-2"> <button onClick={onNext} className="px-5 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 text-sm font-medium">
<div className="flex rounded-lg overflow-hidden border border-gray-300 dark:border-gray-600"> Weiter
<button </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> </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 */} {/* Split view */}
<div className="flex gap-2"> <div className="flex gap-2">
{/* LEFT: Original + OCR overlay */} {/* LEFT: Original + OCR overlay */}
@@ -170,41 +100,9 @@ export function StepAnsicht({ sessionId, onNext }: StepAnsichtProps) {
)} )}
</div> </div>
{/* RIGHT: Spreadsheet or Grid view */} {/* RIGHT: Fortune Sheet */}
<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` }}> <div className="flex-1 border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-900">
{viewMode === 'spreadsheet' && (unifiedGrid || gridEditor.grid) ? ( <SpreadsheetView gridData={gridData} height={Math.max(650, leftHeight - 10)} />
<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> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff