fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
294
admin-v2/components/ocr/CellCorrectionDialog.tsx
Normal file
294
admin-v2/components/ocr/CellCorrectionDialog.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
'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
|
||||
Reference in New Issue
Block a user