'use client' import { useCallback, useEffect, useState, useRef } from 'react' import { useSearchParams } from 'next/navigation' import type { PipelineStep, DocumentCategory } from './types' import { KOMBI_V2_STEPS, dbStepToKombiV2Ui } from './types' import type { SubSession, SessionListItem } from '../ocr-pipeline/types' export type { SessionListItem } const KLAUSUR_API = '/klausur-api' /** Groups sessions by document_group_id for the session list */ export interface DocumentGroupView { group_id: string title: string sessions: SessionListItem[] page_count: number } function initSteps(): PipelineStep[] { return KOMBI_V2_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending', })) } export function useKombiPipeline() { const [currentStep, setCurrentStep] = useState(0) const [sessionId, setSessionId] = useState(null) const [sessionName, setSessionName] = useState('') const [sessions, setSessions] = useState([]) const [loadingSessions, setLoadingSessions] = useState(true) const [activeCategory, setActiveCategory] = useState(undefined) const [isGroundTruth, setIsGroundTruth] = useState(false) const [subSessions, setSubSessions] = useState([]) const [parentSessionId, setParentSessionId] = useState(null) const [steps, setSteps] = useState(initSteps()) const searchParams = useSearchParams() const deepLinkHandled = useRef(false) const gridSaveRef = useRef<(() => Promise) | null>(null) // ---- Session loading ---- 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 || []).filter((s: SessionListItem) => !s.parent_session_id)) } } catch (e) { console.error('Failed to load sessions:', e) } finally { setLoadingSessions(false) } }, []) useEffect(() => { loadSessions() }, [loadSessions]) // ---- Group sessions by document_group_id ---- const groupedSessions = useCallback((): (SessionListItem | DocumentGroupView)[] => { const groups = new Map() const ungrouped: SessionListItem[] = [] for (const s of sessions) { if (s.document_group_id) { const existing = groups.get(s.document_group_id) || [] existing.push(s) groups.set(s.document_group_id, existing) } else { ungrouped.push(s) } } const result: (SessionListItem | DocumentGroupView)[] = [] // Sort groups by earliest created_at const sortedGroups = Array.from(groups.entries()).sort((a, b) => { const aTime = Math.min(...a[1].map(s => new Date(s.created_at).getTime())) const bTime = Math.min(...b[1].map(s => new Date(s.created_at).getTime())) return bTime - aTime }) for (const [groupId, groupSessions] of sortedGroups) { groupSessions.sort((a, b) => (a.page_number || 0) - (b.page_number || 0)) // Extract base title (remove " — S. X" suffix) const baseName = groupSessions[0]?.name?.replace(/ — S\. \d+$/, '') || 'Dokument' result.push({ group_id: groupId, title: baseName, sessions: groupSessions, page_count: groupSessions.length, }) } for (const s of ungrouped) { result.push(s) } // Sort by creation time (most recent first) const getTime = (item: SessionListItem | DocumentGroupView): number => { if ('group_id' in item) { return Math.min(...item.sessions.map((s: SessionListItem) => new Date(s.created_at).getTime())) } return new Date(item.created_at).getTime() } result.sort((a, b) => getTime(b) - getTime(a)) return result }, [sessions]) // ---- Open session ---- 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 const data = await res.json() setSessionId(sid) setSessionName(data.name || data.filename || '') setActiveCategory(data.document_category || undefined) setIsGroundTruth(!!data.ground_truth?.build_grid_reference) // Sub-session handling if (data.sub_sessions?.length > 0) { setSubSessions(data.sub_sessions) setParentSessionId(sid) } else if (data.parent_session_id) { setParentSessionId(data.parent_session_id) } else if (!keepSubSessions) { setSubSessions([]) setParentSessionId(null) } // Determine UI step from DB state const dbStep = data.current_step || 1 const hasGrid = !!data.grid_editor_result const hasStructure = !!data.structure_result const hasWords = !!data.word_result let uiStep: number if (hasGrid) { uiStep = 9 // grid-review } else if (hasStructure) { uiStep = 8 // grid-build } else if (hasWords) { uiStep = 7 // structure } else { uiStep = dbStepToKombiV2Ui(dbStep) } // Sessions only exist after upload, so always skip the upload step if (uiStep === 0) { uiStep = 1 } const skipIds: string[] = [] const isSubSession = !!data.parent_session_id if (isSubSession && dbStep >= 5) { skipIds.push('upload', 'orientation', 'page-split', 'deskew', 'dewarp', 'content-crop') if (uiStep < 6) uiStep = 6 } else if (isSubSession && dbStep >= 2) { skipIds.push('upload', 'orientation') if (uiStep < 2) uiStep = 2 } setSteps( KOMBI_V2_STEPS.map((s, i) => ({ ...s, status: skipIds.includes(s.id) ? 'skipped' : i < uiStep ? 'completed' : i === uiStep ? 'active' : 'pending', })), ) setCurrentStep(uiStep) } catch (e) { console.error('Failed to open session:', e) } }, []) // ---- Deep link handling ---- useEffect(() => { if (deepLinkHandled.current) return const urlSession = searchParams.get('session') const urlStep = searchParams.get('step') if (urlSession) { deepLinkHandled.current = true openSession(urlSession).then(() => { if (urlStep) { const stepIdx = parseInt(urlStep, 10) if (!isNaN(stepIdx) && stepIdx >= 0 && stepIdx < KOMBI_V2_STEPS.length) { setCurrentStep(stepIdx) } } }) } }, [searchParams, openSession]) // ---- Step navigation ---- const goToStep = useCallback((step: number) => { setCurrentStep(step) setSteps(prev => prev.map((s, i) => ({ ...s, status: i < step ? 'completed' : i === step ? 'active' : 'pending', })), ) }, []) const handleStepClick = useCallback((index: number) => { if (index <= currentStep || steps[index].status === 'completed') { setCurrentStep(index) } }, [currentStep, steps]) const handleNext = useCallback(() => { if (currentStep >= steps.length - 1) { // Last step → return to session list setSteps(initSteps()) setCurrentStep(0) setSessionId(null) setSubSessions([]) setParentSessionId(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) }, [currentStep, steps, loadSessions]) // ---- Session CRUD ---- const handleNewSession = useCallback(() => { setSessionId(null) setSessionName('') setCurrentStep(0) setSubSessions([]) setParentSessionId(null) setSteps(initSteps()) }, []) 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) handleNewSession() } catch (e) { console.error('Failed to delete session:', e) } }, [sessionId, handleNewSession]) 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) } }, [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) } }, [sessionId]) // ---- Orientation completion (checks for page-split sub-sessions) ---- const handleOrientationComplete = useCallback(async (sid: string) => { setSessionId(sid) loadSessions() try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`) if (res.ok) { const data = await res.json() if (data.sub_sessions?.length > 0) { const subs: SubSession[] = data.sub_sessions.map((s: SubSession) => ({ id: s.id, name: s.name, box_index: s.box_index, current_step: s.current_step, })) setSubSessions(subs) setParentSessionId(sid) openSession(subs[0].id, true) return } } } catch (e) { console.error('Failed to check for sub-sessions:', e) } handleNext() }, [loadSessions, openSession, handleNext]) const handleSessionChange = useCallback((newSessionId: string) => { openSession(newSessionId, true) }, [openSession]) return { // State currentStep, sessionId, sessionName, sessions, loadingSessions, activeCategory, isGroundTruth, subSessions, parentSessionId, steps, gridSaveRef, // Computed groupedSessions, // Actions loadSessions, openSession, goToStep, handleStepClick, handleNext, handleNewSession, deleteSession, renameSession, updateCategory, handleOrientationComplete, handleSessionChange, setSessionId, setSubSessions, setParentSessionId, setSessionName, setIsGroundTruth, } }