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>
226 lines
7.0 KiB
TypeScript
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,
|
|
}
|
|
}
|