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>
242 lines
8.0 KiB
TypeScript
242 lines
8.0 KiB
TypeScript
import { useCallback, useEffect, useRef } from 'react'
|
|
import type { StructuredGrid } from './types'
|
|
import type { IpaMode, SyllableMode } from './gridEditorTypes'
|
|
import { KLAUSUR_API } from './gridEditorTypes'
|
|
|
|
interface ApiDeps {
|
|
sessionId: string | null
|
|
ipaMode: IpaMode
|
|
syllableMode: SyllableMode
|
|
ocrEnhance: boolean
|
|
ocrMaxCols: number
|
|
ocrMinConf: number
|
|
visionFusion: boolean
|
|
documentCategory: string
|
|
setGrid: React.Dispatch<React.SetStateAction<StructuredGrid | null>>
|
|
setLoading: React.Dispatch<React.SetStateAction<boolean>>
|
|
setSaving: React.Dispatch<React.SetStateAction<boolean>>
|
|
setError: React.Dispatch<React.SetStateAction<string | null>>
|
|
setDirty: React.Dispatch<React.SetStateAction<boolean>>
|
|
grid: StructuredGrid | null
|
|
undoStack: React.MutableRefObject<string[]>
|
|
redoStack: React.MutableRefObject<string[]>
|
|
}
|
|
|
|
export function useGridEditorApi(deps: ApiDeps) {
|
|
const {
|
|
sessionId,
|
|
ipaMode,
|
|
syllableMode,
|
|
ocrEnhance,
|
|
ocrMaxCols,
|
|
ocrMinConf,
|
|
visionFusion,
|
|
documentCategory,
|
|
setGrid,
|
|
setLoading,
|
|
setSaving,
|
|
setError,
|
|
setDirty,
|
|
grid,
|
|
undoStack,
|
|
redoStack,
|
|
} = deps
|
|
|
|
// ------------------------------------------------------------------
|
|
// Build grid
|
|
// ------------------------------------------------------------------
|
|
|
|
const buildGrid = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const params = new URLSearchParams()
|
|
params.set('ipa_mode', ipaMode)
|
|
params.set('syllable_mode', syllableMode)
|
|
params.set('enhance', String(ocrEnhance))
|
|
if (ocrMaxCols > 0) params.set('max_cols', String(ocrMaxCols))
|
|
if (ocrMinConf > 0) params.set('min_conf', String(ocrMinConf))
|
|
const res = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/build-grid?${params}`,
|
|
{ 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, ipaMode, syllableMode, ocrEnhance, ocrMaxCols, ocrMinConf, setGrid, setLoading, setError, setDirty, undoStack, redoStack])
|
|
|
|
// ------------------------------------------------------------------
|
|
// Re-run OCR with current quality settings, then rebuild grid
|
|
// ------------------------------------------------------------------
|
|
|
|
const rerunOcr = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const params = new URLSearchParams()
|
|
params.set('ipa_mode', ipaMode)
|
|
params.set('syllable_mode', syllableMode)
|
|
params.set('enhance', String(ocrEnhance))
|
|
if (ocrMaxCols > 0) params.set('max_cols', String(ocrMaxCols))
|
|
if (ocrMinConf > 0) params.set('min_conf', String(ocrMinConf))
|
|
params.set('vision_fusion', String(visionFusion))
|
|
if (documentCategory) params.set('doc_category', documentCategory)
|
|
const res = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/rerun-ocr-and-build-grid?${params}`,
|
|
{ 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, ipaMode, syllableMode, ocrEnhance, ocrMaxCols, ocrMinConf, visionFusion, documentCategory, setGrid, setLoading, setError, setDirty, undoStack, redoStack])
|
|
|
|
// ------------------------------------------------------------------
|
|
// Load grid (with auto-build fallback on 404)
|
|
// ------------------------------------------------------------------
|
|
|
|
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 with current modes
|
|
const params = new URLSearchParams()
|
|
params.set('ipa_mode', ipaMode)
|
|
params.set('syllable_mode', syllableMode)
|
|
params.set('enhance', String(ocrEnhance))
|
|
if (ocrMaxCols > 0) params.set('max_cols', String(ocrMaxCols))
|
|
if (ocrMinConf > 0) params.set('min_conf', String(ocrMinConf))
|
|
const buildRes = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/build-grid?${params}`,
|
|
{ method: 'POST' },
|
|
)
|
|
if (buildRes.ok) {
|
|
const data: StructuredGrid = await buildRes.json()
|
|
setGrid(data)
|
|
setDirty(false)
|
|
}
|
|
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)
|
|
}
|
|
// Only depends on sessionId — mode changes are handled by the
|
|
// separate useEffect below, not by re-triggering loadGrid.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sessionId])
|
|
|
|
// ------------------------------------------------------------------
|
|
// Auto-rebuild when IPA or syllable mode changes (skip initial mount)
|
|
// ------------------------------------------------------------------
|
|
|
|
const mountedRef = useRef(false)
|
|
useEffect(() => {
|
|
if (!mountedRef.current) {
|
|
// Skip the first trigger (component mount) — don't rebuild yet
|
|
mountedRef.current = true
|
|
return
|
|
}
|
|
if (!sessionId) return
|
|
const rebuild = async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const params = new URLSearchParams()
|
|
params.set('ipa_mode', ipaMode)
|
|
params.set('syllable_mode', syllableMode)
|
|
const res = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/build-grid?${params}`,
|
|
{ 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)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
rebuild()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [ipaMode, syllableMode])
|
|
|
|
// ------------------------------------------------------------------
|
|
// Save grid
|
|
// ------------------------------------------------------------------
|
|
|
|
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, setSaving, setError, setDirty])
|
|
|
|
return {
|
|
buildGrid,
|
|
rerunOcr,
|
|
loadGrid,
|
|
saveGrid,
|
|
}
|
|
}
|