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
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>
99 lines
3.3 KiB
TypeScript
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>
|
|
)
|
|
}
|