From 9d34c5201e032bb2258dd4281dd9ae5b1a6d3933 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 24 Mar 2026 08:51:18 +0100 Subject: [PATCH] feat(grid-editor): add manual cell color control via right-click menu Users can now right-click any cell to set text color (red, green, blue, orange, purple, black) or remove the color bar without changing text. A "reset" option restores the OCR-detected color. This enables accurate Ground Truth marking when OCR assigns colors to wrong cells. Co-Authored-By: Claude Opus 4.6 --- .../components/grid-editor/GridTable.tsx | 81 ++++++++++++++++++- admin-lehrer/components/grid-editor/types.ts | 2 + .../components/grid-editor/useGridEditor.ts | 34 ++++++++ .../ocr-pipeline/StepGridReview.tsx | 3 + 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/admin-lehrer/components/grid-editor/GridTable.tsx b/admin-lehrer/components/grid-editor/GridTable.tsx index bb8839c..0b5d58b 100644 --- a/admin-lehrer/components/grid-editor/GridTable.tsx +++ b/admin-lehrer/components/grid-editor/GridTable.tsx @@ -18,8 +18,19 @@ interface GridTableProps { onAddColumn?: (zoneIndex: number, afterColIndex: number) => void onDeleteRow?: (zoneIndex: number, rowIndex: number) => void onAddRow?: (zoneIndex: number, afterRowIndex: number) => void + onSetCellColor?: (cellId: string, color: string | null | undefined) => void } +/** Color palette for the right-click cell color menu. */ +const COLOR_OPTIONS: { label: string; value: string | null }[] = [ + { label: 'Rot', value: '#dc2626' }, + { label: 'Gruen', value: '#16a34a' }, + { label: 'Blau', value: '#2563eb' }, + { label: 'Orange', value: '#ea580c' }, + { label: 'Lila', value: '#9333ea' }, + { label: 'Schwarz', value: null }, +] + /** Gutter width for row numbers (px). */ const ROW_NUM_WIDTH = 36 @@ -44,9 +55,11 @@ export function GridTable({ onAddColumn, onDeleteRow, onAddRow, + onSetCellColor, }: GridTableProps) { const containerRef = useRef(null) const [containerWidth, setContainerWidth] = useState(0) + const [colorMenu, setColorMenu] = useState<{ cellId: string; x: number; y: number } | null>(null) // ---------------------------------------------------------------- // Observe container width for scaling @@ -148,9 +161,13 @@ export function GridTable({ cellMap.set(`${cell.row_index}_${cell.col_index}`, cell) } - /** Dominant non-black color from a cell's word_boxes, or null. */ + /** Dominant non-black color from a cell's word_boxes, or null. + * `color_override` takes priority when set. */ const getCellColor = (cell: (typeof zone.cells)[0] | undefined): string | null => { - if (!cell?.word_boxes?.length) return null + if (!cell) return null + // Manual override: explicit color or null (= "clear color bar") + if (cell.color_override !== undefined) return cell.color_override ?? 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 @@ -450,9 +467,11 @@ export function GridTable({ // plain input when they diverge. const wbText = cell?.word_boxes?.map((wb) => wb.text).join(' ') ?? '' const textMatches = !cell?.text || wbText === cell.text - // Color bar only when word_boxes still match edited text - const cellColor = textMatches ? getCellColor(cell) : null + // Color: prefer manual override, else word_boxes when text matches + const hasOverride = cell?.color_override !== undefined + const cellColor = hasOverride ? getCellColor(cell) : (textMatches ? getCellColor(cell) : null) const hasColoredWords = + !hasOverride && textMatches && (cell?.word_boxes?.some( (wb) => wb.color_name && wb.color_name !== 'black', @@ -467,6 +486,12 @@ export function GridTable({ isLowConf && !isMultiSelected ? 'bg-amber-50/50 dark:bg-amber-900/10' : '' } ${row.is_header && !isMultiSelected ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`} style={{ height: `${rowH}px` }} + onContextMenu={(e) => { + if (onSetCellColor) { + e.preventDefault() + setColorMenu({ cellId, x: e.clientX, y: e.clientY }) + } + }} > {cellColor && ( )} @@ -530,6 +556,53 @@ export function GridTable({ ) })} + + {/* Color context menu (right-click) */} + {colorMenu && onSetCellColor && ( +
setColorMenu(null)} + onContextMenu={(e) => { e.preventDefault(); setColorMenu(null) }} + > +
e.stopPropagation()} + > +
+ Textfarbe +
+ {COLOR_OPTIONS.map((opt) => ( + + ))} +
+ +
+
+
+ )} ) } diff --git a/admin-lehrer/components/grid-editor/types.ts b/admin-lehrer/components/grid-editor/types.ts index eb1bd75..a8392a6 100644 --- a/admin-lehrer/components/grid-editor/types.ts +++ b/admin-lehrer/components/grid-editor/types.ts @@ -112,6 +112,8 @@ export interface GridEditorCell { word_boxes: OcrWordBox[] ocr_engine: string is_bold: boolean + /** Manual color override: hex string or null to clear. */ + color_override?: string | null } /** Layout dividers for the visual column/margin editor on the original image. */ diff --git a/admin-lehrer/components/grid-editor/useGridEditor.ts b/admin-lehrer/components/grid-editor/useGridEditor.ts index 56a70b7..39275ec 100644 --- a/admin-lehrer/components/grid-editor/useGridEditor.ts +++ b/admin-lehrer/components/grid-editor/useGridEditor.ts @@ -752,6 +752,39 @@ export function useGridEditor(sessionId: string | null) { setSelectedCells(new Set()) }, []) + /** + * Set a manual color override on a cell. + * - hex string (e.g. "#dc2626"): force text color + * - null: force no color (clear bar) + * - undefined: remove override, restore OCR-detected color + */ + const setCellColor = useCallback( + (cellId: string, color: string | null | undefined) => { + if (!grid) return + pushUndo(grid.zones) + setGrid((prev) => { + if (!prev) return prev + return { + ...prev, + zones: prev.zones.map((zone) => ({ + ...zone, + cells: zone.cells.map((cell) => { + if (cell.cell_id !== cellId) return cell + if (color === undefined) { + // Remove override entirely — restore OCR behavior + const { color_override: _, ...rest } = cell + return rest + } + return { ...cell, color_override: color } + }), + })), + } + }) + setDirty(true) + }, + [grid, pushUndo], + ) + /** Toggle bold on all selected cells (and their columns). */ const toggleSelectedBold = useCallback(() => { if (!grid || selectedCells.size === 0) return @@ -881,5 +914,6 @@ export function useGridEditor(sessionId: string | null) { clearCellSelection, toggleSelectedBold, autoCorrectColumnPatterns, + setCellColor, } } diff --git a/admin-lehrer/components/ocr-pipeline/StepGridReview.tsx b/admin-lehrer/components/ocr-pipeline/StepGridReview.tsx index e53c2ca..e3e20be 100644 --- a/admin-lehrer/components/ocr-pipeline/StepGridReview.tsx +++ b/admin-lehrer/components/ocr-pipeline/StepGridReview.tsx @@ -56,6 +56,7 @@ export function StepGridReview({ sessionId, onNext, saveRef }: StepGridReviewPro clearCellSelection, toggleSelectedBold, autoCorrectColumnPatterns, + setCellColor, } = useGridEditor(sessionId) const [showImage, setShowImage] = useState(true) @@ -399,6 +400,7 @@ export function StepGridReview({ sessionId, onNext, saveRef }: StepGridReviewPro onAddColumn={addColumn} onDeleteRow={deleteRow} onAddRow={addRow} + onSetCellColor={setCellColor} /> ))} @@ -438,6 +440,7 @@ export function StepGridReview({ sessionId, onNext, saveRef }: StepGridReviewPro Pfeiltasten: Navigation Ctrl+Klick: Mehrfachauswahl Ctrl+B: Fett + Rechtsklick: Farbe Ctrl+Z/Y: Undo/Redo Ctrl+S: Speichern