diff --git a/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx b/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx index 3ac4d69..2b249ea 100644 --- a/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx +++ b/admin-lehrer/components/ocr-pipeline/StepLlmReview.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useEffect, useRef, useState } from 'react' -import type { GridResult, WordEntry, ColumnMeta } from '@/app/(admin)/ai/ocr-pipeline/types' +import type { GridCell, GridResult, WordEntry, ColumnMeta } from '@/app/(admin)/ai/ocr-pipeline/types' const KLAUSUR_API = '/klausur-api' @@ -83,9 +83,29 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { // Image const [imageNaturalSize, setImageNaturalSize] = useState<{ w: number; h: number } | null>(null) + // Overlay view state + const [viewMode, setViewMode] = useState<'table' | 'overlay'>('table') + const [fontScale, setFontScale] = useState(0.7) + const [leftPaddingPct, setLeftPaddingPct] = useState(0) + const [globalBold, setGlobalBold] = useState(false) + const [cells, setCells] = useState([]) + const reconRef = useRef(null) + const [reconWidth, setReconWidth] = useState(0) + const tableRef = useRef(null) const activeRowRef = useRef(null) + // Track reconstruction container width for font size calculation + useEffect(() => { + const el = reconRef.current + if (!el) return + const obs = new ResizeObserver(entries => { + for (const entry of entries) setReconWidth(entry.contentRect.width) + }) + obs.observe(el) + return () => obs.disconnect() + }, [viewMode]) + // Load session data on mount useEffect(() => { if (!sessionId) return @@ -111,6 +131,7 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { const entries = wordResult.vocab_entries || wordResult.entries || [] setVocabEntries(entries) setColumnsUsed(wordResult.columns_used || []) + setCells(wordResult.cells || []) // Check if LLM review was already run const llmReview = wordResult.llm_review @@ -383,6 +404,22 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) { const pct = progress ? Math.round((progress.current / progress.total) * 100) : 0 + /** Handle inline edit of a cell in the overlay */ + const handleCellEdit = (cellId: string, rowIndex: number, newText: string | null) => { + if (newText === null) return + setCells(prev => prev.map(c => c.cell_id === cellId ? { ...c, text: newText } : c)) + // Also update vocabEntries if this cell maps to a known field + const cell = cells.find(c => c.cell_id === cellId) + if (cell) { + const field = COL_TYPE_TO_FIELD[cell.col_type] + if (field) { + setVocabEntries(prev => prev.map((e, i) => + i === rowIndex ? { ...e, [field]: newText } : e + )) + } + } + } + // --- Ready / Running / Done: 2-column layout --- return (
@@ -439,8 +476,66 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) {
)} - {/* 2-column layout: Image + Table */} -
+ {/* View mode toggle */} +
+ + +
+ + {/* Overlay toolbar */} + {viewMode === 'overlay' && ( +
+ + + +
+ )} + + {/* 2-column layout: Image + Table/Overlay */} +
{/* Left: Dewarped Image with highlight overlay */}
@@ -472,93 +567,145 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) {
- {/* Right: Full vocabulary table */} -
-
- {columnsUsed.length === 1 && columnsUsed[0]?.type === 'column_text' ? 'Tabelle' : 'Vokabeltabelle'} ({vocabEntries.length} Eintraege) -
-
-
- - - - - {columnsUsed.length > 0 ? ( - columnsUsed.map((col, i) => { - const field = COL_TYPE_TO_FIELD[col.type] - if (!field) return null - return ( - - ) - }) - ) : ( - <> - - - - - )} - - - - - {vocabEntries.map((entry, idx) => { - const rowStatus = getRowStatus(idx) - const rowChanges = correctedMap.get(idx) - - const rowBg = { - pending: '', - active: 'bg-yellow-50 dark:bg-yellow-900/20', - reviewed: '', - corrected: 'bg-teal-50/50 dark:bg-teal-900/10', - skipped: 'bg-gray-50 dark:bg-gray-800/50', - }[rowStatus] - - return ( - - + {/* Right: Table or Overlay */} +
+ {viewMode === 'table' ? ( + <> +
+ {columnsUsed.length === 1 && columnsUsed[0]?.type === 'column_text' ? 'Tabelle' : 'Vokabeltabelle'} ({vocabEntries.length} Eintraege) +
+
+
+
# - {FIELD_LABELS[field] || field} - ENDEBeispielStatus
{idx}
+ + + {columnsUsed.length > 0 ? ( columnsUsed.map((col, i) => { const field = COL_TYPE_TO_FIELD[col.type] if (!field) return null - const text = (entry as Record)[field] as string || '' return ( - + ) }) ) : ( <> - - - + + + )} - + + + + {vocabEntries.map((entry, idx) => { + const rowStatus = getRowStatus(idx) + const rowChanges = correctedMap.get(idx) + + const rowBg = { + pending: '', + active: 'bg-yellow-50 dark:bg-yellow-900/20', + reviewed: '', + corrected: 'bg-teal-50/50 dark:bg-teal-900/10', + skipped: 'bg-gray-50 dark:bg-gray-800/50', + }[rowStatus] + + return ( + + + {columnsUsed.length > 0 ? ( + columnsUsed.map((col, i) => { + const field = COL_TYPE_TO_FIELD[col.type] + if (!field) return null + const text = (entry as Record)[field] as string || '' + return ( + + ) + }) + ) : ( + <> + + + + + )} + + + ) + })} + +
# - - + {FIELD_LABELS[field] || field} + - - - - - - ENDEBeispiel - - Status
{idx} + + + + + + + + + +
+
+
+ + ) : ( + <> +
+ Text-Rekonstruktion ({cells.filter(c => c.text).length} Zellen) +
+
+
+ {cells.map(cell => { + if (!cell.bbox_pct || !cell.text) return null + const aspect = imageNaturalSize ? imageNaturalSize.h / imageNaturalSize.w : 4 / 3 + const containerH = reconWidth * aspect + const cellHeightPx = containerH * (cell.bbox_pct.h / 100) + const fontSize = Math.max(6, cellHeightPx * fontScale) + return ( + handleCellEdit(cell.cell_id, cell.row_index, e.currentTarget.textContent)} + > + {cell.text} + ) })} - - -
-
+
+
+ + )}