fix(ocr-compare): Replace Ollama call in grid analysis with heuristic from comparison results

Ollama crashes when two concurrent vision requests hit the 32B model
(compare-ocr + analyze-grid). The grid analysis was redundantly calling
Ollama again even though compare-ocr already extracted all vocabulary.

- compare-ocr now saves vocabulary in session for reuse
- analyze-grid builds grid from session data (no Ollama, instant response)
- Grid button disabled until comparison results are available
- Added export-to-editor functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-09 18:08:13 +01:00
parent 09dd1487b4
commit 2f8ffb7352
2 changed files with 575 additions and 907 deletions

View File

@@ -139,6 +139,10 @@ export default function OCRComparePage() {
const [currentBlockNumber, setCurrentBlockNumber] = useState(1)
const [blockReviewData, setBlockReviewData] = useState<Record<number, BlockReviewData>>({})
// Export State
const [isExporting, setIsExporting] = useState(false)
const [exportSuccess, setExportSuccess] = useState(false)
const KLAUSUR_API = '/klausur-api'
// Load session history
@@ -535,6 +539,72 @@ export default function OCRComparePage() {
}
}, [gridData])
// Export to Worksheet Editor
const handleExportToEditor = useCallback(async () => {
if (!gridData || !sessionId) return
setIsExporting(true)
setExportSuccess(false)
try {
// Convert grid cells (percent coordinates) to mm for A4
const A4_WIDTH_MM = 210
const A4_HEIGHT_MM = 297
const words = gridData.cells.flat()
.filter(cell => cell.status !== 'empty' && cell.text)
.map(cell => ({
text: cell.text,
x_mm: (cell.x / 100) * A4_WIDTH_MM,
y_mm: (cell.y / 100) * A4_HEIGHT_MM,
width_mm: (cell.width / 100) * A4_WIDTH_MM,
height_mm: (cell.height / 100) * A4_HEIGHT_MM,
column_type: cell.column_type || 'unknown',
logical_row: cell.row,
confidence: cell.confidence,
}))
const detectedColumns = gridData.column_types.map((type, idx) => ({
column_type: type,
x_start_mm: (gridData.column_boundaries[idx] / 100) * A4_WIDTH_MM,
x_end_mm: (gridData.column_boundaries[idx + 1] / 100) * A4_WIDTH_MM,
}))
const exportData = {
version: '1.0',
source: 'ocr-compare',
exported_at: new Date().toISOString(),
session_id: sessionId,
page_number: selectedPage + 1,
page_dimensions: {
width_mm: A4_WIDTH_MM,
height_mm: A4_HEIGHT_MM,
format: 'A4',
},
words,
detected_columns: detectedColumns,
}
const res = await fetch(
`${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/ocr-export/${selectedPage + 1}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(exportData),
}
)
if (res.ok) {
setExportSuccess(true)
setTimeout(() => setExportSuccess(false), 3000)
}
} catch (e) {
console.error('Export failed:', e)
} finally {
setIsExporting(false)
}
}, [gridData, sessionId, selectedPage, KLAUSUR_API])
// Count non-empty blocks
const nonEmptyBlockCount = useMemo(() => {
if (!gridData) return 0
@@ -831,6 +901,35 @@ export default function OCRComparePage() {
)}
</button>
)}
{/* Export to Editor Button */}
<button
onClick={handleExportToEditor}
disabled={isExporting}
className={`w-full px-4 py-2 rounded-lg font-medium text-sm transition-colors ${
exportSuccess
? 'bg-green-100 text-green-700'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
<span className="flex items-center justify-center gap-2">
{isExporting ? (
<svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : exportSuccess ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
)}
{exportSuccess ? 'Exportiert!' : 'Zum Editor exportieren'}
</span>
</button>
</div>
)}
</div>
@@ -1015,11 +1114,25 @@ export default function OCRComparePage() {
</div>
<div className="p-4">
{thumbnails[selectedPage] ? (
<img
src={thumbnails[selectedPage]}
alt={`Seite ${selectedPage + 1}`}
className={`rounded-lg border border-slate-200 ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
/>
gridData && showGridOverlay ? (
<GridOverlay
grid={gridData}
imageUrl={thumbnails[selectedPage]}
onCellClick={handleCellClick}
selectedCell={selectedCell}
showEmpty={false}
showNumbers={blockReviewMode}
showTextLabels={true}
highlightedBlockNumber={blockReviewMode ? currentBlockNumber : null}
className={`rounded-lg border border-slate-200 overflow-hidden ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
/>
) : (
<img
src={thumbnails[selectedPage]}
alt={`Seite ${selectedPage + 1}`}
className={`rounded-lg border border-slate-200 ${isFullscreen ? 'max-h-[80vh] mx-auto' : 'w-full max-w-2xl mx-auto'}`}
/>
)
) : (
<div className="h-96 bg-slate-100 rounded-lg flex items-center justify-center text-slate-500">
Kein Bild verfuegbar
@@ -1116,6 +1229,7 @@ export default function OCRComparePage() {
selectedCell={selectedCell}
showEmpty={false}
showNumbers={blockReviewMode}
showTextLabels={!blockReviewMode}
highlightedBlockNumber={blockReviewMode ? currentBlockNumber : null}
className="rounded-lg border border-slate-200 overflow-hidden"
/>

File diff suppressed because it is too large Load Diff