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
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>
213 lines
8.2 KiB
TypeScript
213 lines
8.2 KiB
TypeScript
'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>
|
||
)
|
||
}
|