'use client' /** * GridOverlay Component * * SVG overlay for displaying detected OCR grid structure on document images. * Shows recognized (green), problematic (orange), manual (blue), and empty (transparent) cells. * Supports click-to-edit for problematic cells. */ import { useCallback } from 'react' import { cn } from '@/lib/utils' export type CellStatus = 'empty' | 'recognized' | 'problematic' | 'manual' export interface GridCell { row: number col: number x: number // X position as percentage (0-100) y: number // Y position as percentage (0-100) width: number // Width as percentage (0-100) height: number // Height as percentage (0-100) text: string confidence: number status: CellStatus column_type?: 'english' | 'german' | 'example' | 'unknown' } export interface GridData { rows: number columns: number cells: GridCell[][] column_types: string[] column_boundaries: number[] row_boundaries: number[] deskew_angle: number stats: { recognized: number problematic: number empty: number manual?: number total: number coverage: number } } interface GridOverlayProps { grid: GridData imageUrl?: string onCellClick?: (cell: GridCell) => void selectedCell?: GridCell | null showEmpty?: boolean showLabels?: boolean showNumbers?: boolean // Show block numbers in cells highlightedBlockNumber?: number | null // Highlight specific block className?: string } // Status colors const STATUS_COLORS = { recognized: { fill: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity stroke: '#22c55e', // green-500 hoverFill: 'rgba(34, 197, 94, 0.3)', }, problematic: { fill: 'rgba(249, 115, 22, 0.3)', // orange-500 with opacity stroke: '#f97316', // orange-500 hoverFill: 'rgba(249, 115, 22, 0.4)', }, manual: { fill: 'rgba(59, 130, 246, 0.2)', // blue-500 with opacity stroke: '#3b82f6', // blue-500 hoverFill: 'rgba(59, 130, 246, 0.3)', }, empty: { fill: 'transparent', stroke: 'rgba(148, 163, 184, 0.3)', // slate-400 with opacity hoverFill: 'rgba(148, 163, 184, 0.1)', }, } // Helper to calculate block number (1-indexed, row-by-row) export function getCellBlockNumber(cell: GridCell, grid: GridData): number { return cell.row * grid.columns + cell.col + 1 } export function GridOverlay({ grid, imageUrl, onCellClick, selectedCell, showEmpty = false, showLabels = true, showNumbers = false, highlightedBlockNumber, className, }: GridOverlayProps) { const handleCellClick = useCallback( (cell: GridCell) => { if (onCellClick && cell.status !== 'empty') { onCellClick(cell) } }, [onCellClick] ) const flatCells = grid.cells.flat() return (
{/* Background image */} {imageUrl && ( Document )} {/* SVG overlay */} {/* Column type labels */} {showLabels && grid.column_types.length > 0 && ( {grid.column_types.map((type, idx) => { const x = grid.column_boundaries[idx] const width = grid.column_boundaries[idx + 1] - x const label = type === 'english' ? 'EN' : type === 'german' ? 'DE' : type === 'example' ? 'Ex' : '?' return ( {label} ) })} )} {/* Grid cells */} {flatCells.map((cell) => { const colors = STATUS_COLORS[cell.status] const isSelected = selectedCell?.row === cell.row && selectedCell?.col === cell.col const isClickable = cell.status !== 'empty' && onCellClick const blockNumber = getCellBlockNumber(cell, grid) const isHighlighted = highlightedBlockNumber === blockNumber // Skip empty cells if not showing them if (!showEmpty && cell.status === 'empty') { return null } return ( handleCellClick(cell)} className={isClickable ? 'cursor-pointer' : ''} > {/* Cell rectangle */} {/* Block number badge */} {showNumbers && cell.status !== 'empty' && ( <> {blockNumber} )} {/* Status indicator dot (only when not showing numbers) */} {!showNumbers && cell.status !== 'empty' && ( )} {/* Confidence indicator (for recognized cells) */} {cell.status === 'recognized' && cell.confidence < 0.7 && ( ! )} {/* Selection highlight */} {isSelected && ( )} ) })} {/* Row boundaries (optional grid lines) */} {grid.row_boundaries.map((y, idx) => ( ))} {/* Column boundaries */} {grid.column_boundaries.map((x, idx) => ( ))}
) } /** * GridStats Component * * Displays statistics about the grid detection results. */ interface GridStatsProps { stats: GridData['stats'] deskewAngle?: number className?: string } export function GridStats({ stats, deskewAngle, className }: GridStatsProps) { const coveragePercent = Math.round(stats.coverage * 100) return (
Erkannt: {stats.recognized}
{(stats.manual ?? 0) > 0 && (
Manuell: {stats.manual}
)} {stats.problematic > 0 && (
Problematisch: {stats.problematic}
)}
Leer: {stats.empty}
Abdeckung: {coveragePercent}%
{deskewAngle !== undefined && deskewAngle !== 0 && (
Begradigt: {deskewAngle.toFixed(1)}
)}
) } /** * Legend Component for GridOverlay */ export function GridLegend({ className }: { className?: string }) { return (
Erkannt
Problematisch
Manuell korrigiert
Leer
) }