This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/studio-v2/app/magic-help/page.tsx
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +01:00

267 lines
9.7 KiB
TypeScript

'use client'
/**
* Magic Help - Handschrift-OCR
*
* Ermöglicht das Erkennen von Handschrift in Bildern.
* Backend: POST /api/klausur/trocr/recognize
*/
import { useState, useCallback } from 'react'
// Backend URL - dynamisch basierend auf Protokoll
const getBackendUrl = () => {
if (typeof window === 'undefined') return 'http://localhost:8000'
const { hostname, protocol } = window.location
return hostname === 'localhost' ? 'http://localhost:8000' : `${protocol}//${hostname}:8000`
}
interface OCRResult {
text: string
confidence: number
processing_time_ms: number
model: string
}
export default function MagicHelpPage() {
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [ocrResult, setOcrResult] = useState<OCRResult | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
// Datei auswählen
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
setSelectedFile(file)
setPreviewUrl(URL.createObjectURL(file))
setOcrResult(null)
setError(null)
}
}, [])
// Drag & Drop
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
const file = e.dataTransfer.files[0]
if (file && file.type.startsWith('image/')) {
setSelectedFile(file)
setPreviewUrl(URL.createObjectURL(file))
setOcrResult(null)
setError(null)
}
}, [])
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
}, [])
// OCR ausführen
const runOCR = useCallback(async () => {
if (!selectedFile) return
setLoading(true)
setError(null)
try {
const formData = new FormData()
formData.append('file', selectedFile)
const res = await fetch(`${getBackendUrl()}/api/klausur/trocr/recognize`, {
method: 'POST',
body: formData,
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
const data = await res.json()
setOcrResult(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler bei der OCR-Erkennung')
} finally {
setLoading(false)
}
}, [selectedFile])
// Reset
const handleReset = useCallback(() => {
setSelectedFile(null)
setPreviewUrl(null)
setOcrResult(null)
setError(null)
}, [])
return (
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-slate-800">Magic Help</h1>
<p className="text-slate-500 mt-1">Handschrift-Erkennung mit TrOCR</p>
</div>
{/* Upload Area */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
<h2 className="font-semibold text-slate-700 mb-4">Bild hochladen</h2>
{!previewUrl ? (
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
className="border-2 border-dashed border-slate-300 rounded-lg p-12 text-center hover:border-primary-500 transition-colors cursor-pointer"
>
<input
type="file"
accept="image/*"
onChange={handleFileSelect}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload" className="cursor-pointer">
<svg
className="w-12 h-12 mx-auto text-slate-400 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p className="text-slate-600 font-medium">Bild hier ablegen oder klicken</p>
<p className="text-sm text-slate-400 mt-1">PNG, JPG, JPEG bis 10MB</p>
</label>
</div>
) : (
<div className="space-y-4">
{/* Preview */}
<div className="relative bg-slate-100 rounded-lg overflow-hidden">
<img
src={previewUrl}
alt="Preview"
className="max-h-96 mx-auto object-contain"
/>
</div>
{/* Actions */}
<div className="flex gap-3">
<button
onClick={runOCR}
disabled={loading}
className="flex-1 bg-primary-500 text-white px-6 py-3 rounded-lg font-medium hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
>
{loading ? (
<>
<svg className="animate-spin w-5 h-5" 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Wird erkannt...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
Text erkennen
</>
)}
</button>
<button
onClick={handleReset}
className="px-6 py-3 rounded-lg font-medium border border-slate-300 text-slate-600 hover:bg-slate-50 transition-colors"
>
Zurücksetzen
</button>
</div>
</div>
)}
</div>
{/* Error */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 mb-6">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-red-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="font-medium text-red-800">Fehler</p>
<p className="text-sm text-red-600 mt-1">{error}</p>
</div>
</div>
</div>
)}
{/* Result */}
{ocrResult && (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 className="font-semibold text-slate-700 mb-4">Erkannter Text</h2>
{/* Text Output */}
<div className="bg-slate-50 rounded-lg p-4 mb-4 font-mono text-slate-800 whitespace-pre-wrap">
{ocrResult.text || '(Kein Text erkannt)'}
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 text-sm">
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-slate-500">Konfidenz</div>
<div className="font-semibold text-slate-800">
{(ocrResult.confidence * 100).toFixed(1)}%
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-slate-500">Dauer</div>
<div className="font-semibold text-slate-800">
{ocrResult.processing_time_ms}ms
</div>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-slate-500">Modell</div>
<div className="font-semibold text-slate-800 truncate">
{ocrResult.model}
</div>
</div>
</div>
{/* Copy Button */}
<button
onClick={() => navigator.clipboard.writeText(ocrResult.text)}
className="mt-4 px-4 py-2 rounded-lg font-medium border border-slate-300 text-slate-600 hover:bg-slate-50 transition-colors text-sm flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Text kopieren
</button>
</div>
)}
{/* Info Box */}
{!previewUrl && !ocrResult && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-500 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="font-medium text-blue-800">Tipp</p>
<p className="text-sm text-blue-600 mt-1">
Laden Sie ein Bild mit handgeschriebenem Text hoch. Der TrOCR-Dienst erkennt
deutsche Handschrift und gibt den Text zurück.
</p>
</div>
</div>
</div>
)}
</div>
)
}