diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx index a08de58..c69c58a 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx +++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/page.tsx @@ -13,7 +13,8 @@ import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecogniti import { StepLlmReview } from '@/components/ocr-pipeline/StepLlmReview' import { StepReconstruction } from '@/components/ocr-pipeline/StepReconstruction' import { StepGroundTruth } from '@/components/ocr-pipeline/StepGroundTruth' -import { PIPELINE_STEPS, DOCUMENT_CATEGORIES, type PipelineStep, type SessionListItem, type DocumentTypeResult, type DocumentCategory } from './types' +import { BoxSessionTabs } from '@/components/ocr-pipeline/BoxSessionTabs' +import { PIPELINE_STEPS, DOCUMENT_CATEGORIES, type PipelineStep, type SessionListItem, type DocumentTypeResult, type DocumentCategory, type SubSession } from './types' const KLAUSUR_API = '/klausur-api' @@ -28,6 +29,8 @@ export default function OcrPipelinePage() { const [editingCategory, setEditingCategory] = useState(null) const [docTypeResult, setDocTypeResult] = useState(null) const [activeCategory, setActiveCategory] = useState(undefined) + const [subSessions, setSubSessions] = useState([]) + const [parentSessionId, setParentSessionId] = useState(null) const [steps, setSteps] = useState( PIPELINE_STEPS.map((s, i) => ({ ...s, @@ -55,7 +58,7 @@ export default function OcrPipelinePage() { } } - const openSession = useCallback(async (sid: string) => { + const openSession = useCallback(async (sid: string, keepSubSessions?: boolean) => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`) if (!res.ok) return @@ -65,6 +68,18 @@ export default function OcrPipelinePage() { setSessionName(data.name || data.filename || '') setActiveCategory(data.document_category || undefined) + // Sub-session handling + if (data.sub_sessions && data.sub_sessions.length > 0) { + setSubSessions(data.sub_sessions) + setParentSessionId(sid) + } else if (data.parent_session_id) { + // This is a sub-session — keep parent info but don't reset sub-session list + setParentSessionId(data.parent_session_id) + } else if (!keepSubSessions) { + setSubSessions([]) + setParentSessionId(null) + } + // Restore doc type result if available const savedDocType: DocumentTypeResult | null = data.doc_type_result || null setDocTypeResult(savedDocType) @@ -98,6 +113,8 @@ export default function OcrPipelinePage() { setSessionId(null) setCurrentStep(0) setDocTypeResult(null) + setSubSessions([]) + setParentSessionId(null) setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) } } catch (e) { @@ -144,6 +161,8 @@ export default function OcrPipelinePage() { setCurrentStep(0) setDocTypeResult(null) setActiveCategory(undefined) + setSubSessions([]) + setParentSessionId(null) setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) } catch (e) { console.error('Failed to delete all sessions:', e) @@ -168,10 +187,22 @@ export default function OcrPipelinePage() { const handleNext = () => { if (currentStep >= steps.length - 1) { - // Last step completed — return to session list + // Last step completed + if (parentSessionId && sessionId !== parentSessionId) { + // Sub-session completed — update its status and stay in tab view + setSubSessions((prev) => + prev.map((s) => s.id === sessionId ? { ...s, status: 'completed', current_step: 10 } : s) + ) + // Switch back to parent + handleSessionChange(parentSessionId) + return + } + // Main session: return to session list setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) setCurrentStep(0) setSessionId(null) + setSubSessions([]) + setParentSessionId(null) loadSessions() return } @@ -268,9 +299,20 @@ export default function OcrPipelinePage() { setSessionName('') setCurrentStep(0) setDocTypeResult(null) + setSubSessions([]) + setParentSessionId(null) setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' }))) } + const handleSessionChange = useCallback((newSessionId: string) => { + openSession(newSessionId, true) + }, [openSession]) + + const handleBoxSessionsCreated = useCallback((subs: SubSession[]) => { + setSubSessions(subs) + if (sessionId) setParentSessionId(sessionId) + }, [sessionId]) + const stepNames: Record = { 1: 'Orientierung', 2: 'Begradigung', @@ -318,7 +360,7 @@ export default function OcrPipelinePage() { case 3: return case 4: - return + return case 5: return case 6: @@ -552,6 +594,15 @@ export default function OcrPipelinePage() { onDocTypeChange={handleDocTypeChange} /> + {subSessions.length > 0 && parentSessionId && sessionId && ( + + )} +
{renderStep()}
) diff --git a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts index b927802..b3f56a5 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts +++ b/admin-lehrer/app/(admin)/ai/ocr-pipeline/types.ts @@ -34,6 +34,16 @@ export interface SessionListItem { doc_type?: string created_at: string updated_at?: string + parent_session_id?: string | null + box_index?: number | null +} + +export interface SubSession { + id: string + name: string + box_index: number + current_step?: number + status?: string } export interface PipelineLogEntry { @@ -95,6 +105,9 @@ export interface SessionInfo { row_result?: RowResult word_result?: GridResult doc_type_result?: DocumentTypeResult + sub_sessions?: SubSession[] + parent_session_id?: string + box_index?: number } export interface DeskewResult { @@ -151,9 +164,17 @@ export interface PageRegion { classification_method?: string } +export interface PageZone { + zone_type: 'content' | 'box' + y_start: number + y_end: number + box?: { x: number; y: number; width: number; height: number } +} + export interface ColumnResult { columns: PageRegion[] duration_seconds: number + zones?: PageZone[] } export interface ColumnGroundTruth { diff --git a/admin-lehrer/components/ocr-pipeline/BoxSessionTabs.tsx b/admin-lehrer/components/ocr-pipeline/BoxSessionTabs.tsx new file mode 100644 index 0000000..aa25048 --- /dev/null +++ b/admin-lehrer/components/ocr-pipeline/BoxSessionTabs.tsx @@ -0,0 +1,68 @@ +'use client' + +import type { SubSession } from '@/app/(admin)/ai/ocr-pipeline/types' + +interface BoxSessionTabsProps { + parentSessionId: string + subSessions: SubSession[] + activeSessionId: string + onSessionChange: (sessionId: string) => void +} + +const STATUS_ICONS: Record = { + pending: '\u23F3', // hourglass + processing: '\uD83D\uDD04', // arrows + completed: '\u2713', // checkmark +} + +function getStatusIcon(sub: SubSession): string { + if (sub.status === 'completed' || (sub.current_step && sub.current_step >= 9)) return STATUS_ICONS.completed + if (sub.current_step && sub.current_step > 1) return STATUS_ICONS.processing + return STATUS_ICONS.pending +} + +export function BoxSessionTabs({ parentSessionId, subSessions, activeSessionId, onSessionChange }: BoxSessionTabsProps) { + if (subSessions.length === 0) return null + + const isParentActive = activeSessionId === parentSessionId + + return ( +
+ {/* Main session tab */} + + +
+ + {/* Sub-session tabs */} + {subSessions.map((sub) => { + const isActive = activeSessionId === sub.id + const icon = getStatusIcon(sub) + + return ( + + ) + })} +
+ ) +} diff --git a/admin-lehrer/components/ocr-pipeline/StepColumnDetection.tsx b/admin-lehrer/components/ocr-pipeline/StepColumnDetection.tsx index b707348..86ef8f3 100644 --- a/admin-lehrer/components/ocr-pipeline/StepColumnDetection.tsx +++ b/admin-lehrer/components/ocr-pipeline/StepColumnDetection.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useState } from 'react' -import type { ColumnResult, ColumnGroundTruth, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types' +import type { ColumnResult, ColumnGroundTruth, PageRegion, SubSession } from '@/app/(admin)/ai/ocr-pipeline/types' import { ColumnControls } from './ColumnControls' import { ManualColumnEditor } from './ManualColumnEditor' import type { ColumnTypeKey } from '@/app/(admin)/ai/ocr-pipeline/types' @@ -13,6 +13,7 @@ type ViewMode = 'normal' | 'ground-truth' | 'manual' interface StepColumnDetectionProps { sessionId: string | null onNext: () => void + onBoxSessionsCreated?: (subSessions: SubSession[]) => void } /** Convert PageRegion[] to divider percentages + column types for ManualColumnEditor */ @@ -34,7 +35,7 @@ function columnsToEditorState( return { dividers, columnTypes } } -export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionProps) { +export function StepColumnDetection({ sessionId, onNext, onBoxSessionsCreated }: StepColumnDetectionProps) { const [columnResult, setColumnResult] = useState(null) const [detecting, setDetecting] = useState(false) const [error, setError] = useState(null) @@ -42,6 +43,8 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr const [applying, setApplying] = useState(false) const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null) const [savedGtColumns, setSavedGtColumns] = useState(null) + const [creatingBoxSessions, setCreatingBoxSessions] = useState(false) + const [existingSubSessions, setExistingSubSessions] = useState(null) // Fetch session info (image dimensions) + check for cached column result useEffect(() => { @@ -55,6 +58,10 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr if (info.image_width && info.image_height) { setImageDimensions({ width: info.image_width, height: info.image_height }) } + if (info.sub_sessions && info.sub_sessions.length > 0) { + setExistingSubSessions(info.sub_sessions) + onBoxSessionsCreated?.(info.sub_sessions) + } if (info.column_result) { setColumnResult(info.column_result) return @@ -178,6 +185,39 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr } }, [sessionId]) + // Count box zones from column result + const boxZones = columnResult?.zones?.filter(z => z.zone_type === 'box') || [] + const boxCount = boxZones.length + + const createBoxSessions = useCallback(async () => { + if (!sessionId) return + setCreatingBoxSessions(true) + setError(null) + try { + const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/create-box-sessions`, { + method: 'POST', + }) + if (!res.ok) { + const err = await res.json().catch(() => ({ detail: res.statusText })) + throw new Error(err.detail || 'Box-Sessions konnten nicht erstellt werden') + } + const data = await res.json() + const subs: SubSession[] = data.sub_sessions.map((s: { id: string; name?: string; box_index: number }) => ({ + id: s.id, + name: s.name || `Box ${s.box_index + 1}`, + box_index: s.box_index, + current_step: 1, + status: 'pending', + })) + setExistingSubSessions(subs) + onBoxSessionsCreated?.(subs) + } catch (e) { + setError(e instanceof Error ? e.message : 'Fehler beim Erstellen der Box-Sessions') + } finally { + setCreatingBoxSessions(false) + } + }, [sessionId, onBoxSessionsCreated]) + if (!sessionId) { return (
@@ -317,6 +357,39 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
)} + {/* Box zone info */} + {viewMode === 'normal' && boxCount > 0 && ( +
+
+ 📦 +
+
+ {boxCount} Box{boxCount > 1 ? 'en' : ''} erkannt +
+
+ Box-Bereiche werden separat verarbeitet +
+
+
+ {existingSubSessions && existingSubSessions.length > 0 ? ( +
+ {existingSubSessions.length} Box-Session{existingSubSessions.length > 1 ? 's' : ''} vorhanden +
+ ) : ( + + )} +
+ )} + {/* Controls */} {viewMode === 'normal' && (