Files
breakpilot-lehrer/admin-lehrer/components/grid-editor/GridTable.tsx
Benjamin Admin 4a8d43fd71
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 32s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 2m8s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 18s
feat: display detected text colors in grid editor UI
- Add color/color_name/recovered fields to OcrWordBox type
- GridTable: show colored text + left-edge color indicator strip
- GridEditor: show color stats and recovered count in summary bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 01:03:09 +01:00

176 lines
7.4 KiB
TypeScript

'use client'
import { useCallback, useRef } from 'react'
import type { GridZone } from './types'
interface GridTableProps {
zone: GridZone
selectedCell: string | null
onSelectCell: (cellId: string) => void
onCellTextChange: (cellId: string, text: string) => void
onToggleColumnBold: (zoneIndex: number, colIndex: number) => void
onToggleRowHeader: (zoneIndex: number, rowIndex: number) => void
onNavigate: (cellId: string, direction: 'up' | 'down' | 'left' | 'right') => void
}
export function GridTable({
zone,
selectedCell,
onSelectCell,
onCellTextChange,
onToggleColumnBold,
onToggleRowHeader,
onNavigate,
}: GridTableProps) {
const tableRef = useRef<HTMLTableElement>(null)
const handleKeyDown = useCallback(
(e: React.KeyboardEvent, cellId: string) => {
if (e.key === 'Tab') {
e.preventDefault()
onNavigate(cellId, e.shiftKey ? 'left' : 'right')
} else if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
onNavigate(cellId, 'down')
} else if (e.key === 'ArrowUp' && e.altKey) {
e.preventDefault()
onNavigate(cellId, 'up')
} else if (e.key === 'ArrowDown' && e.altKey) {
e.preventDefault()
onNavigate(cellId, 'down')
} else if (e.key === 'Escape') {
;(e.target as HTMLElement).blur()
}
},
[onNavigate],
)
// Build row→col cell lookup
const cellMap = new Map<string, (typeof zone.cells)[0]>()
for (const cell of zone.cells) {
cellMap.set(`${cell.row_index}_${cell.col_index}`, cell)
}
/** Dominant non-black color from a cell's word_boxes, or null. */
const getCellColor = (cell: (typeof zone.cells)[0] | undefined): string | null => {
if (!cell?.word_boxes?.length) return null
for (const wb of cell.word_boxes) {
if (wb.color_name && wb.color_name !== 'black' && wb.color) {
return wb.color
}
}
return null
}
const isBoxZone = zone.zone_type === 'box'
return (
<div className={`overflow-x-auto ${isBoxZone ? 'border-2 border-gray-400 dark:border-gray-500 rounded-lg' : ''}`}>
{/* Zone label */}
<div className="flex items-center gap-2 px-2 py-1 text-xs text-gray-500 dark:text-gray-400">
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-medium ${
isBoxZone
? 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-300 border border-amber-200 dark:border-amber-800'
: 'bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border border-gray-200 dark:border-gray-700'
}`}>
{isBoxZone ? 'Box' : 'Inhalt'} Zone {zone.zone_index}
</span>
<span>{zone.columns.length} Spalten, {zone.rows.length} Zeilen, {zone.cells.length} Zellen</span>
</div>
<table ref={tableRef} className="w-full border-collapse text-sm">
{/* Column headers */}
<thead>
<tr>
{/* Row number header */}
<th className="w-8 px-1 py-1.5 text-[10px] text-gray-400 dark:text-gray-500 font-normal border-b border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50" />
{zone.columns.map((col) => (
<th
key={col.index}
className={`px-2 py-1.5 text-xs font-medium border-b border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
col.bold ? 'text-teal-700 dark:text-teal-300' : 'text-gray-600 dark:text-gray-400'
}`}
onClick={() => onToggleColumnBold(zone.zone_index, col.index)}
title={`Spalte ${col.index + 1} — Klick fuer Fett-Toggle`}
>
<div className="flex items-center gap-1 justify-center">
<span>{col.label}</span>
{col.bold && (
<span className="text-[9px] px-1 py-0 rounded bg-teal-100 dark:bg-teal-900/40 text-teal-600 dark:text-teal-400">
B
</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{zone.rows.map((row) => (
<tr key={row.index} className={row.is_header ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}>
{/* Row number */}
<td
className={`w-8 px-1 py-1 text-center text-[10px] border-b border-r border-gray-200 dark:border-gray-700 cursor-pointer select-none transition-colors hover:bg-gray-100 dark:hover:bg-gray-700 ${
row.is_header
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium'
: 'bg-gray-50 dark:bg-gray-800/50 text-gray-400 dark:text-gray-500'
}`}
onClick={() => onToggleRowHeader(zone.zone_index, row.index)}
title={`Zeile ${row.index + 1} — Klick fuer Header-Toggle`}
>
{row.index + 1}
{row.is_header && <span className="block text-[8px]">H</span>}
</td>
{/* Cells */}
{zone.columns.map((col) => {
const cell = cellMap.get(`${row.index}_${col.index}`)
const cellId = cell?.cell_id ?? `Z${zone.zone_index}_R${String(row.index).padStart(2, '0')}_C${col.index}`
const isSelected = selectedCell === cellId
const isBold = col.bold || cell?.is_bold
const isLowConf = cell && cell.confidence > 0 && cell.confidence < 60
const cellColor = getCellColor(cell)
return (
<td
key={col.index}
className={`border-b border-r border-gray-200 dark:border-gray-700 p-0 transition-shadow ${
isSelected ? 'ring-2 ring-teal-500 ring-inset z-10 relative' : ''
} ${isLowConf ? 'bg-amber-50/50 dark:bg-amber-900/10' : ''}`}
>
<div className="flex items-center">
{cellColor && (
<span
className="flex-shrink-0 w-1.5 h-full min-h-[28px] rounded-l-sm"
style={{ backgroundColor: cellColor }}
title={`Farbe: ${cell?.word_boxes?.find(wb => wb.color_name !== 'black')?.color_name}`}
/>
)}
<input
id={`cell-${cellId}`}
type="text"
value={cell?.text ?? ''}
onChange={(e) => {
if (cell) onCellTextChange(cellId, e.target.value)
}}
onFocus={() => onSelectCell(cellId)}
onKeyDown={(e) => handleKeyDown(e, cellId)}
className={`w-full px-2 py-1.5 bg-transparent border-0 outline-none ${
isBold ? 'font-bold' : 'font-normal'
} ${row.is_header ? 'text-base' : 'text-sm'}`}
style={cellColor ? { color: cellColor } : undefined}
spellCheck={false}
/>
</div>
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
)
}