Files
Benjamin Admin c3f1547e32
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 27s
CI / test-go-edu-search (push) Successful in 28s
CI / test-python-klausur (push) Failing after 2m1s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 17s
feat: add Excel-like grid editor for OCR overlay (Kombi mode step 6)
Backend: new grid_editor_api.py with build-grid endpoint that detects
bordered boxes, splits page into zones, clusters columns/rows per zone
from Kombi word positions. New DB column grid_editor_result JSONB.

Frontend: GridEditor component with editable HTML tables per zone,
column bold toggle, header row toggle, undo/redo, keyboard navigation
(Tab/Enter/Arrow), image overlay verification, and save/load.

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

99 lines
3.3 KiB
TypeScript

'use client'
import type { StructuredGrid } from './types'
const KLAUSUR_API = '/klausur-api'
interface GridImageOverlayProps {
sessionId: string
grid: StructuredGrid
}
const ZONE_COLORS = [
{ border: 'rgba(20,184,166,0.7)', fill: 'rgba(20,184,166,0.05)' }, // teal
{ border: 'rgba(245,158,11,0.7)', fill: 'rgba(245,158,11,0.05)' }, // amber
{ border: 'rgba(99,102,241,0.7)', fill: 'rgba(99,102,241,0.05)' }, // indigo
{ border: 'rgba(236,72,153,0.7)', fill: 'rgba(236,72,153,0.05)' }, // pink
]
export function GridImageOverlay({ sessionId, grid }: GridImageOverlayProps) {
const imgUrl = `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped`
return (
<div className="relative w-full overflow-auto border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-100 dark:bg-gray-900">
<div className="relative inline-block">
{/* Source image */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={imgUrl}
alt="OCR Scan"
className="block max-w-full"
style={{ imageRendering: 'auto' }}
/>
{/* SVG overlay */}
<svg
className="absolute inset-0 w-full h-full pointer-events-none"
viewBox={`0 0 ${grid.image_width} ${grid.image_height}`}
preserveAspectRatio="xMinYMin meet"
>
{grid.zones.map((zone) => {
const colors = ZONE_COLORS[zone.zone_index % ZONE_COLORS.length]
const b = zone.bbox_px
return (
<g key={zone.zone_index}>
{/* Zone border */}
<rect
x={b.x} y={b.y} width={b.w} height={b.h}
fill={colors.fill}
stroke={colors.border}
strokeWidth={zone.zone_type === 'box' ? 3 : 1.5}
strokeDasharray={zone.zone_type === 'box' ? undefined : '6 3'}
/>
{/* Column separators */}
{zone.columns.slice(1).map((col) => (
<line
key={`col-${col.index}`}
x1={col.x_min_px} y1={b.y}
x2={col.x_min_px} y2={b.y + b.h}
stroke={colors.border}
strokeWidth={1}
strokeDasharray="4 2"
/>
))}
{/* Row separators */}
{zone.rows.slice(1).map((row) => (
<line
key={`row-${row.index}`}
x1={b.x} y1={row.y_min_px}
x2={b.x + b.w} y2={row.y_min_px}
stroke={colors.border}
strokeWidth={0.5}
strokeDasharray="3 3"
opacity={0.5}
/>
))}
{/* Zone label */}
<text
x={b.x + 4} y={b.y + 14}
fill={colors.border}
fontSize={12}
fontWeight="bold"
fontFamily="monospace"
>
{zone.zone_type === 'box' ? 'BOX' : 'CONTENT'} Z{zone.zone_index}
{' '}({zone.columns.length}x{zone.rows.length})
</text>
</g>
)
})}
</svg>
</div>
</div>
)
}