'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: (sessionId: string) => void } export function StepDeskew({ sessionId: existingSessionId, onNext }: StepDeskewProps) { const [session, setSession] = useState(null) const [deskewResult, setDeskewResult] = useState(null) const [uploading, setUploading] = useState(false) const [deskewing, setDeskewing] = useState(false) const [applying, setApplying] = useState(false) const [showBinarized, setShowBinarized] = useState(false) const [showGrid, setShowGrid] = useState(true) const [error, setError] = useState(null) const [dragOver, setDragOver] = useState(false) const [sessionName, setSessionName] = useState('') // Reload session data when navigating back from a later step useEffect(() => { if (!existingSessionId || session) return const loadSession = async () => { try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}`) 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, original_image_url: `${KLAUSUR_API}${data.original_image_url}`, } setSession(sessionInfo) // Reconstruct deskew result from session data if (data.deskew_result) { const dr: DeskewResult = { ...data.deskew_result, deskewed_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}/image/deskewed`, binarized_image_url: `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${existingSessionId}/image/binarized`, } setDeskewResult(dr) } } catch (e) { console.error('Failed to reload session:', e) } } loadSession() }, [existingSessionId, session]) const handleUpload = useCallback(async (file: File) => { setUploading(true) setError(null) setDeskewResult(null) try { const formData = new FormData() formData.append('file', file) if (sessionName.trim()) { formData.append('name', sessionName.trim()) } const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`, { method: 'POST', body: formData, }) if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })) throw new Error(err.detail || 'Upload fehlgeschlagen') } const data: SessionInfo = await res.json() // Prepend API prefix to relative URLs data.original_image_url = `${KLAUSUR_API}${data.original_image_url}` setSession(data) // Auto-trigger deskew setDeskewing(true) const deskewRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${data.session_id}/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 : 'Unbekannter Fehler') } finally { setUploading(false) setDeskewing(false) } }, []) const handleManualDeskew = useCallback(async (angle: number) => { if (!session) return setApplying(true) setError(null) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${session.session_id}/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, // Force reload by appending timestamp 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) } }, [session]) const handleGroundTruth = useCallback(async (gt: DeskewGroundTruth) => { if (!session) return try { await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${session.session_id}/ground-truth/deskew`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(gt), }) } catch (e) { console.error('Ground truth save failed:', e) } }, [session]) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setDragOver(false) const file = e.dataTransfer.files[0] if (file) handleUpload(file) }, [handleUpload]) const handleFileInput = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] if (file) handleUpload(file) }, [handleUpload]) // Upload area (no session yet) if (!session) { return (
{/* Session name input */}
setSessionName(e.target.value)} placeholder="z.B. Unit 3 Seite 42" className="w-full max-w-sm px-3 py-2 text-sm border rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-teal-500" />
{ e.preventDefault(); setDragOver(true) }} onDragLeave={() => setDragOver(false)} onDrop={handleDrop} className={`border-2 border-dashed rounded-xl p-12 text-center transition-colors ${ dragOver ? 'border-teal-400 bg-teal-50 dark:bg-teal-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-teal-400' }`} > {uploading ? (

Wird hochgeladen...

) : ( <>
📄

PDF oder Bild hierher ziehen

oder

)}
{error && (
{error}
)}
) } // Session active: show comparison + controls return (
{/* Filename */}
Datei: {session.filename} {' '}({session.image_width} x {session.image_height} px)
{/* Loading indicator */} {deskewing && (
Begradigung laeuft (beide Methoden)...
)} {/* Image comparison */} {/* Controls */} setShowBinarized((v) => !v)} showGrid={showGrid} onToggleGrid={() => setShowGrid((v) => !v)} onManualDeskew={handleManualDeskew} onGroundTruth={handleGroundTruth} onNext={() => session && onNext(session.session_id)} isApplying={applying} /> {error && (
{error}
)}
) }