'use client' import { useCallback, useEffect, useState } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' import { PipelineStepper } from '@/components/ocr-pipeline/PipelineStepper' import { StepOrientation } from '@/components/ocr-pipeline/StepOrientation' import { StepDeskew } from '@/components/ocr-pipeline/StepDeskew' import { StepDewarp } from '@/components/ocr-pipeline/StepDewarp' import { StepCrop } from '@/components/ocr-pipeline/StepCrop' import { StepRowDetection } from '@/components/ocr-pipeline/StepRowDetection' import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecognition' import { OverlayReconstruction } from '@/components/ocr-overlay/OverlayReconstruction' import { PaddleDirectStep } from '@/components/ocr-overlay/PaddleDirectStep' import { GridEditor } from '@/components/grid-editor/GridEditor' import { OVERLAY_PIPELINE_STEPS, PADDLE_DIRECT_STEPS, KOMBI_STEPS, DOCUMENT_CATEGORIES, dbStepToOverlayUi, type PipelineStep, type SessionListItem, type DocumentCategory } from './types' const KLAUSUR_API = '/klausur-api' export default function OcrOverlayPage() { const [mode, setMode] = useState<'pipeline' | 'paddle-direct' | 'kombi'>('pipeline') const [currentStep, setCurrentStep] = useState(0) const [sessionId, setSessionId] = useState(null) const [sessionName, setSessionName] = useState('') const [sessions, setSessions] = useState([]) const [loadingSessions, setLoadingSessions] = useState(true) const [editingName, setEditingName] = useState(null) const [editNameValue, setEditNameValue] = useState('') const [editingCategory, setEditingCategory] = useState(null) const [activeCategory, setActiveCategory] = useState(undefined) const [steps, setSteps] = useState( OVERLAY_PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending', })), ) useEffect(() => { loadSessions() }, []) const loadSessions = async () => { setLoadingSessions(true) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`) if (res.ok) { const data = await res.json() // Filter to only show top-level sessions (no sub-sessions) setSessions((data.sessions || []).filter((s: SessionListItem) => !s.parent_session_id)) } } catch (e) { console.error('Failed to load sessions:', e) } finally { setLoadingSessions(false) } } const openSession = useCallback(async (sid: string) => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`) if (!res.ok) return const data = await res.json() setSessionId(sid) setSessionName(data.name || data.filename || '') setActiveCategory(data.document_category || undefined) // Check if this session was processed with paddle_direct, kombi, or rapid_kombi const ocrEngine = data.word_result?.ocr_engine const isPaddleDirect = ocrEngine === 'paddle_direct' const isKombi = ocrEngine === 'kombi' || ocrEngine === 'rapid_kombi' if (isPaddleDirect || isKombi) { const m = isKombi ? 'kombi' : 'paddle-direct' const baseSteps = isKombi ? KOMBI_STEPS : PADDLE_DIRECT_STEPS setMode(m) // For Kombi: if grid_editor_result exists, jump to grid editor step (5) const hasGrid = isKombi && data.grid_editor_result const activeStep = hasGrid ? 5 : 4 setSteps( baseSteps.map((s, i) => ({ ...s, status: i < activeStep ? 'completed' : i === activeStep ? 'active' : 'pending', })), ) setCurrentStep(activeStep) } else { setMode('pipeline') // Map DB step to overlay UI step const dbStep = data.current_step || 1 const uiStep = dbStepToOverlayUi(dbStep) setSteps( OVERLAY_PIPELINE_STEPS.map((s, i) => ({ ...s, status: i < uiStep ? 'completed' : i === uiStep ? 'active' : 'pending', })), ) setCurrentStep(uiStep) } } catch (e) { console.error('Failed to open session:', e) } }, []) const deleteSession = useCallback(async (sid: string) => { try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`, { method: 'DELETE' }) setSessions((prev) => prev.filter((s) => s.id !== sid)) if (sessionId === sid) { setSessionId(null) setCurrentStep(0) const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) } } catch (e) { console.error('Failed to delete session:', e) } }, [sessionId, mode]) const renameSession = useCallback(async (sid: string, newName: string) => { try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }), }) setSessions((prev) => prev.map((s) => (s.id === sid ? { ...s, name: newName } : s))) if (sessionId === sid) setSessionName(newName) } catch (e) { console.error('Failed to rename session:', e) } setEditingName(null) }, [sessionId]) const updateCategory = useCallback(async (sid: string, category: DocumentCategory) => { try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ document_category: category }), }) setSessions((prev) => prev.map((s) => (s.id === sid ? { ...s, document_category: category } : s))) if (sessionId === sid) setActiveCategory(category) } catch (e) { console.error('Failed to update category:', e) } setEditingCategory(null) }, [sessionId]) const handleStepClick = (index: number) => { if (index <= currentStep || steps[index].status === 'completed') { setCurrentStep(index) } } const goToStep = (step: number) => { setCurrentStep(step) setSteps((prev) => prev.map((s, i) => ({ ...s, status: i < step ? 'completed' : i === step ? 'active' : 'pending', })), ) } const handleNext = () => { if (currentStep >= steps.length - 1) { // Last step completed — return to session list const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) setCurrentStep(0) setSessionId(null) loadSessions() return } const nextStep = currentStep + 1 setSteps((prev) => prev.map((s, i) => { if (i === currentStep) return { ...s, status: 'completed' } if (i === nextStep) return { ...s, status: 'active' } return s }), ) setCurrentStep(nextStep) } const handleOrientationComplete = (sid: string) => { setSessionId(sid) loadSessions() handleNext() } const handleNewSession = () => { setSessionId(null) setSessionName('') setCurrentStep(0) const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) } const stepNames: Record = { 1: 'Orientierung', 2: 'Begradigung', 3: 'Entzerrung', 4: 'Zuschneiden', 5: 'Zeilen', 6: 'Woerter', 7: 'Overlay', } const reprocessFromStep = useCallback(async (uiStep: number) => { if (!sessionId) return // Map overlay UI step to DB step const dbStepMap: Record = { 0: 2, 1: 3, 2: 4, 3: 5, 4: 7, 5: 8, 6: 9 } const dbStep = dbStepMap[uiStep] || uiStep + 1 if (!confirm(`Ab Schritt ${uiStep + 1} (${stepNames[uiStep + 1] || '?'}) neu verarbeiten?`)) return try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/reprocess`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ from_step: dbStep }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) console.error('Reprocess failed:', data.detail || res.status) return } goToStep(uiStep) } catch (e) { console.error('Reprocess error:', e) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [sessionId, goToStep]) const renderStep = () => { if (mode === 'paddle-direct' || mode === 'kombi') { switch (currentStep) { case 0: return case 1: return case 2: return case 3: return case 4: return mode === 'kombi' ? ( ) : ( ) case 5: return mode === 'kombi' ? ( ) : null default: return null } } switch (currentStep) { case 0: return case 1: return case 2: return case 3: return case 4: return case 5: return case 6: return default: return null } } return (
{/* Session List */}

Sessions ({sessions.length})

{loadingSessions ? (
Lade Sessions...
) : sessions.length === 0 ? (
Noch keine Sessions vorhanden.
) : (
{sessions.map((s) => { const catInfo = DOCUMENT_CATEGORIES.find(c => c.value === s.document_category) return (
{/* Thumbnail */}
openSession(s.id)} > {/* eslint-disable-next-line @next/next/no-img-element */} { (e.target as HTMLImageElement).style.display = 'none' }} />
{/* Info */}
openSession(s.id)}> {editingName === s.id ? ( setEditNameValue(e.target.value)} onBlur={() => renameSession(s.id, editNameValue)} onKeyDown={(e) => { if (e.key === 'Enter') renameSession(s.id, editNameValue) if (e.key === 'Escape') setEditingName(null) }} onClick={(e) => e.stopPropagation()} className="w-full px-1 py-0.5 text-sm border rounded dark:bg-gray-700 dark:border-gray-600" /> ) : (
{s.name || s.filename}
)}
{new Date(s.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })}
{/* Category Badge */}
e.stopPropagation()}>
{/* Actions */}
{/* Category dropdown */} {editingCategory === s.id && (
e.stopPropagation()} > {DOCUMENT_CATEGORIES.map((cat) => ( ))}
)}
) })}
)}
{/* Active session info */} {sessionId && sessionName && (
Aktive Session: {sessionName} {activeCategory && (() => { const cat = DOCUMENT_CATEGORIES.find(c => c.value === activeCategory) return cat ? {cat.icon} {cat.label} : null })()}
)} {/* Mode Toggle */}
{renderStep()}
) }