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>
268 lines
9.3 KiB
TypeScript
268 lines
9.3 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import type { ImageStyle } from '@/app/(admin)/ai/ocr-kombi/types'
|
|
import type {
|
|
SessionData, ImageRegionWithState, GroundTruthStatus,
|
|
} from './ground-truth-types'
|
|
import { KLAUSUR_API } from './ground-truth-types'
|
|
|
|
export function useGroundTruthSession(sessionId: string | null) {
|
|
const [status, setStatus] = useState<GroundTruthStatus>('loading')
|
|
const [error, setError] = useState('')
|
|
const [session, setSession] = useState<SessionData | null>(null)
|
|
const [imageRegions, setImageRegions] = useState<ImageRegionWithState[]>([])
|
|
const [detecting, setDetecting] = useState(false)
|
|
const [zoom, setZoom] = useState(100)
|
|
const [syncScroll, setSyncScroll] = useState(true)
|
|
const [notes, setNotes] = useState('')
|
|
const [score, setScore] = useState<number | null>(null)
|
|
const [drawingRegion, setDrawingRegion] = useState(false)
|
|
const [dragStart, setDragStart] = useState<{ x: number; y: number } | null>(null)
|
|
const [dragEnd, setDragEnd] = useState<{ x: number; y: number } | null>(null)
|
|
const [isGroundTruth, setIsGroundTruth] = useState(false)
|
|
const [gtSaving, setGtSaving] = useState(false)
|
|
const [gtMessage, setGtMessage] = useState('')
|
|
|
|
const leftPanelRef = useRef<HTMLDivElement>(null)
|
|
const rightPanelRef = useRef<HTMLDivElement>(null)
|
|
const reconRef = useRef<HTMLDivElement>(null)
|
|
const [reconWidth, setReconWidth] = useState(0)
|
|
|
|
// Track reconstruction container width for font size calculation
|
|
useEffect(() => {
|
|
const el = reconRef.current
|
|
if (!el) return
|
|
const obs = new ResizeObserver(entries => {
|
|
for (const entry of entries) setReconWidth(entry.contentRect.width)
|
|
})
|
|
obs.observe(el)
|
|
return () => obs.disconnect()
|
|
}, [session])
|
|
|
|
const loadSessionData = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setStatus('loading')
|
|
try {
|
|
const resp = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
|
|
if (!resp.ok) throw new Error(`Failed to load session: ${resp.status}`)
|
|
const data = await resp.json()
|
|
|
|
const wordResult = data.word_result || {}
|
|
setSession({
|
|
cells: wordResult.cells || [],
|
|
columnsUsed: wordResult.columns_used || [],
|
|
imageWidth: wordResult.image_width || data.image_width || 800,
|
|
imageHeight: wordResult.image_height || data.image_height || 600,
|
|
originalImageUrl: data.original_image_url
|
|
? `${KLAUSUR_API}${data.original_image_url}`
|
|
: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/original`,
|
|
})
|
|
|
|
// Check if session has ground truth reference
|
|
const gt = data.ground_truth
|
|
setIsGroundTruth(!!gt?.build_grid_reference)
|
|
|
|
// Load existing validation data
|
|
const valResp = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/reconstruction/validation`)
|
|
if (valResp.ok) {
|
|
const valData = await valResp.json()
|
|
const validation = valData.validation
|
|
if (validation) {
|
|
setImageRegions(validation.image_regions || [])
|
|
setNotes(validation.notes || '')
|
|
setScore(validation.score ?? null)
|
|
}
|
|
}
|
|
|
|
setStatus('ready')
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
setStatus('error')
|
|
}
|
|
}, [sessionId])
|
|
|
|
// Load session data
|
|
useEffect(() => {
|
|
if (!sessionId) return
|
|
loadSessionData()
|
|
}, [sessionId, loadSessionData])
|
|
|
|
// Sync scroll between panels
|
|
const handleScroll = useCallback((source: 'left' | 'right') => {
|
|
if (!syncScroll) return
|
|
const from = source === 'left' ? leftPanelRef.current : rightPanelRef.current
|
|
const to = source === 'left' ? rightPanelRef.current : leftPanelRef.current
|
|
if (from && to) {
|
|
to.scrollTop = from.scrollTop
|
|
to.scrollLeft = from.scrollLeft
|
|
}
|
|
}, [syncScroll])
|
|
|
|
// Detect images via VLM
|
|
const handleDetectImages = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setDetecting(true)
|
|
try {
|
|
const resp = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/reconstruction/detect-images`,
|
|
{ method: 'POST' }
|
|
)
|
|
if (!resp.ok) throw new Error(`Detection failed: ${resp.status}`)
|
|
const data = await resp.json()
|
|
setImageRegions(data.regions || [])
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
} finally {
|
|
setDetecting(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
// Generate image for a region
|
|
const handleGenerateImage = useCallback(async (index: number) => {
|
|
if (!sessionId) return
|
|
const region = imageRegions[index]
|
|
if (!region) return
|
|
|
|
setImageRegions(prev => prev.map((r, i) => i === index ? { ...r, generating: true } : r))
|
|
|
|
try {
|
|
const resp = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/reconstruction/generate-image`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
region_index: index,
|
|
prompt: region.prompt,
|
|
style: region.style,
|
|
}),
|
|
}
|
|
)
|
|
if (!resp.ok) throw new Error(`Generation failed: ${resp.status}`)
|
|
const data = await resp.json()
|
|
|
|
setImageRegions(prev => prev.map((r, i) =>
|
|
i === index ? { ...r, image_b64: data.image_b64, generating: false } : r
|
|
))
|
|
} catch (e) {
|
|
setImageRegions(prev => prev.map((r, i) => i === index ? { ...r, generating: false } : r))
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
}
|
|
}, [sessionId, imageRegions])
|
|
|
|
// Save validation
|
|
const handleSave = useCallback(async () => {
|
|
if (!sessionId) {
|
|
setError('Keine Session-ID vorhanden')
|
|
return
|
|
}
|
|
setStatus('saving')
|
|
setError('')
|
|
try {
|
|
const resp = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/reconstruction/validate`,
|
|
{
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ notes, score: score ?? 0 }),
|
|
}
|
|
)
|
|
if (!resp.ok) {
|
|
const body = await resp.text().catch(() => '')
|
|
throw new Error(`Speichern fehlgeschlagen (${resp.status}): ${body}`)
|
|
}
|
|
setStatus('saved')
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
setStatus('ready')
|
|
}
|
|
}, [sessionId, notes, score])
|
|
|
|
// Mark/update ground truth reference
|
|
const handleMarkGroundTruth = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setGtSaving(true)
|
|
setGtMessage('')
|
|
try {
|
|
const resp = await fetch(
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/mark-ground-truth?pipeline=ocr-pipeline`,
|
|
{ method: 'POST' }
|
|
)
|
|
if (!resp.ok) {
|
|
const body = await resp.text().catch(() => '')
|
|
throw new Error(`Ground Truth fehlgeschlagen (${resp.status}): ${body}`)
|
|
}
|
|
const data = await resp.json()
|
|
setIsGroundTruth(true)
|
|
setGtMessage(`Ground Truth gespeichert (${data.cells_saved} Zellen)`)
|
|
setTimeout(() => setGtMessage(''), 5000)
|
|
} catch (e) {
|
|
setGtMessage(e instanceof Error ? e.message : String(e))
|
|
} finally {
|
|
setGtSaving(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
// Handle manual region drawing on reconstruction
|
|
const handleReconMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!drawingRegion) return
|
|
const rect = e.currentTarget.getBoundingClientRect()
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100
|
|
setDragStart({ x, y })
|
|
setDragEnd({ x, y })
|
|
}, [drawingRegion])
|
|
|
|
const handleReconMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
|
if (!dragStart) return
|
|
const rect = e.currentTarget.getBoundingClientRect()
|
|
const x = ((e.clientX - rect.left) / rect.width) * 100
|
|
const y = ((e.clientY - rect.top) / rect.height) * 100
|
|
setDragEnd({ x, y })
|
|
}, [dragStart])
|
|
|
|
const handleReconMouseUp = useCallback(() => {
|
|
if (!dragStart || !dragEnd) return
|
|
const x = Math.min(dragStart.x, dragEnd.x)
|
|
const y = Math.min(dragStart.y, dragEnd.y)
|
|
const w = Math.abs(dragEnd.x - dragStart.x)
|
|
const h = Math.abs(dragEnd.y - dragStart.y)
|
|
|
|
if (w > 2 && h > 2) {
|
|
setImageRegions(prev => [...prev, {
|
|
bbox_pct: { x, y, w, h },
|
|
prompt: '',
|
|
description: 'Manually selected region',
|
|
image_b64: null,
|
|
style: 'educational' as ImageStyle,
|
|
}])
|
|
}
|
|
|
|
setDragStart(null)
|
|
setDragEnd(null)
|
|
setDrawingRegion(false)
|
|
}, [dragStart, dragEnd])
|
|
|
|
const handleRemoveRegion = useCallback((index: number) => {
|
|
setImageRegions(prev => prev.filter((_, i) => i !== index))
|
|
}, [])
|
|
|
|
return {
|
|
// State
|
|
status, error, session, imageRegions, detecting, zoom, syncScroll,
|
|
notes, score, drawingRegion, dragStart, dragEnd,
|
|
isGroundTruth, gtSaving, gtMessage, reconWidth,
|
|
// Refs
|
|
leftPanelRef, rightPanelRef, reconRef,
|
|
// Setters
|
|
setError, setZoom, setSyncScroll, setNotes, setScore,
|
|
setDrawingRegion, setImageRegions,
|
|
// Handlers
|
|
loadSessionData, handleScroll, handleDetectImages, handleGenerateImage,
|
|
handleSave, handleMarkGroundTruth,
|
|
handleReconMouseDown, handleReconMouseMove, handleReconMouseUp,
|
|
handleRemoveRegion,
|
|
}
|
|
}
|