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 25s
CI / test-go-edu-search (push) Successful in 25s
CI / test-python-klausur (push) Failing after 1m53s
CI / test-python-agent-core (push) Successful in 15s
CI / test-nodejs-website (push) Successful in 17s
Step components (Deskew, Dewarp, Crop, Orientation) had local state guards that prevented reloading when sessionId changed via sub-session tab clicks. Added useEffect reset hooks that clear all local state when sessionId changes, allowing the component to properly reload the new session's data. Also renamed "Box N" to "Seite N" in BoxSessionTabs per user feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
192 lines
6.3 KiB
TypeScript
192 lines
6.3 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import type { DeskewGroundTruth, DeskewResult, SessionInfo } from '@/app/(admin)/ai/ocr-pipeline/types'
|
|
import { DeskewControls } from './DeskewControls'
|
|
import { ImageCompareView } from './ImageCompareView'
|
|
|
|
const KLAUSUR_API = '/klausur-api'
|
|
|
|
interface StepDeskewProps {
|
|
sessionId: string | null
|
|
onNext: () => void
|
|
}
|
|
|
|
export function StepDeskew({ sessionId, onNext }: StepDeskewProps) {
|
|
const [session, setSession] = useState<SessionInfo | null>(null)
|
|
const [deskewResult, setDeskewResult] = useState<DeskewResult | null>(null)
|
|
const [deskewing, setDeskewing] = useState(false)
|
|
const [applying, setApplying] = useState(false)
|
|
const [showBinarized, setShowBinarized] = useState(false)
|
|
const [showGrid, setShowGrid] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [hasAutoRun, setHasAutoRun] = useState(false)
|
|
|
|
// Reset state when sessionId changes (e.g. switching sub-sessions)
|
|
useEffect(() => {
|
|
setSession(null)
|
|
setDeskewResult(null)
|
|
setHasAutoRun(false)
|
|
setError(null)
|
|
}, [sessionId])
|
|
|
|
// Load session and auto-trigger deskew
|
|
useEffect(() => {
|
|
if (!sessionId || session) return
|
|
|
|
const loadAndDeskew = async () => {
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
|
|
if (!res.ok) return
|
|
const data = await res.json()
|
|
|
|
const sessionInfo: SessionInfo = {
|
|
session_id: data.session_id,
|
|
filename: data.filename,
|
|
image_width: data.image_width,
|
|
image_height: data.image_height,
|
|
// Use oriented image as "before" view (deskew runs right after orientation)
|
|
original_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/oriented`,
|
|
}
|
|
setSession(sessionInfo)
|
|
|
|
// If deskew result already exists, use it
|
|
if (data.deskew_result) {
|
|
const dr: DeskewResult = {
|
|
...data.deskew_result,
|
|
deskewed_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/deskewed`,
|
|
binarized_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/binarized`,
|
|
}
|
|
setDeskewResult(dr)
|
|
return
|
|
}
|
|
|
|
// Auto-trigger deskew if not already done
|
|
if (!hasAutoRun) {
|
|
setHasAutoRun(true)
|
|
setDeskewing(true)
|
|
const deskewRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/deskew`, {
|
|
method: 'POST',
|
|
})
|
|
|
|
if (!deskewRes.ok) {
|
|
throw new Error('Begradigung fehlgeschlagen')
|
|
}
|
|
|
|
const deskewData: DeskewResult = await deskewRes.json()
|
|
deskewData.deskewed_image_url = `${KLAUSUR_API}${deskewData.deskewed_image_url}`
|
|
deskewData.binarized_image_url = `${KLAUSUR_API}${deskewData.binarized_image_url}`
|
|
setDeskewResult(deskewData)
|
|
}
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler beim Laden')
|
|
} finally {
|
|
setDeskewing(false)
|
|
}
|
|
}
|
|
|
|
loadAndDeskew()
|
|
}, [sessionId, session, hasAutoRun])
|
|
|
|
const handleManualDeskew = useCallback(async (angle: number) => {
|
|
if (!sessionId) return
|
|
setApplying(true)
|
|
setError(null)
|
|
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/deskew/manual`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ angle }),
|
|
})
|
|
|
|
if (!res.ok) throw new Error('Manuelle Begradigung fehlgeschlagen')
|
|
|
|
const data = await res.json()
|
|
setDeskewResult((prev) =>
|
|
prev
|
|
? {
|
|
...prev,
|
|
angle_applied: data.angle_applied,
|
|
method_used: data.method_used,
|
|
deskewed_image_url: `${KLAUSUR_API}${data.deskewed_image_url}?t=${Date.now()}`,
|
|
}
|
|
: null,
|
|
)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler')
|
|
} finally {
|
|
setApplying(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
const handleGroundTruth = useCallback(async (gt: DeskewGroundTruth) => {
|
|
if (!sessionId) return
|
|
try {
|
|
await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/deskew`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(gt),
|
|
})
|
|
} catch (e) {
|
|
console.error('Ground truth save failed:', e)
|
|
}
|
|
}, [sessionId])
|
|
|
|
if (!sessionId) {
|
|
return <div className="text-sm text-gray-400">Keine Session ausgewaehlt.</div>
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Filename */}
|
|
{session && (
|
|
<div className="text-sm text-gray-500 dark:text-gray-400">
|
|
Datei: <span className="font-medium text-gray-700 dark:text-gray-300">{session.filename}</span>
|
|
{' '}({session.image_width} x {session.image_height} px)
|
|
</div>
|
|
)}
|
|
|
|
{/* Loading indicator */}
|
|
{deskewing && (
|
|
<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" />
|
|
Begradigung laeuft (beide Methoden)...
|
|
</div>
|
|
)}
|
|
|
|
{/* Image comparison */}
|
|
{session && (
|
|
<ImageCompareView
|
|
originalUrl={session.original_image_url}
|
|
deskewedUrl={deskewResult?.deskewed_image_url ?? null}
|
|
showGrid={showGrid}
|
|
showBinarized={showBinarized}
|
|
binarizedUrl={deskewResult?.binarized_image_url ?? null}
|
|
leftLabel="Orientiert"
|
|
rightLabel="Begradigt"
|
|
/>
|
|
)}
|
|
|
|
{/* Controls */}
|
|
<DeskewControls
|
|
deskewResult={deskewResult}
|
|
showBinarized={showBinarized}
|
|
onToggleBinarized={() => setShowBinarized((v) => !v)}
|
|
showGrid={showGrid}
|
|
onToggleGrid={() => setShowGrid((v) => !v)}
|
|
onManualDeskew={handleManualDeskew}
|
|
onGroundTruth={handleGroundTruth}
|
|
onNext={onNext}
|
|
isApplying={applying}
|
|
/>
|
|
|
|
{error && (
|
|
<div className="p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm">
|
|
{error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|