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>
1605 lines
72 KiB
TypeScript
1605 lines
72 KiB
TypeScript
'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">< 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 "Demo starten" 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>
|
||
)
|
||
}
|