Files
breakpilot-lehrer/admin-lehrer/app/(admin)/ai/ocr-pipeline/usePipelineNavigation.ts
Benjamin Admin f931091b57 refactor: independent sessions for page-split + URL-based pipeline navigation
Page-split now creates independent sessions (no parent_session_id),
parent marked as status='split' and hidden from list. Navigation uses
useSearchParams for URL-based step tracking (browser back/forward works).
page.tsx reduced from 684 to 443 lines via usePipelineNavigation hook.

Box sub-sessions (column detection) remain unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 17:05:33 +01:00

226 lines
7.0 KiB
TypeScript

'use client'
import { useCallback, useEffect, useState } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { PIPELINE_STEPS, type PipelineStep, type PipelineStepStatus, type DocumentTypeResult } from './types'
const KLAUSUR_API = '/klausur-api'
export interface PipelineNav {
sessionId: string | null
currentStepIndex: number
currentStepId: string
steps: PipelineStep[]
docTypeResult: DocumentTypeResult | null
goToNextStep: () => void
goToStep: (index: number) => void
goToSession: (sessionId: string) => void
goToSessionList: () => void
setDocType: (result: DocumentTypeResult) => void
reprocessFromStep: (uiStep: number) => Promise<void>
}
const STEP_NAMES: Record<number, string> = {
1: 'Orientierung', 2: 'Begradigung', 3: 'Entzerrung', 4: 'Zuschneiden',
5: 'Spalten', 6: 'Zeilen', 7: 'Woerter', 8: 'Struktur',
9: 'Korrektur', 10: 'Rekonstruktion', 11: 'Validierung',
}
function buildSteps(uiStep: number, skipSteps: string[]): PipelineStep[] {
return PIPELINE_STEPS.map((s, i) => ({
...s,
status: (
skipSteps.includes(s.id) ? 'skipped'
: i < uiStep ? 'completed'
: i === uiStep ? 'active'
: 'pending'
) as PipelineStepStatus,
}))
}
export function usePipelineNavigation(): PipelineNav {
const router = useRouter()
const searchParams = useSearchParams()
const paramSession = searchParams.get('session')
const paramStep = searchParams.get('step')
const [sessionId, setSessionId] = useState<string | null>(paramSession)
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [docTypeResult, setDocTypeResult] = useState<DocumentTypeResult | null>(null)
const [steps, setSteps] = useState<PipelineStep[]>(buildSteps(0, []))
const [loaded, setLoaded] = useState(false)
// Load session info when session param changes
useEffect(() => {
if (!paramSession) {
setSessionId(null)
setCurrentStepIndex(0)
setDocTypeResult(null)
setSteps(buildSteps(0, []))
setLoaded(true)
return
}
const load = async () => {
try {
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${paramSession}`)
if (!res.ok) return
const data = await res.json()
setSessionId(paramSession)
const savedDocType: DocumentTypeResult | null = data.doc_type_result || null
setDocTypeResult(savedDocType)
const dbStep = data.current_step || 1
let uiStep = Math.max(0, dbStep - 1)
const skipSteps = [...(savedDocType?.skip_steps || [])]
// Box sub-sessions (from column detection) skip pre-processing
const isBoxSubSession = !!data.parent_session_id
if (isBoxSubSession && dbStep >= 5) {
const SUB_SESSION_SKIP = ['orientation', 'deskew', 'dewarp', 'crop']
for (const s of SUB_SESSION_SKIP) {
if (!skipSteps.includes(s)) skipSteps.push(s)
}
if (uiStep < 4) uiStep = 4
}
// If URL has a step param, use that instead
if (paramStep) {
const stepIdx = PIPELINE_STEPS.findIndex(s => s.id === paramStep)
if (stepIdx >= 0) uiStep = stepIdx
}
setCurrentStepIndex(uiStep)
setSteps(buildSteps(uiStep, skipSteps))
} catch (e) {
console.error('Failed to load session:', e)
} finally {
setLoaded(true)
}
}
load()
}, [paramSession, paramStep])
const updateUrl = useCallback((sid: string | null, stepIdx?: number) => {
if (!sid) {
router.push('/ai/ocr-pipeline')
return
}
const stepId = stepIdx !== undefined ? PIPELINE_STEPS[stepIdx]?.id : undefined
const params = new URLSearchParams()
params.set('session', sid)
if (stepId) params.set('step', stepId)
router.push(`/ai/ocr-pipeline?${params.toString()}`)
}, [router])
const goToNextStep = useCallback(() => {
if (currentStepIndex >= steps.length - 1) {
// Last step — return to session list
setSessionId(null)
setCurrentStepIndex(0)
setDocTypeResult(null)
setSteps(buildSteps(0, []))
router.push('/ai/ocr-pipeline')
return
}
const skipSteps = docTypeResult?.skip_steps || []
let nextStep = currentStepIndex + 1
while (nextStep < steps.length && skipSteps.includes(PIPELINE_STEPS[nextStep]?.id)) {
nextStep++
}
if (nextStep >= steps.length) nextStep = steps.length - 1
setSteps(prev =>
prev.map((s, i) => {
if (i === currentStepIndex) return { ...s, status: 'completed' as PipelineStepStatus }
if (i === nextStep) return { ...s, status: 'active' as PipelineStepStatus }
if (i > currentStepIndex && i < nextStep && skipSteps.includes(PIPELINE_STEPS[i]?.id)) {
return { ...s, status: 'skipped' as PipelineStepStatus }
}
return s
}),
)
setCurrentStepIndex(nextStep)
if (sessionId) updateUrl(sessionId, nextStep)
}, [currentStepIndex, steps.length, docTypeResult, sessionId, updateUrl, router])
const goToStep = useCallback((index: number) => {
setCurrentStepIndex(index)
setSteps(prev =>
prev.map((s, i) => ({
...s,
status: s.status === 'skipped' ? 'skipped'
: i < index ? 'completed'
: i === index ? 'active'
: 'pending' as PipelineStepStatus,
})),
)
if (sessionId) updateUrl(sessionId, index)
}, [sessionId, updateUrl])
const goToSession = useCallback((sid: string) => {
updateUrl(sid)
}, [updateUrl])
const goToSessionList = useCallback(() => {
setSessionId(null)
setCurrentStepIndex(0)
setDocTypeResult(null)
setSteps(buildSteps(0, []))
router.push('/ai/ocr-pipeline')
}, [router])
const setDocType = useCallback((result: DocumentTypeResult) => {
setDocTypeResult(result)
const skipSteps = result.skip_steps || []
if (skipSteps.length > 0) {
setSteps(prev =>
prev.map(s =>
skipSteps.includes(s.id) ? { ...s, status: 'skipped' as PipelineStepStatus } : s,
),
)
}
}, [])
const reprocessFromStep = useCallback(async (uiStep: number) => {
if (!sessionId) return
const dbStep = uiStep + 1
if (!confirm(`Ab Schritt ${dbStep} (${STEP_NAMES[dbStep] || '?'}) neu verarbeiten? Nachfolgende Daten werden geloescht.`)) 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)
}
}, [sessionId, goToStep])
return {
sessionId,
currentStepIndex,
currentStepId: PIPELINE_STEPS[currentStepIndex]?.id || 'orientation',
steps,
docTypeResult,
goToNextStep,
goToStep,
goToSession,
goToSessionList,
setDocType,
reprocessFromStep,
}
}