Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 27s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 2m15s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 20s
Page-split now creates independent sessions that appear directly in the session list. After split, the UI switches to the first child session. BoxSessionTabs, sub-session state, and parent-child tracking removed from Kombi code. Legacy ocr-overlay still uses BoxSessionTabs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
199 lines
7.5 KiB
TypeScript
199 lines
7.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useRef } from 'react'
|
|
const KLAUSUR_API = '/klausur-api'
|
|
|
|
interface PageSplitResult {
|
|
multi_page: boolean
|
|
page_count?: number
|
|
page_splits?: { x: number; y: number; width: number; height: number; page_index: number }[]
|
|
sub_sessions?: { id: string; name: string; page_index: number }[]
|
|
used_original?: boolean
|
|
duration_seconds?: number
|
|
}
|
|
|
|
interface StepPageSplitProps {
|
|
sessionId: string | null
|
|
sessionName: string
|
|
onNext: () => void
|
|
onSplitComplete: (firstChildId: string, firstChildName: string) => void
|
|
}
|
|
|
|
export function StepPageSplit({ sessionId, sessionName, onNext, onSplitComplete }: StepPageSplitProps) {
|
|
const [detecting, setDetecting] = useState(false)
|
|
const [splitResult, setSplitResult] = useState<PageSplitResult | null>(null)
|
|
const [error, setError] = useState('')
|
|
const didDetect = useRef(false)
|
|
|
|
// Auto-detect page split when step opens
|
|
useEffect(() => {
|
|
if (!sessionId || didDetect.current) return
|
|
didDetect.current = true
|
|
detectPageSplit()
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sessionId])
|
|
|
|
const detectPageSplit = async () => {
|
|
if (!sessionId) return
|
|
setDetecting(true)
|
|
setError('')
|
|
try {
|
|
// First check if this session was already split (status='split')
|
|
const sessionRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
|
|
if (sessionRes.ok) {
|
|
const sessionData = await sessionRes.json()
|
|
if (sessionData.status === 'split' && sessionData.crop_result?.multi_page) {
|
|
// Already split — find the child sessions in the session list
|
|
const listRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`)
|
|
if (listRes.ok) {
|
|
const listData = await listRes.json()
|
|
// Child sessions have names like "ParentName — Seite N"
|
|
const baseName = sessionName || sessionData.name || ''
|
|
const children = (listData.sessions || [])
|
|
.filter((s: { name?: string }) => s.name?.startsWith(baseName + ' — '))
|
|
.sort((a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name))
|
|
if (children.length > 0) {
|
|
setSplitResult({
|
|
multi_page: true,
|
|
page_count: children.length,
|
|
sub_sessions: children.map((s: { id: string; name: string }, i: number) => ({
|
|
id: s.id, name: s.name, page_index: i,
|
|
})),
|
|
})
|
|
onSplitComplete(children[0].id, children[0].name)
|
|
setDetecting(false)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Run page-split detection
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/page-split`, {
|
|
method: 'POST',
|
|
})
|
|
if (!res.ok) {
|
|
const data = await res.json().catch(() => ({}))
|
|
throw new Error(data.detail || 'Seitentrennung fehlgeschlagen')
|
|
}
|
|
const data: PageSplitResult = await res.json()
|
|
setSplitResult(data)
|
|
|
|
if (data.multi_page && data.sub_sessions?.length) {
|
|
// Rename sub-sessions to "Title — S. 1", "Title — S. 2"
|
|
const baseName = sessionName || 'Dokument'
|
|
for (let i = 0; i < data.sub_sessions.length; i++) {
|
|
const sub = data.sub_sessions[i]
|
|
const newName = `${baseName} — S. ${i + 1}`
|
|
await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sub.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name: newName }),
|
|
}).catch(() => {})
|
|
sub.name = newName
|
|
}
|
|
|
|
// Signal parent to switch to the first child session
|
|
onSplitComplete(data.sub_sessions[0].id, data.sub_sessions[0].name)
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : String(e))
|
|
} finally {
|
|
setDetecting(false)
|
|
}
|
|
}
|
|
|
|
if (!sessionId) return null
|
|
|
|
const imageUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/oriented`
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Image */}
|
|
<div className="relative rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-700">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={imageUrl}
|
|
alt="Orientiertes Bild"
|
|
className="w-full object-contain max-h-[500px]"
|
|
onError={(e) => {
|
|
// Fallback to non-oriented image
|
|
(e.target as HTMLImageElement).src =
|
|
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image`
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Detection status */}
|
|
{detecting && (
|
|
<div className="flex items-center gap-2 text-teal-600 dark:text-teal-400 text-sm">
|
|
<div className="animate-spin w-4 h-4 border-2 border-teal-500 border-t-transparent rounded-full" />
|
|
Doppelseiten-Erkennung laeuft...
|
|
</div>
|
|
)}
|
|
|
|
{/* Detection result */}
|
|
{splitResult && !detecting && (
|
|
splitResult.multi_page ? (
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-700 p-4 space-y-2">
|
|
<div className="text-sm font-medium text-blue-700 dark:text-blue-300">
|
|
Doppelseite erkannt — {splitResult.page_count} Seiten getrennt
|
|
</div>
|
|
<p className="text-xs text-blue-600 dark:text-blue-400">
|
|
Jede Seite wird als eigene Session weiterverarbeitet (eigene Begradigung, Entzerrung, etc.).
|
|
{splitResult.used_original && ' Trennung auf Originalbild, da Orientierung die Doppelseite gedreht hat.'}
|
|
</p>
|
|
<div className="flex gap-2 mt-2">
|
|
{splitResult.sub_sessions?.map(s => (
|
|
<span
|
|
key={s.id}
|
|
className="text-xs px-2.5 py-1 rounded-md bg-blue-100 dark:bg-blue-800/40 text-blue-700 dark:text-blue-300 font-medium"
|
|
>
|
|
{s.name}
|
|
</span>
|
|
))}
|
|
</div>
|
|
{splitResult.duration_seconds != null && (
|
|
<div className="text-xs text-gray-400">{splitResult.duration_seconds.toFixed(1)}s</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-800 p-4">
|
|
<div className="flex items-center gap-2 text-sm font-medium text-green-700 dark:text-green-300">
|
|
<span>✓</span> Einzelseite — keine Trennung noetig
|
|
</div>
|
|
{splitResult.duration_seconds != null && (
|
|
<div className="text-xs text-gray-400 mt-1">{splitResult.duration_seconds.toFixed(1)}s</div>
|
|
)}
|
|
</div>
|
|
)
|
|
)}
|
|
|
|
{/* Error */}
|
|
{error && (
|
|
<div className="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 p-3 rounded-lg">
|
|
{error}
|
|
<button
|
|
onClick={() => { didDetect.current = false; detectPageSplit() }}
|
|
className="ml-2 text-teal-600 hover:underline"
|
|
>
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Next button — only show when detection is done */}
|
|
{(splitResult || error) && !detecting && (
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={onNext}
|
|
className="px-6 py-2.5 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors"
|
|
>
|
|
Weiter →
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|