'use client' import { useEffect, useState } from 'react' import type { StructureResult } from '@/app/(admin)/ai/ocr-pipeline/types' const KLAUSUR_API = '/klausur-api' interface StepStructureDetectionProps { sessionId: string | null onNext: () => void } const COLOR_HEX: Record = { red: '#dc2626', orange: '#ea580c', yellow: '#ca8a04', green: '#16a34a', blue: '#2563eb', purple: '#9333ea', } export function StepStructureDetection({ sessionId, onNext }: StepStructureDetectionProps) { const [result, setResult] = useState(null) const [detecting, setDetecting] = useState(false) const [error, setError] = useState(null) const [hasRun, setHasRun] = useState(false) const [overlayTs, setOverlayTs] = useState(0) // Auto-trigger detection on mount useEffect(() => { if (!sessionId || hasRun) return setHasRun(true) const runDetection = async () => { setDetecting(true) setError(null) try { // Check if session already has structure result const sessionRes = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}`) if (sessionRes.ok) { const sessionData = await sessionRes.json() if (sessionData.structure_result) { setResult(sessionData.structure_result) setOverlayTs(Date.now()) setDetecting(false) return } } const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/detect-structure`, { method: 'POST', }) if (!res.ok) { throw new Error('Strukturerkennung fehlgeschlagen') } const data = await res.json() setResult(data) setOverlayTs(Date.now()) } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') } finally { setDetecting(false) } } runDetection() }, [sessionId, hasRun]) const handleRerun = async () => { if (!sessionId) return setDetecting(true) setError(null) try { const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/detect-structure`, { method: 'POST', }) if (!res.ok) throw new Error('Erneute Erkennung fehlgeschlagen') const data = await res.json() setResult(data) setOverlayTs(Date.now()) } catch (e) { setError(e instanceof Error ? e.message : 'Unbekannter Fehler') } finally { setDetecting(false) } } if (!sessionId) { return
Keine Session ausgewaehlt.
} const croppedUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped` const overlayUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/structure-overlay${overlayTs ? `?t=${overlayTs}` : ''}` return (
{/* Loading indicator */} {detecting && (
Dokumentstruktur wird analysiert...
)} {/* Two-column image comparison */}
{/* Left: Original document */}
Original
{/* eslint-disable-next-line @next/next/no-img-element */} Originaldokument { (e.target as HTMLImageElement).style.display = 'none' }} />
{/* Right: Structure overlay */}
Erkannte Struktur
{/* eslint-disable-next-line @next/next/no-img-element */} Strukturerkennung { (e.target as HTMLImageElement).style.display = 'none' }} />
{/* Result info */} {result && (
{/* Summary badges */}
{result.zones.length} Zone(n) {result.boxes.length} Box(en) {result.graphics && result.graphics.length > 0 && ( {result.graphics.length} Grafik(en) )} {result.has_words && ( {result.word_count} Woerter )} {(result.border_ghosts_removed ?? 0) > 0 && ( {result.border_ghosts_removed} Rahmenlinien entfernt )} {result.image_width}x{result.image_height}px | {result.duration_seconds}s
{/* Boxes detail */} {result.boxes.length > 0 && (

Erkannte Boxen

{result.boxes.map((box, i) => (
Box {i + 1}: {box.w}x{box.h}px @ ({box.x}, {box.y}) {box.bg_color_name && box.bg_color_name !== 'unknown' && box.bg_color_name !== 'white' && ( {box.bg_color_name} )} {box.border_thickness > 0 && ( Rahmen: {box.border_thickness}px )} {Math.round(box.confidence * 100)}%
))}
)} {/* Zones detail */}

Seitenzonen

{result.zones.map((zone) => ( {zone.zone_type === 'box' ? 'Box' : 'Inhalt'} {zone.index} ({zone.w}x{zone.h}) ))}
{/* Graphics / visual elements */} {result.graphics && result.graphics.length > 0 && (

Graphische Elemente ({result.graphics.length})

{/* Summary by shape */} {(() => { const shapeCounts: Record = {} for (const g of result.graphics) { shapeCounts[g.shape] = (shapeCounts[g.shape] || 0) + 1 } return (
{Object.entries(shapeCounts) .sort(([, a], [, b]) => b - a) .map(([shape, count]) => ( {shape === 'arrow' ? '→' : shape === 'circle' ? '●' : shape === 'line' ? '─' : shape === 'exclamation' ? '❗' : shape === 'dot' ? '•' : shape === 'illustration' ? '🖼' : '◆'} {' '}{shape} ×{count} ))}
) })()} {/* Individual graphics list */}
{result.graphics.map((g, i) => (
{g.shape} {g.w}x{g.h}px @ ({g.x}, {g.y}) {g.color_name} {Math.round(g.confidence * 100)}%
))}
)} {/* Color regions */} {Object.keys(result.color_pixel_counts).length > 0 && (

Erkannte Farben

{Object.entries(result.color_pixel_counts) .sort(([, a], [, b]) => b - a) .map(([name, count]) => ( {name} {count.toLocaleString()}px ))}
)}
)} {/* Action buttons */} {result && (
)} {error && (
{error}
)}
) }