'use client' import { Suspense, 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 { StepCrop } from '@/components/ocr-pipeline/StepCrop' import { StepDeskew } from '@/components/ocr-pipeline/StepDeskew' import { StepDewarp } from '@/components/ocr-pipeline/StepDewarp' import { StepStructureDetection } from '@/components/ocr-pipeline/StepStructureDetection' import { StepColumnDetection } from '@/components/ocr-pipeline/StepColumnDetection' import { StepRowDetection } from '@/components/ocr-pipeline/StepRowDetection' import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecognition' import { StepLlmReview } from '@/components/ocr-pipeline/StepLlmReview' import { StepReconstruction } from '@/components/ocr-pipeline/StepReconstruction' import { StepGroundTruth } from '@/components/ocr-pipeline/StepGroundTruth' import { DOCUMENT_CATEGORIES, type SessionListItem, type DocumentTypeResult, type DocumentCategory, type SubSession } from './types' import { usePipelineNavigation } from './usePipelineNavigation' const KLAUSUR_API = '/klausur-api' const STEP_NAMES: Record = { 1: 'Orientierung', 2: 'Begradigung', 3: 'Entzerrung', 4: 'Zuschneiden', 5: 'Spalten', 6: 'Zeilen', 7: 'Woerter', 8: 'Struktur', 9: 'Korrektur', 10: 'Rekonstruktion', 11: 'Validierung', } function OcrPipelineContent() { const nav = usePipelineNavigation() const [sessions, setSessions] = useState([]) const [loadingSessions, setLoadingSessions] = useState(true) const [editingName, setEditingName] = useState(null) const [editNameValue, setEditNameValue] = useState('') const [editingCategory, setEditingCategory] = useState(null) const [sessionName, setSessionName] = useState('') const [activeCategory, setActiveCategory] = useState(undefined) const loadSessions = useCallback(async () => { setLoadingSessions(true) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`) if (res.ok) { const data = await res.json() setSessions(data.sessions || []) } } catch (e) { console.error('Failed to load sessions:', e) } finally { setLoadingSessions(false) } }, []) useEffect(() => { loadSessions() }, [loadSessions]) // Sync session name when nav.sessionId changes useEffect(() => { if (!nav.sessionId) { setSessionName('') setActiveCategory(undefined) return } const load = async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${nav.sessionId}`) if (!res.ok) return const data = await res.json() setSessionName(data.name || data.filename || '') setActiveCategory(data.document_category || undefined) } catch { /* ignore */ } } load() }, [nav.sessionId]) const openSession = useCallback((sid: string) => { nav.goToSession(sid) }, [nav]) 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 (nav.sessionId === sid) nav.goToSessionList() } catch (e) { console.error('Failed to delete session:', e) } }, [nav]) 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 (nav.sessionId === sid) setSessionName(newName) } catch (e) { console.error('Failed to rename session:', e) } setEditingName(null) }, [nav.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 (nav.sessionId === sid) setActiveCategory(category) } catch (e) { console.error('Failed to update category:', e) } setEditingCategory(null) }, [nav.sessionId]) const deleteAllSessions = useCallback(async () => { if (!confirm('Alle Sessions loeschen? Dies kann nicht rueckgaengig gemacht werden.')) return try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`, { method: 'DELETE' }) setSessions([]) nav.goToSessionList() } catch (e) { console.error('Failed to delete all sessions:', e) } }, [nav]) const handleStepClick = (index: number) => { if (index <= nav.currentStepIndex || nav.steps[index].status === 'completed') { nav.goToStep(index) } } // Orientation: after upload, navigate to session at deskew step const handleOrientationComplete = useCallback(async (sid: string) => { loadSessions() // Navigate directly to deskew step (index 1) for this session nav.goToSession(sid) }, [nav, loadSessions]) // Crop: detect doc type then advance const handleCropNext = useCallback(async () => { if (nav.sessionId) { try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${nav.sessionId}/detect-type`, { method: 'POST' }, ) if (res.ok) { const data: DocumentTypeResult = await res.json() nav.setDocType(data) } } catch (e) { console.error('Doc type detection failed:', e) } } nav.goToNextStep() }, [nav]) const handleDocTypeChange = (newDocType: DocumentTypeResult['doc_type']) => { if (!nav.docTypeResult) return let skipSteps: string[] = [] if (newDocType === 'full_text') skipSteps = ['columns', 'rows'] nav.setDocType({ ...nav.docTypeResult, doc_type: newDocType, skip_steps: skipSteps, pipeline: newDocType === 'full_text' ? 'full_page' : 'cell_first', }) } // Box sub-sessions (column detection) — still supported const handleBoxSessionsCreated = useCallback((_subs: SubSession[]) => { // Box sub-sessions are tracked by the backend; no client-side state needed anymore }, []) const renderStep = () => { const sid = nav.sessionId switch (nav.currentStepIndex) { case 0: return ( { loadSessions(); nav.goToSessionList() }} /> ) case 1: return case 2: return case 3: return case 4: return case 5: return case 6: return case 7: return case 8: return case 9: return case 10: return default: return null } } return (
{/* Session List */}

Sessions ({sessions.length})

{sessions.length > 0 && ( )}
{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}
)} {/* ID row */}
{new Date(s.created_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })} Schritt {s.current_step}: {STEP_NAMES[s.current_step] || '?'}
{/* Badges */}
e.stopPropagation()}> {s.doc_type && ( {s.doc_type} )}
{/* Action buttons */}
{/* Category dropdown */} {editingCategory === s.id && (
e.stopPropagation()} > {DOCUMENT_CATEGORIES.map((cat) => ( ))}
)}
) })}
)}
{/* Active session info */} {nav.sessionId && sessionName && (
Aktive Session: {sessionName} {activeCategory && (() => { const cat = DOCUMENT_CATEGORIES.find(c => c.value === activeCategory) return cat ? {cat.icon} {cat.label} : null })()} {nav.docTypeResult && ( {nav.docTypeResult.doc_type} )}
)}
{renderStep()}
) } export default function OcrPipelinePage() { return ( Lade Pipeline...}> ) }