Sessions werden jetzt in PostgreSQL gespeichert statt in-memory. Neue Session-Liste mit Name, Datum, Schritt. Sessions ueberleben Browser-Refresh und Container-Neustart. Step 3 nutzt analyze_layout() fuer automatische Spaltenerkennung mit farbigem Overlay. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.5 KiB
TypeScript
169 lines
5.5 KiB
TypeScript
'use client'
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
import type { ColumnResult, ColumnGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types'
|
|
import { ColumnControls } from './ColumnControls'
|
|
|
|
const KLAUSUR_API = '/klausur-api'
|
|
|
|
interface StepColumnDetectionProps {
|
|
sessionId: string | null
|
|
onNext: () => void
|
|
}
|
|
|
|
export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionProps) {
|
|
const [columnResult, setColumnResult] = useState<ColumnResult | null>(null)
|
|
const [detecting, setDetecting] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// Auto-trigger column detection on mount
|
|
useEffect(() => {
|
|
if (!sessionId || columnResult) return
|
|
|
|
const runDetection = async () => {
|
|
setDetecting(true)
|
|
setError(null)
|
|
try {
|
|
// First check if columns already detected (reload case)
|
|
const infoRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`)
|
|
if (infoRes.ok) {
|
|
const info = await infoRes.json()
|
|
if (info.column_result) {
|
|
setColumnResult(info.column_result)
|
|
setDetecting(false)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Run detection
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/columns`, {
|
|
method: 'POST',
|
|
})
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
|
throw new Error(err.detail || 'Spaltenerkennung fehlgeschlagen')
|
|
}
|
|
const data: ColumnResult = await res.json()
|
|
setColumnResult(data)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
|
} finally {
|
|
setDetecting(false)
|
|
}
|
|
}
|
|
|
|
runDetection()
|
|
}, [sessionId, columnResult])
|
|
|
|
const handleRerun = useCallback(async () => {
|
|
if (!sessionId) return
|
|
setDetecting(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/columns`, {
|
|
method: 'POST',
|
|
})
|
|
if (!res.ok) throw new Error('Spaltenerkennung fehlgeschlagen')
|
|
const data: ColumnResult = await res.json()
|
|
setColumnResult(data)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Fehler')
|
|
} finally {
|
|
setDetecting(false)
|
|
}
|
|
}, [sessionId])
|
|
|
|
const handleGroundTruth = useCallback(async (gt: ColumnGroundTruth) => {
|
|
if (!sessionId) return
|
|
try {
|
|
await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/ground-truth/columns`, {
|
|
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="flex flex-col items-center justify-center py-16 text-center">
|
|
<div className="text-5xl mb-4">📊</div>
|
|
<h3 className="text-lg font-medium text-gray-700 dark:text-gray-300 mb-2">
|
|
Schritt 3: Spaltenerkennung
|
|
</h3>
|
|
<p className="text-gray-500 dark:text-gray-400 max-w-md">
|
|
Bitte zuerst Schritt 1 und 2 abschliessen.
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const dewarpedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/dewarped`
|
|
const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/columns-overlay`
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Loading indicator */}
|
|
{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" />
|
|
Spaltenerkennung laeuft...
|
|
</div>
|
|
)}
|
|
|
|
{/* Image comparison: overlay (left) vs clean (right) */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
|
Mit Spalten-Overlay
|
|
</div>
|
|
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
|
{columnResult ? (
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
<img
|
|
src={`${overlayUrl}?t=${Date.now()}`}
|
|
alt="Spalten-Overlay"
|
|
className="w-full h-auto"
|
|
/>
|
|
) : (
|
|
<div className="aspect-[3/4] flex items-center justify-center text-gray-400 text-sm">
|
|
{detecting ? 'Erkenne Spalten...' : 'Keine Daten'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
|
Entzerrtes Bild
|
|
</div>
|
|
<div className="border rounded-lg overflow-hidden dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={dewarpedUrl}
|
|
alt="Entzerrt"
|
|
className="w-full h-auto"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<ColumnControls
|
|
columnResult={columnResult}
|
|
onRerun={handleRerun}
|
|
onGroundTruth={handleGroundTruth}
|
|
onNext={onNext}
|
|
isDetecting={detecting}
|
|
/>
|
|
|
|
{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>
|
|
)
|
|
}
|