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
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:
@@ -11,12 +11,13 @@ import { StepRowDetection } from '@/components/ocr-pipeline/StepRowDetection'
|
||||
import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecognition'
|
||||
import { OverlayReconstruction } from '@/components/ocr-overlay/OverlayReconstruction'
|
||||
import { PaddleDirectStep } from '@/components/ocr-overlay/PaddleDirectStep'
|
||||
import { OVERLAY_PIPELINE_STEPS, PADDLE_DIRECT_STEPS, KOMBI_STEPS, DOCUMENT_CATEGORIES, dbStepToOverlayUi, type PipelineStep, type SessionListItem, type DocumentCategory } from './types'
|
||||
import { KombiCompareStep } from '@/components/ocr-overlay/KombiCompareStep'
|
||||
import { OVERLAY_PIPELINE_STEPS, PADDLE_DIRECT_STEPS, KOMBI_STEPS, KOMBI_COMPARE_STEPS, DOCUMENT_CATEGORIES, dbStepToOverlayUi, type PipelineStep, type SessionListItem, type DocumentCategory } from './types'
|
||||
|
||||
const KLAUSUR_API = '/klausur-api'
|
||||
|
||||
export default function OcrOverlayPage() {
|
||||
const [mode, setMode] = useState<'pipeline' | 'paddle-direct' | 'kombi'>('pipeline')
|
||||
const [mode, setMode] = useState<'pipeline' | 'paddle-direct' | 'kombi' | 'kombi-compare'>('pipeline')
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [sessionId, setSessionId] = useState<string | null>(null)
|
||||
const [sessionName, setSessionName] = useState<string>('')
|
||||
@@ -63,14 +64,15 @@ export default function OcrOverlayPage() {
|
||||
setSessionName(data.name || data.filename || '')
|
||||
setActiveCategory(data.document_category || undefined)
|
||||
|
||||
// Check if this session was processed with paddle_direct or kombi
|
||||
// Check if this session was processed with paddle_direct, kombi, or rapid_kombi
|
||||
const ocrEngine = data.word_result?.ocr_engine
|
||||
const isPaddleDirect = ocrEngine === 'paddle_direct'
|
||||
const isKombi = ocrEngine === 'kombi'
|
||||
const isRapidKombi = ocrEngine === 'rapid_kombi'
|
||||
|
||||
if (isPaddleDirect || isKombi) {
|
||||
const m = isKombi ? 'kombi' : 'paddle-direct'
|
||||
const baseSteps = isKombi ? KOMBI_STEPS : PADDLE_DIRECT_STEPS
|
||||
if (isPaddleDirect || isKombi || isRapidKombi) {
|
||||
const m = isKombi ? 'kombi' : isPaddleDirect ? 'paddle-direct' : 'kombi-compare'
|
||||
const baseSteps = isKombi ? KOMBI_STEPS : isRapidKombi ? KOMBI_COMPARE_STEPS : PADDLE_DIRECT_STEPS
|
||||
setMode(m)
|
||||
setSteps(
|
||||
baseSteps.map((s, i) => ({
|
||||
@@ -105,7 +107,7 @@ export default function OcrOverlayPage() {
|
||||
if (sessionId === sid) {
|
||||
setSessionId(null)
|
||||
setCurrentStep(0)
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -162,7 +164,7 @@ export default function OcrOverlayPage() {
|
||||
const handleNext = () => {
|
||||
if (currentStep >= steps.length - 1) {
|
||||
// Last step completed — return to session list
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||
setCurrentStep(0)
|
||||
setSessionId(null)
|
||||
@@ -191,7 +193,7 @@ export default function OcrOverlayPage() {
|
||||
setSessionId(null)
|
||||
setSessionName('')
|
||||
setCurrentStep(0)
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
const baseSteps = mode === 'kombi' ? KOMBI_STEPS : mode === 'kombi-compare' ? KOMBI_COMPARE_STEPS : mode === 'paddle-direct' ? PADDLE_DIRECT_STEPS : OVERLAY_PIPELINE_STEPS
|
||||
setSteps(baseSteps.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||
}
|
||||
|
||||
@@ -230,7 +232,7 @@ export default function OcrOverlayPage() {
|
||||
}, [sessionId, goToStep])
|
||||
|
||||
const renderStep = () => {
|
||||
if (mode === 'paddle-direct' || mode === 'kombi') {
|
||||
if (mode === 'paddle-direct' || mode === 'kombi' || mode === 'kombi-compare') {
|
||||
switch (currentStep) {
|
||||
case 0:
|
||||
return <StepOrientation sessionId={sessionId} onNext={handleOrientationComplete} />
|
||||
@@ -241,6 +243,9 @@ export default function OcrOverlayPage() {
|
||||
case 3:
|
||||
return <StepCrop sessionId={sessionId} onNext={handleNext} />
|
||||
case 4:
|
||||
if (mode === 'kombi-compare') {
|
||||
return <KombiCompareStep sessionId={sessionId} onNext={handleNext} />
|
||||
}
|
||||
return mode === 'kombi' ? (
|
||||
<PaddleDirectStep
|
||||
sessionId={sessionId}
|
||||
@@ -514,6 +519,22 @@ export default function OcrOverlayPage() {
|
||||
>
|
||||
Kombi (5 Schritte)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (mode === 'kombi-compare') return
|
||||
setMode('kombi-compare')
|
||||
setCurrentStep(0)
|
||||
setSessionId(null)
|
||||
setSteps(KOMBI_COMPARE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||
}}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
mode === 'kombi-compare'
|
||||
? 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-200 shadow-sm'
|
||||
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
Vergleich (5 Schritte)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<PipelineStepper
|
||||
|
||||
@@ -72,6 +72,18 @@ export const KOMBI_STEPS: PipelineStep[] = [
|
||||
{ id: 'kombi', name: 'Paddle + Tesseract', icon: '🔀', status: 'pending' },
|
||||
]
|
||||
|
||||
/**
|
||||
* 5-step pipeline for Kombi-Vergleich mode (Paddle-Kombi vs Rapid-Kombi side-by-side).
|
||||
* Same preprocessing, then both kombi engines run in parallel and are shown side-by-side.
|
||||
*/
|
||||
export const KOMBI_COMPARE_STEPS: PipelineStep[] = [
|
||||
{ id: 'orientation', name: 'Orientierung', icon: '🔄', status: 'pending' },
|
||||
{ id: 'deskew', name: 'Begradigung', icon: '📐', status: 'pending' },
|
||||
{ id: 'dewarp', name: 'Entzerrung', icon: '🔧', status: 'pending' },
|
||||
{ id: 'crop', name: 'Zuschneiden', icon: '✂️', status: 'pending' },
|
||||
{ id: 'kombi-compare', name: 'Kombi-Vergleich', icon: '⚖️', status: 'pending' },
|
||||
]
|
||||
|
||||
/** Map from DB step to overlay UI step index */
|
||||
export function dbStepToOverlayUi(dbStep: number): number {
|
||||
// DB: 1=start, 2=orient, 3=deskew, 4=dewarp, 5=crop, 6=columns, 7=rows, 8=words, 9=recon, 10=gt
|
||||
|
||||
231
admin-lehrer/components/ocr-overlay/KombiCompareStep.tsx
Normal file
231
admin-lehrer/components/ocr-overlay/KombiCompareStep.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { OverlayReconstruction } from './OverlayReconstruction'
|
||||
import type { GridCell } from '@/app/(admin)/ai/ocr-overlay/types'
|
||||
|
||||
const KLAUSUR_API = '/klausur-api'
|
||||
|
||||
type Phase = 'idle' | 'running' | 'compare'
|
||||
|
||||
interface KombiResult {
|
||||
cells: GridCell[]
|
||||
image_width: number
|
||||
image_height: number
|
||||
duration_seconds: number
|
||||
summary: {
|
||||
total_cells: number
|
||||
non_empty_cells: number
|
||||
merged_words: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
interface KombiCompareStepProps {
|
||||
sessionId: string | null
|
||||
onNext: () => void
|
||||
}
|
||||
|
||||
export function KombiCompareStep({ sessionId, onNext }: KombiCompareStepProps) {
|
||||
const [phase, setPhase] = useState<Phase>('idle')
|
||||
const [error, setError] = useState('')
|
||||
const [paddleResult, setPaddleResult] = useState<KombiResult | null>(null)
|
||||
const [rapidResult, setRapidResult] = useState<KombiResult | null>(null)
|
||||
const [paddleStatus, setPaddleStatus] = useState<'pending' | 'running' | 'done' | 'error'>('pending')
|
||||
const [rapidStatus, setRapidStatus] = useState<'pending' | 'running' | 'done' | 'error'>('pending')
|
||||
|
||||
const runBothEngines = async () => {
|
||||
if (!sessionId) return
|
||||
setPhase('running')
|
||||
setError('')
|
||||
setPaddleStatus('running')
|
||||
setRapidStatus('running')
|
||||
setPaddleResult(null)
|
||||
setRapidResult(null)
|
||||
|
||||
const fetchEngine = async (
|
||||
endpoint: string,
|
||||
setResult: (r: KombiResult) => void,
|
||||
setStatus: (s: 'pending' | 'running' | 'done' | 'error') => void,
|
||||
) => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/${endpoint}`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}))
|
||||
throw new Error(body.detail || `HTTP ${res.status}`)
|
||||
}
|
||||
const data = await res.json()
|
||||
setResult(data)
|
||||
setStatus('done')
|
||||
} catch (e: unknown) {
|
||||
setStatus('error')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
fetchEngine('paddle-kombi', setPaddleResult, setPaddleStatus),
|
||||
fetchEngine('rapid-kombi', setRapidResult, setRapidStatus),
|
||||
])
|
||||
setPhase('compare')
|
||||
} catch (e: unknown) {
|
||||
// At least one failed — still show compare if the other succeeded
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
setPhase('compare')
|
||||
}
|
||||
}
|
||||
|
||||
if (phase === 'idle') {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-8 text-center">
|
||||
<div className="text-4xl mb-3">⚖️</div>
|
||||
<h3 className="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2">
|
||||
Kombi-Vergleich
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6 max-w-lg mx-auto">
|
||||
Beide Kombi-Modi (Paddle + Tesseract vs. RapidOCR + Tesseract) laufen parallel.
|
||||
Die Ergebnisse werden nebeneinander angezeigt, damit die Qualitaet direkt verglichen werden kann.
|
||||
</p>
|
||||
<button
|
||||
onClick={runBothEngines}
|
||||
disabled={!sessionId}
|
||||
className="px-5 py-2.5 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||
>
|
||||
Beide Kombi-Modi starten
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (phase === 'running' && !paddleResult && !rapidResult) {
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-8">
|
||||
<div className="flex items-center justify-center gap-8">
|
||||
<EngineStatusCard label="Paddle + Tesseract" status={paddleStatus} />
|
||||
<EngineStatusCard label="RapidOCR + Tesseract" status={rapidStatus} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// compare phase
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 text-sm text-red-700 dark:text-red-300">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Side-by-Side Vergleich
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => { setPhase('idle'); setPaddleResult(null); setRapidResult(null) }}
|
||||
className="text-xs px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
Neu starten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Left: Paddle-Kombi */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
🔀 Paddle + Tesseract
|
||||
</span>
|
||||
{paddleStatus === 'error' && (
|
||||
<span className="text-xs text-red-500">Fehler</span>
|
||||
)}
|
||||
</div>
|
||||
{paddleResult ? (
|
||||
<>
|
||||
<OverlayReconstruction
|
||||
sessionId={sessionId}
|
||||
onNext={() => {}}
|
||||
wordResultOverride={paddleResult}
|
||||
/>
|
||||
<StatsBar result={paddleResult} engine="Paddle-Kombi" />
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-12 text-center text-sm text-gray-400">
|
||||
{paddleStatus === 'running' ? 'Laeuft...' : 'Fehlgeschlagen'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Rapid-Kombi */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
⚡ RapidOCR + Tesseract
|
||||
</span>
|
||||
{rapidStatus === 'error' && (
|
||||
<span className="text-xs text-red-500">Fehler</span>
|
||||
)}
|
||||
</div>
|
||||
{rapidResult ? (
|
||||
<>
|
||||
<OverlayReconstruction
|
||||
sessionId={sessionId}
|
||||
onNext={() => {}}
|
||||
wordResultOverride={rapidResult}
|
||||
/>
|
||||
<StatsBar result={rapidResult} engine="Rapid-Kombi" />
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-12 text-center text-sm text-gray-400">
|
||||
{rapidStatus === 'running' ? 'Laeuft...' : 'Fehlgeschlagen'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onNext}
|
||||
className="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors text-sm font-medium"
|
||||
>
|
||||
Fertig
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EngineStatusCard({ label, status }: { label: string; status: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 bg-gray-50 dark:bg-gray-900 rounded-lg px-5 py-4">
|
||||
{status === 'running' && (
|
||||
<div className="w-5 h-5 border-2 border-teal-400 border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
{status === 'done' && <span className="text-green-500 text-lg">✓</span>}
|
||||
{status === 'error' && <span className="text-red-500 text-lg">✗</span>}
|
||||
{status === 'pending' && <span className="text-gray-400 text-lg">○</span>}
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatsBar({ result, engine }: { result: KombiResult; engine: string }) {
|
||||
const nonEmpty = result.summary?.non_empty_cells ?? 0
|
||||
const totalCells = result.summary?.total_cells ?? 0
|
||||
const merged = result.summary?.merged_words ?? 0
|
||||
const duration = result.duration_seconds ?? 0
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 text-[11px] text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-900 rounded-lg px-3 py-2">
|
||||
<span className="font-medium text-gray-600 dark:text-gray-300">{engine}</span>
|
||||
<span>{merged} Woerter</span>
|
||||
<span>{nonEmpty}/{totalCells} Zellen</span>
|
||||
<span>{duration.toFixed(2)}s</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user