'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 { 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 [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) => { 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) // 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 } setSteps( KOMBI_V2_STEPS.map((s, i) => ({ ...s, status: 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) 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) 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]) return { // State currentStep, sessionId, sessionName, sessions, loadingSessions, activeCategory, isGroundTruth, steps, gridSaveRef, // Computed groupedSessions, // Actions loadSessions, openSession, goToStep, handleStepClick, handleNext, handleNewSession, deleteSession, renameSession, updateCategory, setSessionId, setSessionName, setIsGroundTruth, } }