'use client' import { useState, useEffect, useCallback } from 'react' const KLAUSUR_API = '/klausur-api' interface GutterSuggestion { id: string type: 'hyphen_join' | 'spell_fix' zone_index: number row_index: number col_index: number col_type: string cell_id: string original_text: string suggested_text: string next_row_index: number next_row_cell_id: string next_row_text: string missing_chars: string display_parts: string[] alternatives: string[] confidence: number reason: string } interface GutterRepairResult { suggestions: GutterSuggestion[] stats: { words_checked: number gutter_candidates: number suggestions_found: number error?: string } duration_seconds: number } interface StepGutterRepairProps { sessionId: string | null onNext: () => void } /** * Step 11: Gutter Repair (Wortkorrektur). * Detects words truncated at the book gutter and proposes corrections. * User can accept/reject each suggestion individually or in batch. */ export function StepGutterRepair({ sessionId, onNext }: StepGutterRepairProps) { const [loading, setLoading] = useState(false) const [applying, setApplying] = useState(false) const [result, setResult] = useState(null) const [accepted, setAccepted] = useState>(new Set()) const [rejected, setRejected] = useState>(new Set()) const [selectedText, setSelectedText] = useState>({}) const [applied, setApplied] = useState(false) const [error, setError] = useState('') const [applyMessage, setApplyMessage] = useState('') const analyse = useCallback(async () => { if (!sessionId) return setLoading(true) setError('') setApplied(false) setApplyMessage('') try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/gutter-repair`, { method: 'POST' }, ) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.detail || `Analyse fehlgeschlagen (${res.status})`) } const data: GutterRepairResult = await res.json() setResult(data) // Auto-accept all suggestions with high confidence const autoAccept = new Set() for (const s of data.suggestions) { if (s.confidence >= 0.85) { autoAccept.add(s.id) } } setAccepted(autoAccept) setRejected(new Set()) } catch (e) { setError(e instanceof Error ? e.message : String(e)) } finally { setLoading(false) } }, [sessionId]) // Auto-trigger analysis on mount useEffect(() => { if (sessionId) analyse() }, [sessionId, analyse]) const toggleSuggestion = (id: string) => { setAccepted(prev => { const next = new Set(prev) if (next.has(id)) { next.delete(id) setRejected(r => new Set(r).add(id)) } else { next.add(id) setRejected(r => { const n = new Set(r); n.delete(id); return n }) } return next }) } const acceptAll = () => { if (!result) return setAccepted(new Set(result.suggestions.map(s => s.id))) setRejected(new Set()) } const rejectAll = () => { if (!result) return setRejected(new Set(result.suggestions.map(s => s.id))) setAccepted(new Set()) } const applyAccepted = async () => { if (!sessionId || accepted.size === 0) return setApplying(true) setApplyMessage('') try { const res = await fetch( `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/gutter-repair/apply`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accepted: Array.from(accepted), text_overrides: selectedText, }), }, ) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.detail || `Anwenden fehlgeschlagen (${res.status})`) } const data = await res.json() setApplied(true) setApplyMessage(`${data.applied_count} Korrektur(en) angewendet.`) } catch (e) { setApplyMessage(e instanceof Error ? e.message : String(e)) } finally { setApplying(false) } } const suggestions = result?.suggestions || [] const hasSuggestions = suggestions.length > 0 return (
{/* Header */}

Wortkorrektur (Buchfalz)

Erkennt abgeschnittene oder unscharfe Woerter am Buchfalz und Bindestrich-Trennungen ueber Zeilen hinweg.

{result && !loading && ( )}
{/* Loading */} {loading && (
Analysiere Woerter am Buchfalz...
)} {/* Error */} {error && (
{error}
)} {/* No suggestions */} {result && !hasSuggestions && !loading && (
Keine Buchfalz-Fehler erkannt.
{result.stats.words_checked} Woerter geprueft, {result.stats.gutter_candidates} Kandidaten am Rand analysiert.
)} {/* Suggestions list */} {hasSuggestions && !loading && ( <> {/* Stats bar */}
{suggestions.length} Vorschlag/Vorschlaege ·{' '} {result!.stats.words_checked} Woerter geprueft ·{' '} {result!.duration_seconds}s
{/* Suggestion cards */}
{suggestions.map((s) => { const isAccepted = accepted.has(s.id) const isRejected = rejected.has(s.id) return (
{/* Left: suggestion details */}
{/* Type badge */}
{s.type === 'hyphen_join' ? 'Zeilenumbruch' : 'Buchfalz-Korrektur'} Zeile {s.row_index + 1}, Spalte {s.col_index + 1} {s.col_type && ` (${s.col_type.replace('column_', '')})`} = 0.9 ? 'text-green-500' : s.confidence >= 0.7 ? 'text-yellow-500' : 'text-red-500' }`}> {Math.round(s.confidence * 100)}%
{/* Correction display */} {s.type === 'hyphen_join' ? (
{s.original_text} Z.{s.row_index + 1} + {s.next_row_text.split(' ')[0]} Z.{s.next_row_index + 1} {s.suggested_text}
{s.missing_chars && (
Fehlende Zeichen: {s.missing_chars} {' '}· Darstellung: {s.display_parts.join(' | ')}
)}
) : (
{s.original_text} {selectedText[s.id] || s.suggested_text}
{/* Alternatives: show other candidates the user can pick */} {s.alternatives && s.alternatives.length > 0 && !applied && (
Alternativen: {[s.suggested_text, ...s.alternatives].map((alt) => { const isSelected = (selectedText[s.id] || s.suggested_text) === alt return ( ) })}
)}
)}
{/* Right: accept/reject toggle */} {!applied && ( )}
) })}
{/* Apply / Next buttons */}
{!applied ? ( ) : ( )} {!applied && ( )}
{/* Apply result message */} {applyMessage && (
{applyMessage}
)} )} {/* Skip button when no suggestions */} {result && !hasSuggestions && !loading && ( )}
) }