'use client' import { useState, useMemo } from 'react' import type { ColumnResult, ColumnGroundTruth, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types' interface ColumnControlsProps { columnResult: ColumnResult | null onRerun: () => void onManualMode: () => void onGtMode: () => void onGroundTruth: (gt: ColumnGroundTruth) => void onNext: () => void isDetecting: boolean savedGtColumns: PageRegion[] | null } const TYPE_COLORS: Record = { column_en: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400', column_de: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400', column_example: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400', column_text: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/30 dark:text-cyan-400', page_ref: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400', column_marker: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', column_ignore: 'bg-gray-100 text-gray-500 dark:bg-gray-700/30 dark:text-gray-500', header: 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400', footer: 'bg-gray-100 text-gray-600 dark:bg-gray-700/50 dark:text-gray-400', } const TYPE_LABELS: Record = { column_en: 'EN', column_de: 'DE', column_example: 'Beispiel', column_text: 'Text', page_ref: 'Seite', column_marker: 'Marker', column_ignore: 'Ignorieren', header: 'Header', footer: 'Footer', } const METHOD_LABELS: Record = { content: 'Inhalt', position_enhanced: 'Position', position_fallback: 'Fallback', } interface DiffRow { index: number autoCol: PageRegion | null gtCol: PageRegion | null diffX: number | null diffW: number | null typeMismatch: boolean } /** Match auto columns to GT columns by overlap on X-axis (IoU > 50%) */ function computeDiff(autoCols: PageRegion[], gtCols: PageRegion[]): DiffRow[] { const rows: DiffRow[] = [] const usedGt = new Set() const usedAuto = new Set() // Match auto → GT by best X-axis overlap for (let ai = 0; ai < autoCols.length; ai++) { const a = autoCols[ai] let bestIdx = -1 let bestIoU = 0 for (let gi = 0; gi < gtCols.length; gi++) { if (usedGt.has(gi)) continue const g = gtCols[gi] const overlapStart = Math.max(a.x, g.x) const overlapEnd = Math.min(a.x + a.width, g.x + g.width) const overlap = Math.max(0, overlapEnd - overlapStart) const union = (a.width + g.width) - overlap const iou = union > 0 ? overlap / union : 0 if (iou > bestIoU) { bestIoU = iou bestIdx = gi } } if (bestIdx >= 0 && bestIoU > 0.3) { usedGt.add(bestIdx) usedAuto.add(ai) const g = gtCols[bestIdx] rows.push({ index: rows.length + 1, autoCol: a, gtCol: g, diffX: g.x - a.x, diffW: g.width - a.width, typeMismatch: a.type !== g.type, }) } } // Unmatched auto columns for (let ai = 0; ai < autoCols.length; ai++) { if (usedAuto.has(ai)) continue rows.push({ index: rows.length + 1, autoCol: autoCols[ai], gtCol: null, diffX: null, diffW: null, typeMismatch: false, }) } // Unmatched GT columns for (let gi = 0; gi < gtCols.length; gi++) { if (usedGt.has(gi)) continue rows.push({ index: rows.length + 1, autoCol: null, gtCol: gtCols[gi], diffX: null, diffW: null, typeMismatch: false, }) } return rows } export function ColumnControls({ columnResult, onRerun, onManualMode, onGtMode, onGroundTruth, onNext, isDetecting, savedGtColumns }: ColumnControlsProps) { const [gtSaved, setGtSaved] = useState(false) const diffRows = useMemo(() => { if (!columnResult || !savedGtColumns) return null const autoCols = columnResult.columns.filter(c => c.type.startsWith('column') || c.type === 'page_ref') const gtCols = savedGtColumns.filter(c => c.type.startsWith('column') || c.type === 'page_ref') return computeDiff(autoCols, gtCols) }, [columnResult, savedGtColumns]) if (!columnResult) return null const columns = columnResult.columns.filter((c: PageRegion) => c.type.startsWith('column') || c.type === 'page_ref') const headerFooter = columnResult.columns.filter((c: PageRegion) => !c.type.startsWith('column') && c.type !== 'page_ref') const handleGt = (isCorrect: boolean) => { onGroundTruth({ is_correct: isCorrect }) setGtSaved(true) } return (
{/* Summary */}
{columns.length} Spalten erkannt {columnResult.duration_seconds > 0 && ( ({columnResult.duration_seconds}s) )}
{/* Column list */}
{columns.map((col: PageRegion, i: number) => (
{TYPE_LABELS[col.type] || col.type} {col.classification_confidence != null && col.classification_confidence < 1.0 && ( {Math.round(col.classification_confidence * 100)}% )} {col.classification_method && ( ({METHOD_LABELS[col.classification_method] || col.classification_method}) )} x={col.x} y={col.y} {col.width}x{col.height}px
))} {headerFooter.map((r: PageRegion, i: number) => (
{TYPE_LABELS[r.type] || r.type} x={r.x} y={r.y} {r.width}x{r.height}px
))}
{/* Diff table (Auto vs GT) */} {diffRows && diffRows.length > 0 && (
Vergleich: Auto vs Ground Truth
{diffRows.map((row) => ( 20) || (row.diffW !== null && Math.abs(row.diffW) > 20) ? 'bg-amber-50 dark:bg-amber-900/10' : '' } > ))}
# Auto (Typ, x, w) GT (Typ, x, w) Diff X Diff W
{row.index} {row.autoCol ? ( {TYPE_LABELS[row.autoCol.type] || row.autoCol.type} {' '}{row.autoCol.x}, {row.autoCol.width} ) : ( fehlt )} {row.gtCol ? ( {TYPE_LABELS[row.gtCol.type] || row.gtCol.type} {' '}{row.gtCol.x}, {row.gtCol.width} ) : ( fehlt )} {row.diffX !== null ? ( 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}> {row.diffX > 0 ? '+' : ''}{row.diffX} ) : '—'} {row.diffW !== null ? ( 20 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500'}> {row.diffW > 0 ? '+' : ''}{row.diffW} ) : '—'}
)} {/* Ground Truth + Navigation */}
Spalten korrekt? {gtSaved ? ( Gespeichert ) : ( <> )}
) }