import { useCallback, useRef, useState } from 'react' import type { StructuredGrid, GridZone } from './types' const KLAUSUR_API = '/klausur-api' const MAX_UNDO = 50 export interface GridEditorState { grid: StructuredGrid | null loading: boolean saving: boolean error: string | null dirty: boolean selectedCell: string | null selectedZone: number | null } export function useGridEditor(sessionId: string | null) { const [grid, setGrid] = useState(null) const [loading, setLoading] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [dirty, setDirty] = useState(false) const [selectedCell, setSelectedCell] = useState(null) const [selectedZone, setSelectedZone] = useState(null) // Undo/redo stacks store serialized zone arrays const undoStack = useRef([]) const redoStack = useRef([]) const pushUndo = useCallback((zones: GridZone[]) => { undoStack.current.push(JSON.stringify(zones)) if (undoStack.current.length > MAX_UNDO) { undoStack.current.shift() } redoStack.current = [] }, []) // ------------------------------------------------------------------ // Load / Build // ------------------------------------------------------------------ const buildGrid = useCallback(async () => { if (!sessionId) return setLoading(true) setError(null) try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/build-grid`, { method: 'POST' }, ) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.detail || `HTTP ${res.status}`) } const data: StructuredGrid = await res.json() setGrid(data) setDirty(false) undoStack.current = [] redoStack.current = [] } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setLoading(false) } }, [sessionId]) const loadGrid = useCallback(async () => { if (!sessionId) return setLoading(true) setError(null) try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/grid-editor`, ) if (res.status === 404) { // No grid yet — build it await buildGrid() return } if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.detail || `HTTP ${res.status}`) } const data: StructuredGrid = await res.json() setGrid(data) setDirty(false) undoStack.current = [] redoStack.current = [] } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setLoading(false) } }, [sessionId, buildGrid]) // ------------------------------------------------------------------ // Save // ------------------------------------------------------------------ const saveGrid = useCallback(async () => { if (!sessionId || !grid) return setSaving(true) try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/save-grid`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(grid), }, ) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.detail || `HTTP ${res.status}`) } setDirty(false) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setSaving(false) } }, [sessionId, grid]) // ------------------------------------------------------------------ // Cell editing // ------------------------------------------------------------------ const updateCellText = useCallback( (cellId: string, newText: string) => { if (!grid) return pushUndo(grid.zones) setGrid((prev) => { if (!prev) return prev return { ...prev, zones: prev.zones.map((zone) => ({ ...zone, cells: zone.cells.map((cell) => cell.cell_id === cellId ? { ...cell, text: newText } : cell, ), })), } }) setDirty(true) }, [grid, pushUndo], ) // ------------------------------------------------------------------ // Column formatting // ------------------------------------------------------------------ const toggleColumnBold = useCallback( (zoneIndex: number, colIndex: number) => { if (!grid) return pushUndo(grid.zones) setGrid((prev) => { if (!prev) return prev return { ...prev, zones: prev.zones.map((zone) => { if (zone.zone_index !== zoneIndex) return zone const col = zone.columns.find((c) => c.index === colIndex) const newBold = col ? !col.bold : true return { ...zone, columns: zone.columns.map((c) => c.index === colIndex ? { ...c, bold: newBold } : c, ), cells: zone.cells.map((cell) => cell.col_index === colIndex ? { ...cell, is_bold: newBold } : cell, ), } }), } }) setDirty(true) }, [grid, pushUndo], ) // ------------------------------------------------------------------ // Row formatting // ------------------------------------------------------------------ const toggleRowHeader = useCallback( (zoneIndex: number, rowIndex: number) => { if (!grid) return pushUndo(grid.zones) setGrid((prev) => { if (!prev) return prev return { ...prev, zones: prev.zones.map((zone) => { if (zone.zone_index !== zoneIndex) return zone return { ...zone, rows: zone.rows.map((r) => r.index === rowIndex ? { ...r, is_header: !r.is_header } : r, ), } }), } }) setDirty(true) }, [grid, 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) { const cell = zone.cells.find((c) => c.cell_id === cellId) if (!cell) continue let targetRow = cell.row_index let targetCol = cell.col_index if (direction === 'up') targetRow-- if (direction === 'down') targetRow++ if (direction === 'left') targetCol-- if (direction === 'right') targetCol++ const target = zone.cells.find( (c) => c.row_index === targetRow && c.col_index === targetCol, ) return target?.cell_id ?? null } return null }, [grid], ) return { grid, loading, saving, error, dirty, selectedCell, selectedZone, setSelectedCell, setSelectedZone, buildGrid, loadGrid, saveGrid, updateCellText, toggleColumnBold, toggleRowHeader, undo, redo, canUndo, canRedo, getAdjacentCell, } }