Phase 1 — Python (klausur-service): 5 monoliths → 36 files - dsfa_corpus_ingestion.py (1,828 LOC → 5 files) - cv_ocr_engines.py (2,102 LOC → 7 files) - cv_layout.py (3,653 LOC → 10 files) - vocab_worksheet_api.py (2,783 LOC → 8 files) - grid_build_core.py (1,958 LOC → 6 files) Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files - staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3) - policy_handlers.go (700 → 2), repository.go (684 → 2) - search.go (592 → 2), ai_extraction_handlers.go (554 → 2) - seed_data.go (591 → 2), grade_service.go (646 → 2) Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files - sdk/types.ts (2,108 → 16 domain files) - ai/rag/page.tsx (2,686 → 14 files) - 22 page.tsx files split into _components/ + _hooks/ - 11 component files split into sub-components - 10 SDK data catalogs added to loc-exceptions - Deleted dead backup index_original.ts (4,899 LOC) All original public APIs preserved via re-export facades. Zero new errors: Python imports verified, Go builds clean, TypeScript tsc --noEmit shows only pre-existing errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
218 lines
6.6 KiB
TypeScript
218 lines
6.6 KiB
TypeScript
import { useCallback, useRef, useState } from 'react'
|
|
import type { StructuredGrid } from './types'
|
|
import { MAX_UNDO } from './gridEditorTypes'
|
|
import type { IpaMode, SyllableMode } from './gridEditorTypes'
|
|
import { useGridEditorApi } from './useGridEditorApi'
|
|
import { useGridEditorActions } from './useGridEditorActions'
|
|
import { useGridEditorLayout } from './useGridEditorLayout'
|
|
|
|
// Re-export types so existing imports keep working
|
|
export type { GridEditorState, IpaMode, SyllableMode } from './gridEditorTypes'
|
|
|
|
export function useGridEditor(sessionId: string | null) {
|
|
const [grid, setGrid] = useState<StructuredGrid | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
const [saving, setSaving] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [dirty, setDirty] = useState(false)
|
|
const [selectedCell, setSelectedCell] = useState<string | null>(null)
|
|
const [selectedZone, setSelectedZone] = useState<number | null>(null)
|
|
const [ipaMode, setIpaMode] = useState<IpaMode>('auto')
|
|
const [syllableMode, setSyllableMode] = useState<SyllableMode>('auto')
|
|
|
|
// OCR Quality Steps (A/B testing toggles — defaults off for now)
|
|
const [ocrEnhance, setOcrEnhance] = useState(false)
|
|
const [ocrMaxCols, setOcrMaxCols] = useState(0)
|
|
const [ocrMinConf, setOcrMinConf] = useState(0)
|
|
|
|
// Vision-LLM Fusion (Step 4)
|
|
const [visionFusion, setVisionFusion] = useState(false)
|
|
const [documentCategory, setDocumentCategory] = useState('vokabelseite')
|
|
|
|
// Undo/redo stacks store serialized zone arrays
|
|
const undoStack = useRef<string[]>([])
|
|
const redoStack = useRef<string[]>([])
|
|
|
|
const pushUndo = useCallback((zones: StructuredGrid['zones']) => {
|
|
undoStack.current.push(JSON.stringify(zones))
|
|
if (undoStack.current.length > MAX_UNDO) {
|
|
undoStack.current.shift()
|
|
}
|
|
redoStack.current = []
|
|
}, [])
|
|
|
|
// ------------------------------------------------------------------
|
|
// API calls (build, load, save, rerunOcr)
|
|
// ------------------------------------------------------------------
|
|
|
|
const { buildGrid, rerunOcr, loadGrid, saveGrid } = useGridEditorApi({
|
|
sessionId,
|
|
ipaMode,
|
|
syllableMode,
|
|
ocrEnhance,
|
|
ocrMaxCols,
|
|
ocrMinConf,
|
|
visionFusion,
|
|
documentCategory,
|
|
setGrid,
|
|
setLoading,
|
|
setSaving,
|
|
setError,
|
|
setDirty,
|
|
grid,
|
|
undoStack,
|
|
redoStack,
|
|
})
|
|
|
|
// ------------------------------------------------------------------
|
|
// Cell editing, formatting, multi-select
|
|
// ------------------------------------------------------------------
|
|
|
|
const {
|
|
updateCellText,
|
|
setCellColor,
|
|
toggleColumnBold,
|
|
toggleRowHeader,
|
|
autoCorrectColumnPatterns,
|
|
selectedCells,
|
|
toggleCellSelection,
|
|
clearCellSelection,
|
|
toggleSelectedBold,
|
|
} = useGridEditorActions({ grid, setGrid, setDirty, pushUndo })
|
|
|
|
// ------------------------------------------------------------------
|
|
// Column/row management, layout dividers
|
|
// ------------------------------------------------------------------
|
|
|
|
const {
|
|
deleteColumn,
|
|
addColumn,
|
|
deleteRow,
|
|
addRow,
|
|
commitUndoPoint,
|
|
updateColumnDivider,
|
|
updateLayoutHorizontals,
|
|
splitColumnAt,
|
|
} = useGridEditorLayout({ grid, setGrid, setDirty, pushUndo })
|
|
|
|
// ------------------------------------------------------------------
|
|
// Undo / Redo
|
|
// ------------------------------------------------------------------
|
|
|
|
const undo = useCallback(() => {
|
|
if (!grid || undoStack.current.length === 0) return
|
|
redoStack.current.push(JSON.stringify(grid.zones))
|
|
const prev = undoStack.current.pop()!
|
|
setGrid((g) => (g ? { ...g, zones: JSON.parse(prev) } : g))
|
|
setDirty(true)
|
|
}, [grid])
|
|
|
|
const redo = useCallback(() => {
|
|
if (!grid || redoStack.current.length === 0) return
|
|
undoStack.current.push(JSON.stringify(grid.zones))
|
|
const next = redoStack.current.pop()!
|
|
setGrid((g) => (g ? { ...g, zones: JSON.parse(next) } : g))
|
|
setDirty(true)
|
|
}, [grid])
|
|
|
|
const canUndo = undoStack.current.length > 0
|
|
const canRedo = redoStack.current.length > 0
|
|
|
|
// ------------------------------------------------------------------
|
|
// Navigation helpers
|
|
// ------------------------------------------------------------------
|
|
|
|
const getAdjacentCell = useCallback(
|
|
(cellId: string, direction: 'up' | 'down' | 'left' | 'right'): string | null => {
|
|
if (!grid) return null
|
|
for (const zone of grid.zones) {
|
|
// Find the cell or derive row/col from cellId pattern
|
|
const cell = zone.cells.find((c) => c.cell_id === cellId)
|
|
let currentRow: number, currentCol: number
|
|
if (cell) {
|
|
currentRow = cell.row_index
|
|
currentCol = cell.col_index
|
|
} else {
|
|
// Try to parse from cellId: Z{zone}_R{row}_C{col}
|
|
const match = cellId.match(/^Z(\d+)_R(\d+)_C(\d+)$/)
|
|
if (!match || parseInt(match[1]) !== zone.zone_index) continue
|
|
currentRow = parseInt(match[2])
|
|
currentCol = parseInt(match[3])
|
|
}
|
|
|
|
let targetRow = currentRow
|
|
let targetCol = currentCol
|
|
if (direction === 'up') targetRow--
|
|
if (direction === 'down') targetRow++
|
|
if (direction === 'left') targetCol--
|
|
if (direction === 'right') targetCol++
|
|
|
|
// Check bounds
|
|
const hasRow = zone.rows.some((r) => r.index === targetRow)
|
|
const hasCol = zone.columns.some((c) => c.index === targetCol)
|
|
if (!hasRow || !hasCol) return null
|
|
|
|
// Return existing cell ID or construct one
|
|
const target = zone.cells.find(
|
|
(c) => c.row_index === targetRow && c.col_index === targetCol,
|
|
)
|
|
return target?.cell_id ?? `Z${zone.zone_index}_R${String(targetRow).padStart(2, '0')}_C${targetCol}`
|
|
}
|
|
return null
|
|
},
|
|
[grid],
|
|
)
|
|
|
|
return {
|
|
grid,
|
|
loading,
|
|
saving,
|
|
error,
|
|
dirty,
|
|
selectedCell,
|
|
selectedZone,
|
|
selectedCells,
|
|
setSelectedCell,
|
|
setSelectedZone,
|
|
buildGrid,
|
|
loadGrid,
|
|
saveGrid,
|
|
updateCellText,
|
|
toggleColumnBold,
|
|
toggleRowHeader,
|
|
undo,
|
|
redo,
|
|
canUndo,
|
|
canRedo,
|
|
getAdjacentCell,
|
|
deleteColumn,
|
|
addColumn,
|
|
deleteRow,
|
|
addRow,
|
|
commitUndoPoint,
|
|
updateColumnDivider,
|
|
updateLayoutHorizontals,
|
|
splitColumnAt,
|
|
toggleCellSelection,
|
|
clearCellSelection,
|
|
toggleSelectedBold,
|
|
autoCorrectColumnPatterns,
|
|
setCellColor,
|
|
ipaMode,
|
|
setIpaMode,
|
|
syllableMode,
|
|
setSyllableMode,
|
|
ocrEnhance,
|
|
setOcrEnhance,
|
|
ocrMaxCols,
|
|
setOcrMaxCols,
|
|
ocrMinConf,
|
|
setOcrMinConf,
|
|
visionFusion,
|
|
setVisionFusion,
|
|
documentCategory,
|
|
setDocumentCategory,
|
|
rerunOcr,
|
|
}
|
|
}
|