'use client' import { useEffect, useState } from 'react' import type { DeskewResult, DewarpResult, DewarpDetection, DewarpGroundTruth } from '@/app/(admin)/ai/ocr-pipeline/types' interface DewarpControlsProps { dewarpResult: DewarpResult | null deskewResult?: DeskewResult | null showGrid: boolean onToggleGrid: () => void onManualDewarp: (shearDegrees: number) => void onCombinedAdjust?: (rotationDegrees: number, shearDegrees: number) => void onGroundTruth: (gt: DewarpGroundTruth) => void onNext: () => void isApplying: boolean } const METHOD_LABELS: Record = { vertical_edge: 'A: Vertikale Kanten', projection: 'B: Projektions-Varianz', hough_lines: 'C: Hough-Linien', text_lines: 'D: Textzeilenanalyse', manual: 'Manuell', manual_combined: 'Manuell (kombiniert)', none: 'Keine Korrektur', } const SHEAR_METHOD_KEYS = ['vertical_edge', 'projection', 'hough_lines', 'text_lines'] as const /** Colour for a confidence value (0-1). */ function confColor(conf: number): string { if (conf >= 0.7) return 'text-green-600 dark:text-green-400' if (conf >= 0.5) return 'text-yellow-600 dark:text-yellow-400' return 'text-gray-400' } /** Short confidence bar (visual). */ function ConfBar({ value }: { value: number }) { const pct = Math.round(value * 100) const bg = value >= 0.7 ? 'bg-green-500' : value >= 0.5 ? 'bg-yellow-500' : 'bg-gray-400' return (
{pct}%
) } /** A single slider row for fine-tuning. */ function FineTuneSlider({ label, value, onChange, min, max, step, unit = '\u00B0', radioName, radioChecked, onRadioChange, }: { label: string value: number onChange: (v: number) => void min: number max: number step: number unit?: string radioName?: string radioChecked?: boolean onRadioChange?: () => void }) { return (
{radioName !== undefined && ( )} {label} {min}{unit} onChange(parseInt(e.target.value) / 100)} className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500" /> +{max}{unit} {value >= 0 ? '+' : ''}{value.toFixed(2)}{unit}
) } export function DewarpControls({ dewarpResult, deskewResult, showGrid, onToggleGrid, onManualDewarp, onCombinedAdjust, onGroundTruth, onNext, isApplying, }: DewarpControlsProps) { const [manualShear, setManualShear] = useState(0) const [gtFeedback, setGtFeedback] = useState<'correct' | 'incorrect' | null>(null) const [gtNotes, setGtNotes] = useState('') const [gtSaved, setGtSaved] = useState(false) const [showDetails, setShowDetails] = useState(false) const [showFineTune, setShowFineTune] = useState(false) // Fine-tuning rotation sliders (3 passes) const [p1Iterative, setP1Iterative] = useState(0) const [p2Residual, setP2Residual] = useState(0) const [p3Textline, setP3Textline] = useState(0) // Fine-tuning shear sliders (4 methods) + selected method const [shearValues, setShearValues] = useState>({ vertical_edge: 0, projection: 0, hough_lines: 0, text_lines: 0, }) const [selectedShearMethod, setSelectedShearMethod] = useState('vertical_edge') // Initialize slider to auto-detected value when result arrives useEffect(() => { if (dewarpResult && dewarpResult.shear_degrees !== undefined) { setManualShear(dewarpResult.shear_degrees) } }, [dewarpResult?.shear_degrees]) // Initialize fine-tuning sliders from deskew result useEffect(() => { if (deskewResult) { setP1Iterative(deskewResult.angle_iterative ?? 0) setP2Residual(deskewResult.angle_residual ?? 0) setP3Textline(deskewResult.angle_textline ?? 0) } }, [deskewResult]) // Initialize shear sliders from dewarp detections useEffect(() => { if (dewarpResult?.detections) { const newValues = { ...shearValues } let bestMethod = selectedShearMethod let bestConf = -1 for (const d of dewarpResult.detections) { if (d.method in newValues) { newValues[d.method] = d.shear_degrees if (d.confidence > bestConf) { bestConf = d.confidence bestMethod = d.method } } } setShearValues(newValues) // Select the method that was actually used, or the highest confidence if (dewarpResult.method_used && dewarpResult.method_used in newValues) { setSelectedShearMethod(dewarpResult.method_used) } else { setSelectedShearMethod(bestMethod) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dewarpResult?.detections]) const rotationSum = p1Iterative + p2Residual + p3Textline const activeShear = shearValues[selectedShearMethod] ?? 0 const handleGroundTruth = (isCorrect: boolean) => { setGtFeedback(isCorrect ? 'correct' : 'incorrect') if (isCorrect) { onGroundTruth({ is_correct: true }) setGtSaved(true) } } const handleGroundTruthIncorrect = () => { onGroundTruth({ is_correct: false, corrected_shear: manualShear !== 0 ? manualShear : undefined, notes: gtNotes || undefined, }) setGtSaved(true) } const handleShearValueChange = (method: string, value: number) => { setShearValues((prev) => ({ ...prev, [method]: value })) } const handleFineTunePreview = () => { if (onCombinedAdjust) { onCombinedAdjust(rotationSum, activeShear) } } const wasRejected = dewarpResult && dewarpResult.method_used === 'none' && (dewarpResult.detections || []).length > 0 const wasApplied = dewarpResult && dewarpResult.method_used !== 'none' && dewarpResult.method_used !== 'manual' && dewarpResult.method_used !== 'manual_combined' const detections = dewarpResult?.detections || [] return (
{/* Summary banner */} {dewarpResult && (
{/* Status line */}
{wasRejected ? '\u26A0\uFE0F' : wasApplied ? '\u2705' : '\u2796'} {wasRejected ? 'Quality Gate: Korrektur verworfen (Projektion nicht verbessert)' : wasApplied ? `Korrektur angewendet: ${dewarpResult.shear_degrees.toFixed(2)}\u00B0` : dewarpResult.method_used === 'manual' || dewarpResult.method_used === 'manual_combined' ? `Manuelle Korrektur: ${dewarpResult.shear_degrees.toFixed(2)}\u00B0` : 'Keine Korrektur noetig'}
{/* Key metrics */}
Scherung:{' '} {dewarpResult.shear_degrees.toFixed(2)}\u00B0
Methode:{' '} {dewarpResult.method_used.includes('+') ? `Ensemble (${dewarpResult.method_used.split('+').map(m => METHOD_LABELS[m] || m).join(' + ')})` : METHOD_LABELS[dewarpResult.method_used] || dewarpResult.method_used}
Konfidenz:
{/* Toggles row */}
{detections.length > 0 && ( )}
{/* Detailed detections */} {showDetails && detections.length > 0 && (
Einzelne Detektoren:
{detections.map((d: DewarpDetection) => { const isUsed = dewarpResult.method_used.includes(d.method) const aboveThreshold = d.confidence >= 0.5 return (
{isUsed ? '\u2713' : aboveThreshold ? '\u2012' : '\u2717'} {METHOD_LABELS[d.method] || d.method} {d.shear_degrees.toFixed(2)}\u00B0 {!aboveThreshold && ( (unter Schwelle) )}
) })}
{wasRejected && (
Die Korrektur wurde verworfen, weil die horizontale Projektions-Varianz nach Anwendung nicht besser war als vorher.
)}
)}
)} {/* Manual shear angle slider */} {dewarpResult && (
Scherwinkel (manuell)
-2.0\u00B0 setManualShear(parseInt(e.target.value) / 100)} className="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700 accent-teal-500" /> +2.0\u00B0 {manualShear.toFixed(2)}\u00B0

Scherung der vertikalen Achse in Grad. Positiv = Spalten nach rechts kippen, negativ = nach links.

)} {/* Fine-tuning panel */} {dewarpResult && onCombinedAdjust && (
{showFineTune && (
{/* Rotation section */}
Rotation (Begradigung)
Summe Rotation {rotationSum >= 0 ? '+' : ''}{rotationSum.toFixed(2)}\u00B0
{/* Shear section */}
Scherung (Entzerrung) — einen Wert waehlen
{SHEAR_METHOD_KEYS.map((method) => ( handleShearValueChange(method, v)} min={-5} max={5} step={0.05} radioName="shear-method" radioChecked={selectedShearMethod === method} onRadioChange={() => setSelectedShearMethod(method)} /> ))}
Gewaehlte Scherung {activeShear >= 0 ? '+' : ''}{activeShear.toFixed(2)}\u00B0 ({METHOD_LABELS[selectedShearMethod]})
{/* Preview + Save */}
Rotation: {rotationSum >= 0 ? '+' : ''}{rotationSum.toFixed(2)}\u00B0 + Scherung: {activeShear >= 0 ? '+' : ''}{activeShear.toFixed(2)}\u00B0
)}
)} {/* Ground Truth */} {dewarpResult && !showFineTune && (
Spalten vertikal ausgerichtet?

Pruefen ob die Spaltenraender jetzt senkrecht zum Raster stehen.

{!gtSaved ? (
{gtFeedback === 'incorrect' && (