feat: add Kombi-Vergleich mode for side-by-side Paddle vs RapidOCR comparison
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 33s
CI / test-go-edu-search (push) Successful in 26s
CI / test-python-klausur (push) Failing after 1m55s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 21s

Add /rapid-kombi backend endpoint using local RapidOCR + Tesseract merge,
KombiCompareStep component for parallel execution and side-by-side overlay,
and wordResultOverride prop on OverlayReconstruction for direct data injection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-14 07:59:06 +01:00
parent c2c082d4b4
commit a994ddee83
6 changed files with 504 additions and 35 deletions

View File

@@ -10,6 +10,8 @@ const KLAUSUR_API = '/klausur-api'
interface OverlayReconstructionProps {
sessionId: string | null
onNext: () => void
/** When set, use this data directly instead of fetching from the session API. */
wordResultOverride?: { cells: GridCell[]; image_width: number; image_height: number; [key: string]: unknown }
}
interface EditableCell {
@@ -24,7 +26,7 @@ interface EditableCell {
type UndoAction = { cellId: string; oldText: string; newText: string }
export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructionProps) {
export function OverlayReconstruction({ sessionId, onNext, wordResultOverride }: OverlayReconstructionProps) {
const [status, setStatus] = useState<'loading' | 'ready' | 'saving' | 'saved' | 'error'>('loading')
const [error, setError] = useState('')
const [cells, setCells] = useState<EditableCell[]>([])
@@ -78,10 +80,39 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
// Load session data
useEffect(() => {
if (wordResultOverride) {
applyWordResult(wordResultOverride)
return
}
if (!sessionId) return
loadSessionData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])
}, [sessionId, wordResultOverride])
const applyWordResult = (wordResult: { cells: GridCell[]; image_width: number; image_height: number; [key: string]: unknown }) => {
const rawGridCells: GridCell[] = wordResult.cells || []
setGridCells(rawGridCells)
const editableCells: EditableCell[] = rawGridCells.map(c => ({
cellId: c.cell_id,
text: c.text,
originalText: c.text,
bboxPct: c.bbox_pct,
colType: c.col_type,
rowIndex: c.row_index,
colIndex: c.col_index,
}))
setCells(editableCells)
setEditedTexts(new Map())
setUndoStack([])
setRedoStack([])
if (wordResult.image_width && wordResult.image_height) {
setImageNaturalSize({ w: wordResult.image_width, h: wordResult.image_height })
}
setStatus('ready')
}
const loadSessionData = async () => {
if (!sessionId) return
@@ -98,33 +129,11 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
return
}
const rawGridCells: GridCell[] = wordResult.cells || []
setGridCells(rawGridCells)
const editableCells: EditableCell[] = rawGridCells.map(c => ({
cellId: c.cell_id,
text: c.text,
originalText: c.text,
bboxPct: c.bbox_pct,
colType: c.col_type,
rowIndex: c.row_index,
colIndex: c.col_index,
}))
setCells(editableCells)
setEditedTexts(new Map())
setUndoStack([])
setRedoStack([])
applyWordResult(wordResult as unknown as { cells: GridCell[]; image_width: number; image_height: number })
// Load rows
const rowResult: RowResult | undefined = data.row_result
if (rowResult?.rows) setRows(rowResult.rows)
// Store image dimensions
if (wordResult.image_width && wordResult.image_height) {
setImageNaturalSize({ w: wordResult.image_width, h: wordResult.image_height })
}
setStatus('ready')
} catch (e: unknown) {
setError(e instanceof Error ? e.message : String(e))
setStatus('error')