Files
breakpilot-lehrer/admin-lehrer/app/(admin)/ai/magic-help/page.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

1605 lines
72 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* Magic Help Admin Page - Admin v2 Migration
*
* Comprehensive admin interface for TrOCR Handwriting Recognition and Exam Correction.
* Features:
* - Model status monitoring
* - OCR testing with image upload
* - Training data management
* - Fine-tuning controls
* - Architecture documentation
* - Configuration settings
*
* Phase 1 Enhancements:
* - Clipboard Paste (Ctrl+V) support
* - Global Drag & Drop anywhere on window
* - Skeleton loading states
* - Live OCR preview with debounce
* - Keyboard shortcuts
*
* Phase 2-4 Enhancements:
* - Batch processing with SSE progress
* - Confidence heatmap visualization
* - Training metrics dashboard
* - Model export functionality
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import Link from 'next/link'
import { SkeletonOCRResult, SkeletonText, SkeletonDots } from '@/components/common/SkeletonText'
import { ConfidenceHeatmap, ConfidenceStats } from '@/components/ai/ConfidenceHeatmap'
import { TrainingMetrics } from '@/components/ai/TrainingMetrics'
import { BatchUploader } from '@/components/ai/BatchUploader'
import { AIModuleSidebarResponsive } from '@/components/ai/AIModuleSidebar'
import { PagePurpose } from '@/components/common/PagePurpose'
type TabId = 'overview' | 'test' | 'batch' | 'training' | 'architecture' | 'settings'
interface TrOCRStatus {
status: 'available' | 'not_installed' | 'error'
model_name?: string
model_id?: string
device?: string
is_loaded?: boolean
has_lora_adapter?: boolean
training_examples_count?: number
error?: string
install_command?: string
}
interface OCRResult {
text: string
confidence: number
processing_time_ms: number
model: string
has_lora_adapter: boolean
char_confidences?: number[]
word_boxes?: Array<{ text: string; confidence: number; bbox: number[] }>
}
interface TrainingExample {
image_path: string
ground_truth: string
teacher_id: string
created_at: string
}
interface MagicSettings {
autoDetectLines: boolean
confidenceThreshold: number
maxImageSize: number
loraRank: number
loraAlpha: number
learningRate: number
epochs: number
batchSize: number
enableCache: boolean
cacheMaxAge: number
livePreview: boolean
soundFeedback: boolean
}
const DEFAULT_SETTINGS: MagicSettings = {
autoDetectLines: true,
confidenceThreshold: 0.7,
maxImageSize: 4096,
loraRank: 8,
loraAlpha: 32,
learningRate: 0.00005,
epochs: 3,
batchSize: 4,
enableCache: true,
cacheMaxAge: 3600,
livePreview: true,
soundFeedback: false,
}
export default function MagicHelpPage() {
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [status, setStatus] = useState<TrOCRStatus | null>(null)
const [loading, setLoading] = useState(true)
const [ocrResult, setOcrResult] = useState<OCRResult | null>(null)
const [ocrLoading, setOcrLoading] = useState(false)
const [examples, setExamples] = useState<TrainingExample[]>([])
const [trainingImage, setTrainingImage] = useState<File | null>(null)
const [trainingText, setTrainingText] = useState('')
const [fineTuning, setFineTuning] = useState(false)
const [settings, setSettings] = useState<MagicSettings>(DEFAULT_SETTINGS)
const [settingsSaved, setSettingsSaved] = useState(false)
// Phase 1: New state for enhanced features
const [globalDragActive, setGlobalDragActive] = useState(false)
const [uploadedImage, setUploadedImage] = useState<File | null>(null)
const [imagePreview, setImagePreview] = useState<string | null>(null)
const [showShortcutHint, setShowShortcutHint] = useState(false)
const debounceTimer = useRef<NodeJS.Timeout | null>(null)
const dragCounter = useRef(0)
// Use same-origin nginx proxy to avoid CORS issues
const API_BASE = '/klausur-api'
const fetchStatus = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/api/klausur/trocr/status`)
const data = await res.json()
setStatus(data)
} catch {
setStatus({ status: 'error', error: 'Failed to fetch status' })
} finally {
setLoading(false)
}
}, [API_BASE])
const fetchExamples = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/api/klausur/trocr/training/examples`)
const data = await res.json()
setExamples(data.examples || [])
} catch (error) {
console.error('Failed to fetch examples:', error)
}
}, [API_BASE])
// Phase 1: Live OCR with debounce
const triggerOCR = useCallback(async (file: File) => {
setOcrLoading(true)
setOcrResult(null)
const formData = new FormData()
formData.append('file', file)
try {
const res = await fetch(`${API_BASE}/api/klausur/trocr/extract?detect_lines=${settings.autoDetectLines}`, {
method: 'POST',
body: formData,
})
const data = await res.json()
if (data.text !== undefined) {
setOcrResult(data)
// Play sound feedback if enabled
if (settings.soundFeedback && data.confidence > 0.7) {
playSuccessSound()
}
} else {
setOcrResult({ text: `Error: ${data.detail || 'Unknown error'}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false })
}
} catch (error) {
setOcrResult({ text: `Error: ${error}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false })
} finally {
setOcrLoading(false)
}
}, [API_BASE, settings.autoDetectLines, settings.soundFeedback])
// Play subtle success sound
const playSuccessSound = () => {
try {
const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)()
const oscillator = audioContext.createOscillator()
const gainNode = audioContext.createGain()
oscillator.connect(gainNode)
gainNode.connect(audioContext.destination)
oscillator.frequency.value = 800
oscillator.type = 'sine'
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime)
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2)
oscillator.start(audioContext.currentTime)
oscillator.stop(audioContext.currentTime + 0.2)
} catch {
// Audio not supported, ignore
}
}
// Handle file upload with live preview
const handleFileUpload = useCallback((file: File) => {
if (!file.type.startsWith('image/')) return
setUploadedImage(file)
// Create preview URL
const previewUrl = URL.createObjectURL(file)
setImagePreview(previewUrl)
// Auto-switch to test tab if not there
setActiveTab('test')
// Live preview: trigger OCR with debounce
if (settings.livePreview) {
if (debounceTimer.current) {
clearTimeout(debounceTimer.current)
}
debounceTimer.current = setTimeout(() => {
triggerOCR(file)
}, 500)
}
}, [settings.livePreview, triggerOCR])
// Manual OCR trigger
const handleManualOCR = () => {
if (uploadedImage) {
triggerOCR(uploadedImage)
}
}
// Phase 1: Global Drag & Drop handler
useEffect(() => {
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
dragCounter.current++
if (e.dataTransfer?.types.includes('Files')) {
setGlobalDragActive(true)
}
}
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
dragCounter.current--
if (dragCounter.current === 0) {
setGlobalDragActive(false)
}
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
dragCounter.current = 0
setGlobalDragActive(false)
const file = e.dataTransfer?.files[0]
if (file?.type.startsWith('image/')) {
handleFileUpload(file)
}
}
document.addEventListener('dragenter', handleDragEnter)
document.addEventListener('dragleave', handleDragLeave)
document.addEventListener('dragover', handleDragOver)
document.addEventListener('drop', handleDrop)
return () => {
document.removeEventListener('dragenter', handleDragEnter)
document.removeEventListener('dragleave', handleDragLeave)
document.removeEventListener('dragover', handleDragOver)
document.removeEventListener('drop', handleDrop)
}
}, [handleFileUpload])
// Phase 1: Clipboard paste handler (Ctrl+V)
useEffect(() => {
const handlePaste = async (e: ClipboardEvent) => {
const items = e.clipboardData?.items
if (!items) return
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault()
const file = item.getAsFile()
if (file) {
handleFileUpload(file)
}
break
}
}
}
document.addEventListener('paste', handlePaste)
return () => document.removeEventListener('paste', handlePaste)
}, [handleFileUpload])
// Phase 1: Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+Enter: Start OCR
if (e.ctrlKey && e.key === 'Enter' && uploadedImage) {
e.preventDefault()
handleManualOCR()
}
// Tab: Switch tabs (with numbers 1-6)
if (e.key >= '1' && e.key <= '6' && e.altKey) {
e.preventDefault()
const tabIndex = parseInt(e.key) - 1
const tabIds: TabId[] = ['overview', 'test', 'batch', 'training', 'architecture', 'settings']
if (tabIds[tabIndex]) {
setActiveTab(tabIds[tabIndex])
}
}
// Escape: Clear uploaded image
if (e.key === 'Escape' && uploadedImage) {
setUploadedImage(null)
setImagePreview(null)
setOcrResult(null)
}
// ? : Show shortcuts
if (e.key === '?') {
setShowShortcutHint(prev => !prev)
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [uploadedImage])
useEffect(() => {
fetchStatus()
fetchExamples()
// Load settings from localStorage
const saved = localStorage.getItem('magic-help-settings')
if (saved) {
try {
setSettings({ ...DEFAULT_SETTINGS, ...JSON.parse(saved) })
} catch {
// ignore parse errors
}
}
}, [fetchStatus, fetchExamples])
// Cleanup preview URL
useEffect(() => {
return () => {
if (imagePreview) {
URL.revokeObjectURL(imagePreview)
}
}
}, [imagePreview])
const handleAddTrainingExample = async () => {
if (!trainingImage || !trainingText.trim()) {
alert('Please provide both an image and the correct text')
return
}
const formData = new FormData()
formData.append('file', trainingImage)
try {
const res = await fetch(`${API_BASE}/api/klausur/trocr/training/add?ground_truth=${encodeURIComponent(trainingText)}`, {
method: 'POST',
body: formData,
})
const data = await res.json()
if (data.example_id) {
alert(`Training example added! Total: ${data.total_examples}`)
setTrainingImage(null)
setTrainingText('')
fetchStatus()
fetchExamples()
} else {
alert(`Error: ${data.detail || 'Unknown error'}`)
}
} catch (error) {
alert(`Error: ${error}`)
}
}
const handleFineTune = async () => {
if (!confirm('Start fine-tuning? This may take several minutes.')) return
setFineTuning(true)
try {
const res = await fetch(`${API_BASE}/api/klausur/trocr/training/fine-tune`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
epochs: settings.epochs,
learning_rate: settings.learningRate,
lora_rank: settings.loraRank,
lora_alpha: settings.loraAlpha,
}),
})
const data = await res.json()
if (data.status === 'success') {
alert(`Fine-tuning successful!\nExamples used: ${data.examples_used}\nEpochs: ${data.epochs}`)
fetchStatus()
} else {
alert(`Fine-tuning failed: ${data.message}`)
}
} catch (error) {
alert(`Error: ${error}`)
} finally {
setFineTuning(false)
}
}
const saveSettings = () => {
localStorage.setItem('magic-help-settings', JSON.stringify(settings))
setSettingsSaved(true)
setTimeout(() => setSettingsSaved(false), 2000)
}
const getStatusBadge = () => {
if (!status) return null
switch (status.status) {
case 'available':
return <span className="px-3 py-1 text-xs font-medium rounded-full bg-green-100 text-green-700">Verfuegbar</span>
case 'not_installed':
return <span className="px-3 py-1 text-xs font-medium rounded-full bg-red-100 text-red-700">Nicht installiert</span>
case 'error':
return <span className="px-3 py-1 text-xs font-medium rounded-full bg-yellow-100 text-yellow-700">Fehler</span>
}
}
// Get confidence color for visualization
const getConfidenceColor = (confidence: number) => {
if (confidence >= 0.9) return 'bg-green-500'
if (confidence >= 0.7) return 'bg-yellow-500'
return 'bg-red-500'
}
// State for new features
const [showHeatmap, setShowHeatmap] = useState(false)
const [showTrainingDashboard, setShowTrainingDashboard] = useState(false)
const tabs = [
{ id: 'overview' as TabId, label: 'Uebersicht', icon: '📊', shortcut: 'Alt+1' },
{ id: 'test' as TabId, label: 'OCR Test', icon: '🔍', shortcut: 'Alt+2' },
{ id: 'batch' as TabId, label: 'Batch OCR', icon: '📁', shortcut: 'Alt+3' },
{ id: 'training' as TabId, label: 'Training', icon: '🎯', shortcut: 'Alt+4' },
{ id: 'architecture' as TabId, label: 'Architektur', icon: '🏗️', shortcut: 'Alt+5' },
{ id: 'settings' as TabId, label: 'Einstellungen', icon: '⚙️', shortcut: 'Alt+6' },
]
return (
<div className="space-y-6 relative">
{/* Global Drag Overlay */}
{globalDragActive && (
<div className="fixed inset-0 z-50 bg-purple-900/80 backdrop-blur-sm flex items-center justify-center pointer-events-none">
<div className="text-center">
<div className="text-7xl mb-4 animate-bounce">📄</div>
<div className="text-2xl font-bold text-white">Bild hier ablegen</div>
<div className="text-purple-200 mt-2">PNG, JPG - Handgeschriebener Text</div>
</div>
</div>
)}
{/* Keyboard Shortcuts Modal */}
{showShortcutHint && (
<div className="fixed inset-0 z-40 bg-black/50 flex items-center justify-center" onClick={() => setShowShortcutHint(false)}>
<div className="bg-white rounded-xl shadow-2xl p-6 max-w-md" onClick={e => e.stopPropagation()}>
<h3 className="text-lg font-bold text-slate-900 mb-4">Tastenkuerzel</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-600">Bild einfuegen</span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-mono">Ctrl+V</kbd>
</div>
<div className="flex justify-between">
<span className="text-slate-600">OCR starten</span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-mono">Ctrl+Enter</kbd>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Tab wechseln</span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-mono">Alt+1-6</kbd>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Bild entfernen</span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-mono">Escape</kbd>
</div>
<div className="flex justify-between">
<span className="text-slate-600">Shortcuts anzeigen</span>
<kbd className="px-2 py-1 bg-slate-100 rounded text-xs font-mono">?</kbd>
</div>
</div>
<button
onClick={() => setShowShortcutHint(false)}
className="w-full mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm"
>
Schliessen
</button>
</div>
</div>
)}
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<span className="text-2xl"></span>
Magic Help - Handschrifterkennung
</h1>
<p className="text-slate-500 mt-1">
KI-gestuetzte Klausurkorrektur mit TrOCR und Privacy-by-Design
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={() => setShowShortcutHint(true)}
className="p-2 text-slate-400 hover:text-slate-600 transition-colors"
title="Tastenkuerzel anzeigen (?)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</button>
{getStatusBadge()}
</div>
</div>
{/* Page Purpose with Related Pages */}
<PagePurpose
title="Magic Help"
purpose="Testen und verbessern Sie die TrOCR-Handschrifterkennung. Laden Sie Bilder hoch, um die OCR-Qualitaet zu pruefen, und trainieren Sie das Modell mit LoRA Fine-Tuning. Teil der KI-Daten-Pipeline: Exportieren Sie Trainingsdaten zu OCR-Labeling oder importieren Sie Ground Truth fuer Fine-Tuning."
audience={['Entwickler', 'Administratoren', 'QA-Team']}
architecture={{
services: ['klausur-service (Python)', 'TrOCR (PyTorch)'],
databases: ['Modell-Cache', 'LoRA-Adapter'],
}}
relatedPages={[
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Ground Truth erstellen' },
{ name: 'RAG Pipeline', href: '/ai/rag-pipeline', description: 'Erkannte Texte indexieren' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* AI Module Sidebar - Desktop: Fixed, Mobile: FAB + Drawer */}
<AIModuleSidebarResponsive currentModule="magic-help" />
{/* Quick paste hint */}
<div className="bg-gradient-to-r from-purple-50 to-blue-50 border border-purple-200 rounded-lg px-4 py-2 flex items-center gap-3 text-sm">
<span className="text-purple-600">💡</span>
<span className="text-slate-600">
<strong>Tipp:</strong> Druecke <kbd className="px-1.5 py-0.5 bg-white rounded border text-xs font-mono">Ctrl+V</kbd> um ein Bild aus der Zwischenablage einzufuegen, oder ziehe es einfach irgendwo ins Fenster.
</span>
</div>
{/* Tabs */}
<div className="flex gap-2 border-b border-slate-200 pb-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-t-lg text-sm font-medium transition-colors ${
activeTab === tab.id
? 'bg-purple-600 text-white'
: 'text-slate-600 hover:text-slate-900 hover:bg-slate-100'
}`}
title={tab.shortcut}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="space-y-6">
{/* Status Card */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">Systemstatus</h2>
<button
onClick={fetchStatus}
className="px-3 py-1 bg-purple-600 hover:bg-purple-700 text-white rounded text-sm transition-colors"
>
Aktualisieren
</button>
</div>
{loading ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-slate-50 rounded-lg p-4">
<SkeletonText lines={1} className="mb-2" />
<div className="h-3 w-16 bg-slate-200 rounded animate-pulse" />
</div>
))}
</div>
) : status?.status === 'available' ? (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-2xl font-bold text-slate-900">{status.model_name || 'trocr-base'}</div>
<div className="text-xs text-slate-500">Modell</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-2xl font-bold text-slate-900">{status.device || 'CPU'}</div>
<div className="text-xs text-slate-500">Geraet</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-2xl font-bold text-slate-900">{status.training_examples_count || 0}</div>
<div className="text-xs text-slate-500">Trainingsbeispiele</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-2xl font-bold text-slate-900">{status.has_lora_adapter ? 'Aktiv' : 'Keiner'}</div>
<div className="text-xs text-slate-500">LoRA Adapter</div>
</div>
</div>
) : status?.status === 'not_installed' ? (
<div className="text-slate-600">
<p className="mb-2">TrOCR ist nicht installiert. Fuehre aus:</p>
<code className="bg-slate-100 px-3 py-2 rounded text-sm block font-mono">{status.install_command}</code>
</div>
) : (
<div className="text-red-600">{status?.error || 'Unbekannter Fehler'}</div>
)}
</div>
{/* Quick Overview Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gradient-to-br from-purple-50 to-purple-100 border border-purple-200 rounded-xl p-6">
<div className="text-3xl mb-2">🎯</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Handschrifterkennung</h3>
<p className="text-sm text-slate-600">
TrOCR erkennt automatisch handgeschriebenen Text in Klausuren.
Das Modell wurde speziell fuer deutsche Handschriften optimiert.
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 border border-green-200 rounded-xl p-6">
<div className="text-3xl mb-2">🔒</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Privacy by Design</h3>
<p className="text-sm text-slate-600">
Alle Daten werden lokal verarbeitet. Schuelernamen werden durch
QR-Codes pseudonymisiert - DSGVO-konform.
</p>
</div>
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-6">
<div className="text-3xl mb-2">📈</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Kontinuierliches Lernen</h3>
<p className="text-sm text-slate-600">
Mit LoRA Fine-Tuning passt sich das Modell an individuelle
Handschriften an - ohne das Basismodell zu veraendern.
</p>
</div>
</div>
{/* Workflow Overview */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Magic Onboarding Workflow</h2>
<div className="flex flex-wrap items-center gap-4 text-sm">
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
<span className="text-2xl">📄</span>
<div>
<div className="font-medium text-slate-900">1. Upload</div>
<div className="text-slate-500">25 Klausuren hochladen</div>
</div>
</div>
<div className="text-slate-400"></div>
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
<span className="text-2xl">🔍</span>
<div>
<div className="font-medium text-slate-900">2. Analyse</div>
<div className="text-slate-500">Lokale OCR in 5-10 Sek</div>
</div>
</div>
<div className="text-slate-400"></div>
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
<span className="text-2xl"></span>
<div>
<div className="font-medium text-slate-900">3. Bestaetigung</div>
<div className="text-slate-500">Klasse, Schueler, Fach</div>
</div>
</div>
<div className="text-slate-400"></div>
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
<span className="text-2xl">🤖</span>
<div>
<div className="font-medium text-slate-900">4. KI-Korrektur</div>
<div className="text-slate-500">Cloud mit Pseudonymisierung</div>
</div>
</div>
<div className="text-slate-400"></div>
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
<span className="text-2xl">📊</span>
<div>
<div className="font-medium text-slate-900">5. Integration</div>
<div className="text-slate-500">Notenbuch, Zeugnisse</div>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'test' && (
<div className="space-y-6">
{/* OCR Test */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">OCR Test</h2>
<p className="text-sm text-slate-500 mb-4">
Teste die Handschrifterkennung mit einem eigenen Bild. Das Ergebnis zeigt
den erkannten Text, Konfidenz und Verarbeitungszeit.
{settings.livePreview && (
<span className="text-purple-600 ml-1">(Live-Vorschau aktiv)</span>
)}
</p>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Upload Area */}
<div>
<div
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all ${
imagePreview
? 'border-purple-500 bg-purple-50'
: 'border-slate-300 hover:border-purple-500'
}`}
onClick={() => document.getElementById('ocr-file-input')?.click()}
onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('border-purple-500', 'bg-purple-50') }}
onDragLeave={(e) => { e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50') }}
onDrop={(e) => {
e.preventDefault()
e.stopPropagation()
e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50')
const file = e.dataTransfer.files[0]
if (file?.type.startsWith('image/')) handleFileUpload(file)
}}
>
{imagePreview ? (
<div className="relative">
<img
src={imagePreview}
alt="Hochgeladenes Bild"
className="max-h-64 mx-auto rounded-lg shadow-sm"
/>
<button
onClick={(e) => {
e.stopPropagation()
setUploadedImage(null)
setImagePreview(null)
setOcrResult(null)
}}
className="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
title="Bild entfernen (Escape)"
>
<svg className="w-4 h-4" 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>
) : (
<>
<div className="text-4xl mb-2">📄</div>
<div className="text-slate-700">Bild hierher ziehen oder klicken zum Hochladen</div>
<div className="text-xs text-slate-400 mt-1">PNG, JPG - Handgeschriebener Text</div>
<div className="text-xs text-purple-500 mt-2">
oder <kbd className="px-1.5 py-0.5 bg-purple-100 rounded font-mono">Ctrl+V</kbd> zum Einfuegen
</div>
</>
)}
</div>
<input
type="file"
id="ocr-file-input"
accept="image/*"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0]
if (file) handleFileUpload(file)
}}
/>
{/* Manual trigger button if live preview is off */}
{uploadedImage && !settings.livePreview && (
<button
onClick={handleManualOCR}
disabled={ocrLoading}
className="w-full mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-slate-300 text-white rounded-lg text-sm font-medium transition-colors"
>
{ocrLoading ? (
<span className="flex items-center justify-center gap-2">
<SkeletonDots />
Analysiere...
</span>
) : (
'OCR starten (Ctrl+Enter)'
)}
</button>
)}
</div>
{/* Results Area */}
<div>
{ocrLoading ? (
<SkeletonOCRResult />
) : ocrResult ? (
<div className="bg-slate-50 rounded-lg p-4">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-slate-700">Erkannter Text:</h3>
<div className={`px-2 py-1 rounded-full text-xs font-medium ${
ocrResult.confidence >= 0.9 ? 'bg-green-100 text-green-700' :
ocrResult.confidence >= 0.7 ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{(ocrResult.confidence * 100).toFixed(0)}% Konfidenz
</div>
</div>
<pre className="bg-white border p-3 rounded text-sm text-slate-900 whitespace-pre-wrap max-h-48 overflow-y-auto">
{ocrResult.text || '(Kein Text erkannt)'}
</pre>
{/* Confidence bar visualization */}
<div className="mt-3 mb-3">
<div className="h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-500 ${getConfidenceColor(ocrResult.confidence)}`}
style={{ width: `${ocrResult.confidence * 100}%` }}
/>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="bg-white border rounded p-2">
<div className="text-slate-500 text-xs">Konfidenz</div>
<div className="text-slate-900 font-medium">{(ocrResult.confidence * 100).toFixed(1)}%</div>
</div>
<div className="bg-white border rounded p-2">
<div className="text-slate-500 text-xs">Verarbeitungszeit</div>
<div className="text-slate-900 font-medium">{ocrResult.processing_time_ms}ms</div>
</div>
<div className="bg-white border rounded p-2">
<div className="text-slate-500 text-xs">Modell</div>
<div className="text-slate-900 font-medium">{ocrResult.model || 'TrOCR'}</div>
</div>
<div className="bg-white border rounded p-2">
<div className="text-slate-500 text-xs">LoRA Adapter</div>
<div className="text-slate-900 font-medium">{ocrResult.has_lora_adapter ? 'Ja' : 'Nein'}</div>
</div>
</div>
{/* Quick training action */}
{ocrResult.confidence < 0.9 && (
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-800 mb-2">
Die Erkennung koennte verbessert werden! Moechtest du dieses Beispiel zum Training hinzufuegen?
</p>
<button
onClick={() => {
if (uploadedImage) {
setTrainingImage(uploadedImage)
setTrainingText(ocrResult.text)
setActiveTab('training')
}
}}
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors"
>
Als Trainingsbeispiel hinzufuegen
</button>
</div>
)}
</div>
) : (
<div className="bg-slate-50 rounded-lg p-8 text-center text-slate-400">
<div className="text-4xl mb-2">🔍</div>
<div>Lade ein Bild hoch um die Erkennung zu testen</div>
</div>
)}
</div>
</div>
</div>
{/* Confidence Heatmap (when image and result available) */}
{imagePreview && ocrResult && ocrResult.confidence > 0 && (
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900">Konfidenz-Visualisierung</h2>
<button
onClick={() => setShowHeatmap(prev => !prev)}
className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
showHeatmap
? 'bg-purple-600 text-white'
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
}`}
>
{showHeatmap ? 'Heatmap verbergen' : 'Heatmap anzeigen'}
</button>
</div>
{showHeatmap && (
<ConfidenceHeatmap
imageSrc={imagePreview}
text={ocrResult.text}
confidence={ocrResult.confidence}
wordBoxes={ocrResult.word_boxes?.map(w => ({
text: w.text,
confidence: w.confidence,
bbox: w.bbox as [number, number, number, number]
})) || []}
charConfidences={ocrResult.char_confidences || []}
showLegend={true}
toggleable={true}
/>
)}
</div>
)}
{/* Confidence Interpretation */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Konfidenz-Interpretation</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="text-green-700 font-medium">90-100%</div>
<div className="text-sm text-slate-600 mt-1">Sehr hohe Sicherheit - Text kann direkt uebernommen werden</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="text-yellow-700 font-medium">70-90%</div>
<div className="text-sm text-slate-600 mt-1">Gute Sicherheit - manuelle Ueberpruefung empfohlen</div>
</div>
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="text-red-700 font-medium">&lt; 70%</div>
<div className="text-sm text-slate-600 mt-1">Niedrige Sicherheit - manuelle Eingabe erforderlich</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'batch' && (
<div className="space-y-6">
{/* Batch OCR Processing */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-2">Batch-Verarbeitung</h2>
<p className="text-sm text-slate-500 mb-6">
Verarbeite mehrere Bilder gleichzeitig mit Echtzeit-Fortschrittsanzeige.
Die Ergebnisse werden per Server-Sent Events gestreamt.
</p>
<BatchUploader
apiBase={API_BASE}
maxFiles={20}
autoProcess={false}
onComplete={(results) => {
console.log('Batch complete:', results)
}}
/>
</div>
{/* Batch Processing Info */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-6">
<div className="text-3xl mb-2">🚀</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Parallele Verarbeitung</h3>
<p className="text-sm text-slate-600">
Mehrere Bilder werden parallel verarbeitet fuer maximale Geschwindigkeit.
</p>
</div>
<div className="bg-gradient-to-br from-green-50 to-green-100 border border-green-200 rounded-xl p-6">
<div className="text-3xl mb-2">💾</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Smart Caching</h3>
<p className="text-sm text-slate-600">
Identische Bilder werden automatisch aus dem Cache geladen (unter 50ms).
</p>
</div>
<div className="bg-gradient-to-br from-purple-50 to-purple-100 border border-purple-200 rounded-xl p-6">
<div className="text-3xl mb-2">📊</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Live-Fortschritt</h3>
<p className="text-sm text-slate-600">
Echtzeit-Updates via Server-Sent Events zeigen den Verarbeitungsfortschritt.
</p>
</div>
</div>
</div>
)}
{activeTab === 'training' && (
<div className="space-y-6">
{/* Training Overview */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Training mit LoRA</h2>
<p className="text-sm text-slate-500 mb-4">
LoRA (Low-Rank Adaptation) ermoeglicht effizientes Fine-Tuning ohne das Basismodell zu veraendern.
Das Training erfolgt lokal auf Ihrem System.
</p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-slate-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-slate-900">{status?.training_examples_count || 0}</div>
<div className="text-xs text-slate-500">Trainingsbeispiele</div>
</div>
<div className="bg-slate-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-slate-900">10</div>
<div className="text-xs text-slate-500">Minimum benoetigt</div>
</div>
<div className="bg-slate-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-slate-900">{settings.loraRank}</div>
<div className="text-xs text-slate-500">LoRA Rank</div>
</div>
<div className="bg-slate-50 rounded-lg p-4 text-center">
<div className="text-3xl font-bold text-slate-900">{status?.has_lora_adapter ? '✓' : '✗'}</div>
<div className="text-xs text-slate-500">Adapter aktiv</div>
</div>
</div>
{/* Progress Bar */}
<div className="mb-6">
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-500">Fortschritt zum Fine-Tuning</span>
<span className="text-slate-500">{Math.min(100, ((status?.training_examples_count || 0) / 10) * 100).toFixed(0)}%</span>
</div>
<div className="h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-purple-500 to-blue-500 transition-all duration-500"
style={{ width: `${Math.min(100, ((status?.training_examples_count || 0) / 10) * 100)}%` }}
/>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Add Training Example */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Trainingsbeispiel hinzufuegen</h2>
<p className="text-sm text-slate-500 mb-4">
Lade ein Bild mit handgeschriebenem Text hoch und gib die korrekte Transkription ein.
</p>
<div className="space-y-4">
<div>
<label className="block text-sm text-slate-700 mb-1">Bild</label>
<input
type="file"
accept="image/*"
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-sm"
onChange={(e) => setTrainingImage(e.target.files?.[0] || null)}
/>
{trainingImage && (
<div className="mt-2 text-xs text-green-600">
Bild ausgewaehlt: {trainingImage.name}
</div>
)}
</div>
<div>
<label className="block text-sm text-slate-700 mb-1">Korrekter Text (Ground Truth)</label>
<textarea
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-sm text-slate-900 resize-none"
rows={3}
placeholder="Gib hier den korrekten Text ein..."
value={trainingText}
onChange={(e) => setTrainingText(e.target.value)}
/>
</div>
<button
onClick={handleAddTrainingExample}
className="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium transition-colors"
>
+ Trainingsbeispiel hinzufuegen
</button>
</div>
</div>
{/* Fine-Tuning */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Fine-Tuning starten</h2>
<p className="text-sm text-slate-500 mb-4">
Trainiere das Modell mit den gesammelten Beispielen. Der Prozess dauert
je nach Anzahl der Beispiele einige Minuten.
</p>
<div className="bg-slate-50 rounded-lg p-4 mb-4">
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-slate-500">Epochen:</span>
<span className="text-slate-900 ml-2">{settings.epochs}</span>
</div>
<div>
<span className="text-slate-500">Learning Rate:</span>
<span className="text-slate-900 ml-2">{settings.learningRate}</span>
</div>
<div>
<span className="text-slate-500">LoRA Rank:</span>
<span className="text-slate-900 ml-2">{settings.loraRank}</span>
</div>
<div>
<span className="text-slate-500">Batch Size:</span>
<span className="text-slate-900 ml-2">{settings.batchSize}</span>
</div>
</div>
</div>
<button
onClick={handleFineTune}
disabled={fineTuning || (status?.training_examples_count || 0) < 10}
className="w-full px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-slate-300 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors"
>
{fineTuning ? (
<span className="flex items-center justify-center gap-2">
<SkeletonDots />
Fine-Tuning laeuft...
</span>
) : (
'Fine-Tuning starten'
)}
</button>
{(status?.training_examples_count || 0) < 10 && (
<p className="text-xs text-yellow-600 mt-2 text-center">
Noch {10 - (status?.training_examples_count || 0)} Beispiele benoetigt
</p>
)}
{/* Cross-Link to OCR-Labeling for Ground Truth */}
<Link
href="/ai/ocr-labeling?model=trocr-lora"
className="w-full mt-4 px-4 py-2 bg-teal-100 text-teal-700 border border-teal-300 rounded-lg hover:bg-teal-200 flex items-center justify-center gap-2 transition-colors"
>
<span>🏷</span>
Ground Truth in OCR-Labeling sammeln
</Link>
</div>
</div>
{/* Training Examples List */}
{examples.length > 0 && (
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Trainingsbeispiele ({examples.length})</h2>
<div className="space-y-2 max-h-64 overflow-y-auto">
{examples.map((ex, i) => (
<div key={i} className="flex items-center gap-4 bg-slate-50 rounded-lg p-3">
<span className="text-slate-400 font-mono text-sm w-8">{i + 1}.</span>
<span className="text-slate-900 text-sm flex-1 truncate">{ex.ground_truth}</span>
<span className="text-slate-400 text-xs">{new Date(ex.created_at).toLocaleDateString('de-DE')}</span>
</div>
))}
</div>
</div>
)}
{/* Training Dashboard Demo */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold text-slate-900">Training Dashboard</h2>
<p className="text-sm text-slate-500">Live-Metriken waehrend des Trainings</p>
</div>
<button
onClick={() => setShowTrainingDashboard(prev => !prev)}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
showTrainingDashboard
? 'bg-red-600 hover:bg-red-700 text-white'
: 'bg-purple-600 hover:bg-purple-700 text-white'
}`}
>
{showTrainingDashboard ? 'Demo stoppen' : 'Demo starten'}
</button>
</div>
{showTrainingDashboard && (
<TrainingMetrics
apiBase={API_BASE}
simulateMode={true}
onComplete={() => {
setShowTrainingDashboard(false)
}}
/>
)}
{!showTrainingDashboard && (
<div className="bg-slate-50 rounded-lg p-8 text-center">
<div className="text-4xl mb-3">📈</div>
<div className="text-slate-600 mb-2">
Das Training Dashboard zeigt Echtzeit-Metriken waehrend des Fine-Tunings
</div>
<div className="text-sm text-slate-400">
Klicke &quot;Demo starten&quot; um eine simulierte Training-Session zu sehen
</div>
</div>
)}
</div>
</div>
)}
{activeTab === 'architecture' && (
<div className="space-y-6">
{/* Architecture Diagram */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Systemarchitektur</h2>
{/* ASCII Art Diagram */}
<div className="bg-slate-900 rounded-lg p-6 font-mono text-xs overflow-x-auto">
<pre className="text-slate-300">
{`┌─────────────────────────────────────────────────────────────────────────────┐
│ MAGIC HELP ARCHITEKTUR │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌──────────────────┐ ┌───────────────┐ │
│ │ FRONTEND │ │ BACKEND │ │ STORAGE │ │
│ │ (Next.js) │ │ (FastAPI) │ │ │ │
│ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ REST │ ┌────────────┐ │ │ ┌─────────┐ │ │
│ │ │ Admin │──┼─────────┼──│ TrOCR │ │ │ │ Models │ │ │
│ │ │ Panel │ │ │ │ Service │──┼─────────┼──│ (ONNX) │ │ │
│ │ └─────────┘ │ │ └────────────┘ │ │ └─────────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ ┌─────────┐ │ WebSocket│ ┌────────────┐ │ │ ┌─────────┐ │ │
│ │ │ Lehrer │──┼─────────┼──│ Klausur │ │ │ │ LoRA │ │ │
│ │ │ Portal │ │ │ │ Processor │──┼─────────┼──│ Adapter │ │ │
│ │ └─────────┘ │ │ └────────────┘ │ │ └─────────┘ │ │
│ │ │ │ │ │ │ │ │
│ └───────────────┘ │ ┌────────────┐ │ │ ┌─────────┐ │ │
│ │ │ Pseudo- │ │ │ │Training │ │ │
│ │ │ nymizer │──┼─────────┼──│ Data │ │ │
│ │ └────────────┘ │ │ └─────────┘ │ │
│ │ │ │ │ │
│ └──────────────────┘ └───────────────┘ │
│ │ │
│ │ (nur pseudonymisiert) │
│ ▼ │
│ ┌──────────────────┐ │
│ │ CLOUD LLM │ │
│ │ (SysEleven) │ │
│ │ Namespace- │ │
│ │ Isolation │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘`}
</pre>
</div>
</div>
{/* Components */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<span>🔍</span> TrOCR Service
</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Modell</span>
<span className="text-slate-900">microsoft/trocr-base-handwritten</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Groesse</span>
<span className="text-slate-900">~350 MB</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Lizenz</span>
<span className="text-slate-900">MIT</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Framework</span>
<span className="text-slate-900">PyTorch / Transformers</span>
</div>
</div>
<p className="text-slate-500 text-sm mt-4">
Das TrOCR-Modell von Microsoft ist speziell fuer Handschrifterkennung trainiert.
Es verwendet eine Vision-Transformer (ViT) Architektur fuer Bildverarbeitung
und einen Text-Decoder fuer die Textgenerierung.
</p>
</div>
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<span>🎯</span> LoRA Fine-Tuning
</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Methode</span>
<span className="text-slate-900">Low-Rank Adaptation</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Adapter-Groesse</span>
<span className="text-slate-900">~10 MB</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Trainingszeit</span>
<span className="text-slate-900">5-15 Min (CPU)</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Min. Beispiele</span>
<span className="text-slate-900">10</span>
</div>
</div>
<p className="text-slate-500 text-sm mt-4">
LoRA fuegt kleine, trainierbare Matrizen zu bestimmten Schichten hinzu,
ohne das Basismodell zu veraendern. Dies ermoeglicht effizientes Fine-Tuning
mit minimaler Speichernutzung.
</p>
</div>
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<span>🔒</span> Pseudonymisierung
</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Methode</span>
<span className="text-slate-900">QR-Code Tokens</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Token-Format</span>
<span className="text-slate-900">UUID v4</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Mapping</span>
<span className="text-slate-900">Lokal beim Lehrer</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Cloud-Daten</span>
<span className="text-slate-900">Nur Tokens + Text</span>
</div>
</div>
<p className="text-slate-500 text-sm mt-4">
Schuelernamen werden durch anonyme Tokens ersetzt, bevor Daten die lokale
Umgebung verlassen. Das Mapping wird ausschliesslich lokal gespeichert.
</p>
</div>
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2">
<span></span> Cloud LLM
</h3>
<div className="space-y-3 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Provider</span>
<span className="text-slate-900">SysEleven (DE)</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Standort</span>
<span className="text-slate-900">Deutschland</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Isolation</span>
<span className="text-slate-900">Namespace pro Schule</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Datenverarbeitung</span>
<span className="text-slate-900">Nur pseudonymisiert</span>
</div>
</div>
<p className="text-slate-500 text-sm mt-4">
Die KI-Korrektur erfolgt auf deutschen Servern mit strikter Mandantentrennung.
Es werden keine Klarnamen oder identifizierenden Informationen uebertragen.
</p>
</div>
</div>
{/* Data Flow */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Datenfluss</h2>
<div className="space-y-4">
<div className="flex items-start gap-4 bg-slate-50 rounded-lg p-4">
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold">1</div>
<div>
<div className="font-medium text-slate-900">Lokale Header-Extraktion</div>
<div className="text-sm text-slate-500">TrOCR erkennt Schuelernamen, Klasse und Fach direkt im Browser/PWA (offline-faehig)</div>
</div>
</div>
<div className="flex items-start gap-4 bg-slate-50 rounded-lg p-4">
<div className="w-8 h-8 rounded-full bg-purple-100 flex items-center justify-center text-purple-600 font-bold">2</div>
<div>
<div className="font-medium text-slate-900">Pseudonymisierung</div>
<div className="text-sm text-slate-500">Namen werden durch QR-Code Tokens ersetzt, Mapping bleibt lokal</div>
</div>
</div>
<div className="flex items-start gap-4 bg-slate-50 rounded-lg p-4">
<div className="w-8 h-8 rounded-full bg-green-100 flex items-center justify-center text-green-600 font-bold">3</div>
<div>
<div className="font-medium text-slate-900">Cloud-Korrektur</div>
<div className="text-sm text-slate-500">Nur pseudonymisierte Dokument-Tokens werden an die KI gesendet</div>
</div>
</div>
<div className="flex items-start gap-4 bg-slate-50 rounded-lg p-4">
<div className="w-8 h-8 rounded-full bg-yellow-100 flex items-center justify-center text-yellow-600 font-bold">4</div>
<div>
<div className="font-medium text-slate-900">Re-Identifikation</div>
<div className="text-sm text-slate-500">Ergebnisse werden lokal mit dem Mapping wieder den echten Namen zugeordnet</div>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'settings' && (
<div className="space-y-6">
{/* OCR Settings */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">OCR Einstellungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.autoDetectLines}
onChange={(e) => setSettings({ ...settings, autoDetectLines: e.target.checked })}
className="w-5 h-5 rounded bg-slate-100 border-slate-300"
/>
<div>
<div className="text-slate-900 font-medium">Automatische Zeilenerkennung</div>
<div className="text-sm text-slate-500">Erkennt und verarbeitet einzelne Zeilen separat</div>
</div>
</label>
</div>
<div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.livePreview}
onChange={(e) => setSettings({ ...settings, livePreview: e.target.checked })}
className="w-5 h-5 rounded bg-slate-100 border-slate-300"
/>
<div>
<div className="text-slate-900 font-medium">Live-Vorschau</div>
<div className="text-sm text-slate-500">OCR startet automatisch nach Bild-Upload</div>
</div>
</label>
</div>
<div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.soundFeedback}
onChange={(e) => setSettings({ ...settings, soundFeedback: e.target.checked })}
className="w-5 h-5 rounded bg-slate-100 border-slate-300"
/>
<div>
<div className="text-slate-900 font-medium">Sound-Feedback</div>
<div className="text-sm text-slate-500">Akustisches Feedback bei erfolgreicher Erkennung</div>
</div>
</label>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">Konfidenz-Schwellwert</label>
<input
type="range"
min="0"
max="1"
step="0.1"
value={settings.confidenceThreshold}
onChange={(e) => setSettings({ ...settings, confidenceThreshold: parseFloat(e.target.value) })}
className="w-full"
/>
<div className="flex justify-between text-xs text-slate-400 mt-1">
<span>0%</span>
<span className="text-slate-900">{(settings.confidenceThreshold * 100).toFixed(0)}%</span>
<span>100%</span>
</div>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">Max. Bildgroesse (px)</label>
<input
type="number"
value={settings.maxImageSize}
onChange={(e) => setSettings({ ...settings, maxImageSize: parseInt(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
<div className="text-xs text-slate-400 mt-1">Groessere Bilder werden skaliert</div>
</div>
<div>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={settings.enableCache}
onChange={(e) => setSettings({ ...settings, enableCache: e.target.checked })}
className="w-5 h-5 rounded bg-slate-100 border-slate-300"
/>
<div>
<div className="text-slate-900 font-medium">Ergebnis-Cache aktivieren</div>
<div className="text-sm text-slate-500">Speichert OCR-Ergebnisse fuer identische Bilder</div>
</div>
</label>
</div>
</div>
</div>
{/* Training Settings */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Training Einstellungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm text-slate-700 mb-2">LoRA Rank</label>
<select
value={settings.loraRank}
onChange={(e) => setSettings({ ...settings, loraRank: parseInt(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
>
<option value="4">4 (Schnell, weniger Kapazitaet)</option>
<option value="8">8 (Ausgewogen)</option>
<option value="16">16 (Mehr Kapazitaet)</option>
<option value="32">32 (Maximum)</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">LoRA Alpha</label>
<input
type="number"
value={settings.loraAlpha}
onChange={(e) => setSettings({ ...settings, loraAlpha: parseInt(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
<div className="text-xs text-slate-400 mt-1">Empfohlen: 4 x LoRA Rank</div>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">Epochen</label>
<input
type="number"
min="1"
max="10"
value={settings.epochs}
onChange={(e) => setSettings({ ...settings, epochs: parseInt(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">Batch Size</label>
<select
value={settings.batchSize}
onChange={(e) => setSettings({ ...settings, batchSize: parseInt(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
>
<option value="1">1 (Wenig RAM)</option>
<option value="2">2</option>
<option value="4">4 (Standard)</option>
<option value="8">8 (Viel RAM)</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-700 mb-2">Learning Rate</label>
<select
value={settings.learningRate}
onChange={(e) => setSettings({ ...settings, learningRate: parseFloat(e.target.value) })}
className="w-full bg-slate-50 border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
>
<option value="0.0001">0.0001 (Schnell)</option>
<option value="0.00005">0.00005 (Standard)</option>
<option value="0.00001">0.00001 (Konservativ)</option>
</select>
</div>
</div>
</div>
{/* Save Button */}
<div className="flex justify-end gap-4">
<button
onClick={() => setSettings(DEFAULT_SETTINGS)}
className="px-6 py-2 bg-slate-200 hover:bg-slate-300 text-slate-700 rounded-lg text-sm font-medium transition-colors"
>
Zuruecksetzen
</button>
<button
onClick={saveSettings}
className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium transition-colors"
>
{settingsSaved ? '✓ Gespeichert!' : 'Einstellungen speichern'}
</button>
</div>
{/* Technical Info */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Technische Informationen</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-slate-500">API Endpoint:</span>
<code className="text-slate-900 ml-2 bg-slate-100 px-2 py-1 rounded text-xs">/api/klausur/trocr</code>
</div>
<div>
<span className="text-slate-500">Model Path:</span>
<code className="text-slate-900 ml-2 bg-slate-100 px-2 py-1 rounded text-xs">~/.cache/huggingface</code>
</div>
<div>
<span className="text-slate-500">LoRA Path:</span>
<code className="text-slate-900 ml-2 bg-slate-100 px-2 py-1 rounded text-xs">./models/lora</code>
</div>
<div>
<span className="text-slate-500">Training Data:</span>
<code className="text-slate-900 ml-2 bg-slate-100 px-2 py-1 rounded text-xs">./data/training</code>
</div>
</div>
</div>
</div>
)}
</div>
)
}