'use client' import { useCallback, useEffect, useRef, useState } from 'react' import type { ColumnTypeKey, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types' const COLUMN_TYPES: { value: ColumnTypeKey; label: string }[] = [ { value: 'column_en', label: 'EN' }, { value: 'column_de', label: 'DE' }, { value: 'column_example', label: 'Beispiel' }, { value: 'column_text', label: 'Text' }, { value: 'page_ref', label: 'Seite' }, { value: 'column_marker', label: 'Marker' }, { value: 'column_ignore', label: 'Ignorieren' }, ] const TYPE_OVERLAY_COLORS: Record = { column_en: 'rgba(59, 130, 246, 0.12)', column_de: 'rgba(34, 197, 94, 0.12)', column_example: 'rgba(249, 115, 22, 0.12)', column_text: 'rgba(6, 182, 212, 0.12)', page_ref: 'rgba(168, 85, 247, 0.12)', column_marker: 'rgba(239, 68, 68, 0.12)', column_ignore: 'rgba(128, 128, 128, 0.06)', } const TYPE_BADGE_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', } // Default column type sequence for newly created columns const DEFAULT_TYPE_SEQUENCE: ColumnTypeKey[] = [ 'page_ref', 'column_en', 'column_de', 'column_example', 'column_text', ] const MIN_DIVIDER_DISTANCE_PERCENT = 2 // Minimum 2% apart interface ManualColumnEditorProps { imageUrl: string imageWidth: number imageHeight: number onApply: (columns: PageRegion[]) => void onCancel: () => void applying: boolean mode?: 'manual' | 'ground-truth' layout?: 'two-column' | 'stacked' initialDividers?: number[] initialColumnTypes?: ColumnTypeKey[] } export function ManualColumnEditor({ imageUrl, imageWidth, imageHeight, onApply, onCancel, applying, mode = 'manual', layout = 'two-column', initialDividers, initialColumnTypes, }: ManualColumnEditorProps) { const containerRef = useRef(null) const [dividers, setDividers] = useState(initialDividers ?? []) const [columnTypes, setColumnTypes] = useState(initialColumnTypes ?? []) const [dragging, setDragging] = useState(null) const [imageLoaded, setImageLoaded] = useState(false) const isGT = mode === 'ground-truth' // Sync columnTypes length when dividers change useEffect(() => { const numColumns = dividers.length + 1 setColumnTypes(prev => { if (prev.length === numColumns) return prev const next = [...prev] while (next.length < numColumns) { const idx = next.length next.push(DEFAULT_TYPE_SEQUENCE[idx] || 'column_text') } while (next.length > numColumns) { next.pop() } return next }) }, [dividers.length]) const getXPercent = useCallback((clientX: number): number => { if (!containerRef.current) return 0 const rect = containerRef.current.getBoundingClientRect() const pct = ((clientX - rect.left) / rect.width) * 100 return Math.max(0, Math.min(100, pct)) }, []) const canPlaceDivider = useCallback((xPct: number, excludeIndex?: number): boolean => { for (let i = 0; i < dividers.length; i++) { if (i === excludeIndex) continue if (Math.abs(dividers[i] - xPct) < MIN_DIVIDER_DISTANCE_PERCENT) return false } return xPct > MIN_DIVIDER_DISTANCE_PERCENT && xPct < (100 - MIN_DIVIDER_DISTANCE_PERCENT) }, [dividers]) // Click on image to add a divider const handleImageClick = useCallback((e: React.MouseEvent) => { if (dragging !== null) return // Don't add if clicking on a divider handle if ((e.target as HTMLElement).dataset.divider) return const xPct = getXPercent(e.clientX) if (!canPlaceDivider(xPct)) return setDividers(prev => [...prev, xPct].sort((a, b) => a - b)) }, [dragging, getXPercent, canPlaceDivider]) // Drag handlers const handleDividerMouseDown = useCallback((e: React.MouseEvent, index: number) => { e.stopPropagation() e.preventDefault() setDragging(index) }, []) useEffect(() => { if (dragging === null) return const handleMouseMove = (e: MouseEvent) => { const xPct = getXPercent(e.clientX) if (canPlaceDivider(xPct, dragging)) { setDividers(prev => { const next = [...prev] next[dragging] = xPct return next.sort((a, b) => a - b) }) } } const handleMouseUp = () => { setDragging(null) } window.addEventListener('mousemove', handleMouseMove) window.addEventListener('mouseup', handleMouseUp) return () => { window.removeEventListener('mousemove', handleMouseMove) window.removeEventListener('mouseup', handleMouseUp) } }, [dragging, getXPercent, canPlaceDivider]) const removeDivider = useCallback((index: number) => { setDividers(prev => prev.filter((_, i) => i !== index)) }, []) const updateColumnType = useCallback((colIndex: number, type: ColumnTypeKey) => { setColumnTypes(prev => { const next = [...prev] next[colIndex] = type return next }) }, []) const handleApply = useCallback(() => { // Build PageRegion array from dividers const sorted = [...dividers].sort((a, b) => a - b) const columns: PageRegion[] = [] for (let i = 0; i <= sorted.length; i++) { const leftPct = i === 0 ? 0 : sorted[i - 1] const rightPct = i === sorted.length ? 100 : sorted[i] const x = Math.round((leftPct / 100) * imageWidth) const w = Math.round(((rightPct - leftPct) / 100) * imageWidth) columns.push({ type: columnTypes[i] || 'column_text', x, y: 0, width: w, height: imageHeight, classification_confidence: 1.0, classification_method: 'manual', }) } onApply(columns) }, [dividers, columnTypes, imageWidth, imageHeight, onApply]) // Compute column regions for overlay const sorted = [...dividers].sort((a, b) => a - b) const columnRegions = Array.from({ length: sorted.length + 1 }, (_, i) => ({ leftPct: i === 0 ? 0 : sorted[i - 1], rightPct: i === sorted.length ? 100 : sorted[i], type: columnTypes[i] || 'column_text', })) return (
{/* Layout: image + controls */}
{/* Left: Interactive image */}
Klicken um Trennlinien zu setzen
{/* eslint-disable-next-line @next/next/no-img-element */} Entzerrtes Bild setImageLoaded(true)} /> {imageLoaded && ( <> {/* Column overlays */} {columnRegions.map((region, i) => (
{i + 1}
))} {/* Divider lines */} {sorted.map((xPct, i) => (
handleDividerMouseDown(e, i)} > {/* Visible line */}
{/* Delete button */}
))} )}
{/* Right: Column type assignment + actions */}
Spaltentypen
{dividers.length === 0 ? (
👆

Klicken Sie auf das Bild links, um vertikale Trennlinien zwischen den Spalten zu setzen.

Linien koennen per Drag verschoben und per Hover geloescht werden.

) : (
{dividers.length} Linien = {dividers.length + 1} Spalten
{columnRegions.map((region, i) => (
Spalte {i + 1} {Math.round(region.rightPct - region.leftPct)}%
))}
)} {/* Action buttons */}
) }