@@ -306,96 +353,304 @@ export function StepLlmReview({ sessionId, onNext }: StepLlmReviewProps) {
)
}
- // --- Done: diff table with checkboxes ---
- if (changes.length === 0) {
- return (
-
-
👍
-
Keine Korrekturen noetig
-
Das LLM hat keine OCR-Fehler gefunden.
- {meta && (
-
- {meta.to_review} geprueft, {meta.skipped} uebersprungen · {totalDuration}ms · {meta.model}
-
- )}
-
-
- )
- }
+ // Active entry for highlighting on image
+ const activeEntry = vocabEntries.find((_: WordEntry, i: number) => activeRowIndices.has(i))
+ const pct = progress ? Math.round((progress.current / progress.total) * 100) : 0
+
+ // --- Ready / Running / Done: 2-column layout ---
return (
{/* Header */}
-
LLM-Korrekturvorschlaege
+
+ Schritt 6: LLM-Korrektur
+
- {changes.length} Korrektur{changes.length !== 1 ? 'en' : ''} gefunden
- {meta && <> · {meta.skipped} uebersprungen (Lautschrift)>}
- {' '}· {totalDuration}ms · {meta?.model}
+ {status === 'ready' && `${vocabEntries.length} Eintraege bereit zur Pruefung`}
+ {status === 'running' && meta && `${meta.model} · ${meta.to_review} zu pruefen, ${meta.skipped} uebersprungen`}
+ {status === 'done' && (
+ <>
+ {changes.length} Korrektur{changes.length !== 1 ? 'en' : ''} gefunden
+ {meta && <> · {meta.skipped} uebersprungen>}
+ {' '}· {totalDuration}ms · {meta?.model}
+ >
+ )}
-
-
-
- {/* Diff table */}
-
-
- {/* Actions */}
-
-
{accepted.size} von {changes.length} ausgewaehlt
-
-
-
+
+ {status === 'ready' && (
+
+ )}
+ {status === 'running' && (
+
+
+ {progress ? `${progress.current}/${progress.total}` : 'Startet...'}
+
+ )}
+ {status === 'done' && changes.length > 0 && (
+
+ )}
+
+ {/* Progress bar (while running) */}
+ {status === 'running' && progress && (
+
+
+ {progress.current} / {progress.total} Eintraege geprueft
+ {pct}%
+
+
+
+ )}
+
+ {/* 2-column layout: Image + Table */}
+
+ {/* Left: Dewarped Image with highlight overlay */}
+
+
+ Originalbild
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+

{
+ const img = e.target as HTMLImageElement
+ setImageNaturalSize({ w: img.naturalWidth, h: img.naturalHeight })
+ }}
+ />
+ {/* Highlight overlay for active row */}
+ {activeEntry?.bbox && (
+
+ )}
+
+
+
+ {/* Right: Full vocabulary table */}
+
+
+ Vokabeltabelle ({vocabEntries.length} Eintraege)
+
+
+
+
+
+
+ | # |
+ EN |
+ DE |
+ Beispiel |
+ Status |
+
+
+
+ {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 (
+
+ | {idx} |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ )
+ })}
+
+
+
+
+
+
+
+ {/* Done state: summary + actions */}
+ {status === 'done' && (
+
+ {/* Summary */}
+
+ {changes.length === 0 ? (
+ Keine Korrekturen noetig — alle Eintraege sind korrekt.
+ ) : (
+
+ {changes.length} Korrektur{changes.length !== 1 ? 'en' : ''} gefunden ·{' '}
+ {accepted.size} ausgewaehlt ·{' '}
+ {meta?.skipped || 0} uebersprungen (Lautschrift) ·{' '}
+ {totalDuration}ms
+
+ )}
+
+
+ {/* Corrections detail list (if any) */}
+ {changes.length > 0 && (
+
+
+
+ Korrekturvorschlaege ({accepted.size}/{changes.length} ausgewaehlt)
+
+
+
+
+ )}
+
+ {/* Actions */}
+
+
+ {changes.length > 0 ? `${accepted.size} von ${changes.length} ausgewaehlt` : ''}
+
+
+ {changes.length > 0 && (
+
+ )}
+ {changes.length > 0 ? (
+
+ ) : (
+
+ )}
+
+
+
+ )}
)
}
+
+/** Cell content with inline diff for corrections */
+function CellContent({ text, field, rowChanges }: {
+ text: string
+ field: string
+ rowChanges?: LlmChange[]
+}) {
+ const change = rowChanges?.find(c => c.field === field)
+
+ if (!text && !change) {
+ return
—
+ }
+
+ if (change) {
+ return (
+
+ {change.old}
+ {change.new}
+
+ )
+ }
+
+ return
{text}
+}
+
+/** Status icon for each row */
+function StatusIcon({ status }: { status: RowStatus }) {
+ switch (status) {
+ case 'pending':
+ return
—
+ case 'active':
+ return (
+
+ )
+ case 'reviewed':
+ return (
+
+ )
+ case 'corrected':
+ return (
+
+ korr.
+
+ )
+ case 'skipped':
+ return (
+
+ skip
+
+ )
+ }
+}
diff --git a/admin-lehrer/components/ocr-pipeline/StepReconstruction.tsx b/admin-lehrer/components/ocr-pipeline/StepReconstruction.tsx
index f727d5f..ea5c197 100644
--- a/admin-lehrer/components/ocr-pipeline/StepReconstruction.tsx
+++ b/admin-lehrer/components/ocr-pipeline/StepReconstruction.tsx
@@ -1,18 +1,343 @@
'use client'
-export function StepReconstruction() {
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import type { GridResult, GridCell, WordEntry } from '@/app/(admin)/ai/ocr-pipeline/types'
+
+const KLAUSUR_API = '/klausur-api'
+
+interface StepReconstructionProps {
+ sessionId: string | null
+ onNext: () => void
+}
+
+interface EditableCell {
+ cellId: string
+ text: string
+ originalText: string
+ bboxPct: { x: number; y: number; w: number; h: number }
+ colType: string
+ rowIndex: number
+ colIndex: number
+}
+
+export function StepReconstruction({ sessionId, onNext }: StepReconstructionProps) {
+ const [status, setStatus] = useState<'loading' | 'ready' | 'saving' | 'saved' | 'error'>('loading')
+ const [error, setError] = useState('')
+ const [cells, setCells] = useState
([])
+ const [editedTexts, setEditedTexts] = useState