feat(grid-editor): add manual cell color control via right-click menu
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 23s
CI / test-go-edu-search (push) Successful in 25s
CI / test-python-klausur (push) Failing after 1m53s
CI / test-python-agent-core (push) Successful in 13s
CI / test-nodejs-website (push) Successful in 15s
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 23s
CI / test-go-edu-search (push) Successful in 25s
CI / test-python-klausur (push) Failing after 1m53s
CI / test-python-agent-core (push) Successful in 13s
CI / test-nodejs-website (push) Successful in 15s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<HTMLDivElement>(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 && (
|
||||
<span
|
||||
@@ -519,6 +544,7 @@ export function GridTable({
|
||||
className={`w-full px-2 bg-transparent border-0 outline-none ${
|
||||
isBold ? 'font-bold' : 'font-normal'
|
||||
}`}
|
||||
style={{ color: cellColor || undefined }}
|
||||
spellCheck={false}
|
||||
/>
|
||||
)}
|
||||
@@ -530,6 +556,53 @@ export function GridTable({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Color context menu (right-click) */}
|
||||
{colorMenu && onSetCellColor && (
|
||||
<div
|
||||
className="fixed inset-0 z-50"
|
||||
onClick={() => setColorMenu(null)}
|
||||
onContextMenu={(e) => { e.preventDefault(); setColorMenu(null) }}
|
||||
>
|
||||
<div
|
||||
className="absolute bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[140px]"
|
||||
style={{ left: colorMenu.x, top: colorMenu.y }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="px-3 py-1 text-[10px] text-gray-400 dark:text-gray-500 font-medium uppercase tracking-wider">
|
||||
Textfarbe
|
||||
</div>
|
||||
{COLOR_OPTIONS.map((opt) => (
|
||||
<button
|
||||
key={opt.label}
|
||||
className="w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2"
|
||||
onClick={() => {
|
||||
onSetCellColor(colorMenu.cellId, opt.value)
|
||||
setColorMenu(null)
|
||||
}}
|
||||
>
|
||||
{opt.value ? (
|
||||
<span className="w-3 h-3 rounded-full flex-shrink-0" style={{ backgroundColor: opt.value }} />
|
||||
) : (
|
||||
<span className="w-3 h-3 rounded-full flex-shrink-0 border border-gray-300 dark:border-gray-600" />
|
||||
)}
|
||||
<span>{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="border-t border-gray-100 dark:border-gray-700 mt-1 pt-1">
|
||||
<button
|
||||
className="w-full px-3 py-1.5 text-xs text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400"
|
||||
onClick={() => {
|
||||
onSetCellColor(colorMenu.cellId, undefined)
|
||||
setColorMenu(null)
|
||||
}}
|
||||
>
|
||||
Farbe zuruecksetzen (OCR)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -438,6 +440,7 @@ export function StepGridReview({ sessionId, onNext, saveRef }: StepGridReviewPro
|
||||
<span>Pfeiltasten: Navigation</span>
|
||||
<span>Ctrl+Klick: Mehrfachauswahl</span>
|
||||
<span>Ctrl+B: Fett</span>
|
||||
<span>Rechtsklick: Farbe</span>
|
||||
<span>Ctrl+Z/Y: Undo/Redo</span>
|
||||
<span>Ctrl+S: Speichern</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user