Files
breakpilot-lehrer/admin-lehrer/components/ocr/CellCorrectionDialog.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

295 lines
9.9 KiB
TypeScript

'use client'
/**
* CellCorrectionDialog Component
*
* Modal dialog for manually correcting OCR text in problematic or recognized cells.
* Shows cropped image of the cell for reference and allows text input.
*/
import { useState, useEffect } from 'react'
import type { GridCell } from './GridOverlay'
interface CellCorrectionDialogProps {
cell: GridCell
columnType: 'english' | 'german' | 'example' | 'unknown'
sessionId: string
pageNumber: number
onSave: (text: string) => void
onRetryOCR?: () => void
onClose: () => void
}
export function CellCorrectionDialog({
cell,
columnType,
sessionId,
pageNumber,
onSave,
onRetryOCR,
onClose,
}: CellCorrectionDialogProps) {
const [text, setText] = useState(cell.text || '')
const [loading, setLoading] = useState(false)
const [retrying, setRetrying] = useState(false)
const [cropUrl, setCropUrl] = useState<string | null>(null)
const KLAUSUR_API = '/klausur-api'
// Load cell crop image
useEffect(() => {
const loadCrop = async () => {
try {
const res = await fetch(
`${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/cell-crop/${pageNumber}/${cell.row}/${cell.col}`
)
if (res.ok) {
const blob = await res.blob()
setCropUrl(URL.createObjectURL(blob))
}
} catch (e) {
console.error('Failed to load cell crop:', e)
}
}
loadCrop()
return () => {
if (cropUrl) {
URL.revokeObjectURL(cropUrl)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, pageNumber, cell.row, cell.col])
const handleSave = async () => {
if (!text.trim()) return
setLoading(true)
try {
const formData = new FormData()
formData.append('text', text)
const res = await fetch(
`${KLAUSUR_API}/api/v1/vocab/sessions/${sessionId}/cell/${cell.row}/${cell.col}`,
{
method: 'PUT',
body: formData,
}
)
if (res.ok) {
onSave(text)
onClose()
} else {
console.error('Failed to save cell:', await res.text())
}
} catch (e) {
console.error('Save failed:', e)
} finally {
setLoading(false)
}
}
const handleRetryOCR = async () => {
if (!onRetryOCR) return
setRetrying(true)
try {
await onRetryOCR()
} finally {
setRetrying(false)
}
}
const getColumnLabel = () => {
switch (columnType) {
case 'english':
return 'Englisch'
case 'german':
return 'Deutsch'
case 'example':
return 'Beispielsatz'
default:
return 'Text'
}
}
const getPlaceholder = () => {
switch (columnType) {
case 'english':
return 'Englisches Wort eingeben...'
case 'german':
return 'Deutsche Ubersetzung eingeben...'
case 'example':
return 'Beispielsatz eingeben...'
default:
return 'Text eingeben...'
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={onClose}
/>
{/* Dialog */}
<div className="relative w-full max-w-lg bg-white rounded-xl shadow-xl overflow-hidden">
{/* Header */}
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold text-slate-900">
{cell.status === 'problematic' ? 'Nicht erkannter Bereich' : 'Text bearbeiten'}
</h3>
<p className="text-sm text-slate-500">
Zeile {cell.row + 1}, Spalte {cell.col + 1} ({getColumnLabel()})
</p>
</div>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-slate-100 transition-colors"
>
<svg className="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-6 space-y-4">
{/* Cell image preview */}
<div className="border rounded-lg p-3 bg-slate-50">
<p className="text-xs text-slate-500 mb-2 font-medium">Originalbild:</p>
{cropUrl ? (
<img
src={cropUrl}
alt="Zellinhalt"
className="max-h-32 mx-auto rounded border border-slate-200 bg-white"
/>
) : (
<div className="h-20 flex items-center justify-center text-slate-400 text-sm">
Lade Vorschau...
</div>
)}
</div>
{/* Status indicator */}
{cell.status === 'problematic' && (
<div className="flex items-center gap-2 p-3 bg-orange-50 border border-orange-200 rounded-lg">
<svg className="w-5 h-5 text-orange-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span className="text-sm text-orange-700">
Diese Zelle konnte nicht automatisch erkannt werden.
</span>
</div>
)}
{/* Current recognized text */}
{cell.status === 'recognized' && cell.text && (
<div className="p-3 bg-green-50 border border-green-200 rounded-lg">
<p className="text-xs text-green-600 font-medium mb-1">Erkannter Text:</p>
<p className="text-sm text-green-800">{cell.text}</p>
{cell.confidence < 1 && (
<p className="text-xs text-green-600 mt-1">
Konfidenz: {Math.round(cell.confidence * 100)}%
</p>
)}
</div>
)}
{/* Text input */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{cell.status === 'problematic' ? 'Text eingeben' : 'Text korrigieren'}
</label>
{columnType === 'example' ? (
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={getPlaceholder()}
rows={3}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-slate-900"
/>
) : (
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder={getPlaceholder()}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 text-slate-900"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleSave()
}
}}
/>
)}
</div>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-slate-200 flex items-center justify-between bg-slate-50">
<div>
{onRetryOCR && cell.status === 'problematic' && (
<button
onClick={handleRetryOCR}
disabled={retrying}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-50 flex items-center gap-2"
>
{retrying ? (
<>
<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>
Erneut versuchen...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
OCR erneut versuchen
</>
)}
</button>
)}
</div>
<div className="flex gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50"
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={loading || !text.trim()}
className="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2"
>
{loading ? (
<>
<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>
Speichern...
</>
) : (
'Speichern'
)}
</button>
</div>
</div>
</div>
</div>
)
}
export default CellCorrectionDialog