Fix: Remove broken getKlausurApiUrl and clean up empty lines
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
sed replacement left orphaned hostname references in story page and empty lines in getApiBase functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
'use client'
|
||||
|
||||
interface GlobalDragOverlayProps {
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export function GlobalDragOverlay({ active }: GlobalDragOverlayProps) {
|
||||
if (!active) return null
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
interface KeyboardShortcutsModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function KeyboardShortcutsModal({ open, onClose }: KeyboardShortcutsModalProps) {
|
||||
if (!open) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-40 bg-black/50 flex items-center justify-center" onClick={onClose}>
|
||||
<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={onClose}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
'use client'
|
||||
|
||||
export function TabArchitecture() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Architecture Diagram */}
|
||||
<ArchitectureDiagram />
|
||||
|
||||
{/* Components */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<ComponentCard
|
||||
icon="🔍"
|
||||
title="TrOCR Service"
|
||||
description="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."
|
||||
specs={[
|
||||
{ label: 'Modell', value: 'microsoft/trocr-base-handwritten' },
|
||||
{ label: 'Groesse', value: '~350 MB' },
|
||||
{ label: 'Lizenz', value: 'MIT' },
|
||||
{ label: 'Framework', value: 'PyTorch / Transformers' },
|
||||
]}
|
||||
/>
|
||||
<ComponentCard
|
||||
icon="🎯"
|
||||
title="LoRA Fine-Tuning"
|
||||
description="LoRA fuegt kleine, trainierbare Matrizen zu bestimmten Schichten hinzu, ohne das Basismodell zu veraendern. Dies ermoeglicht effizientes Fine-Tuning mit minimaler Speichernutzung."
|
||||
specs={[
|
||||
{ label: 'Methode', value: 'Low-Rank Adaptation' },
|
||||
{ label: 'Adapter-Groesse', value: '~10 MB' },
|
||||
{ label: 'Trainingszeit', value: '5-15 Min (CPU)' },
|
||||
{ label: 'Min. Beispiele', value: '10' },
|
||||
]}
|
||||
/>
|
||||
<ComponentCard
|
||||
icon="🔒"
|
||||
title="Pseudonymisierung"
|
||||
description="Schuelernamen werden durch anonyme Tokens ersetzt, bevor Daten die lokale Umgebung verlassen. Das Mapping wird ausschliesslich lokal gespeichert."
|
||||
specs={[
|
||||
{ label: 'Methode', value: 'QR-Code Tokens' },
|
||||
{ label: 'Token-Format', value: 'UUID v4' },
|
||||
{ label: 'Mapping', value: 'Lokal beim Lehrer' },
|
||||
{ label: 'Cloud-Daten', value: 'Nur Tokens + Text' },
|
||||
]}
|
||||
/>
|
||||
<ComponentCard
|
||||
icon="☁️"
|
||||
title="Cloud LLM"
|
||||
description="Die KI-Korrektur erfolgt auf deutschen Servern mit strikter Mandantentrennung. Es werden keine Klarnamen oder identifizierenden Informationen uebertragen."
|
||||
specs={[
|
||||
{ label: 'Provider', value: 'SysEleven (DE)' },
|
||||
{ label: 'Standort', value: 'Deutschland' },
|
||||
{ label: 'Isolation', value: 'Namespace pro Schule' },
|
||||
{ label: 'Datenverarbeitung', value: 'Nur pseudonymisiert' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Data Flow */}
|
||||
<DataFlowCard />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function ArchitectureDiagram() {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Systemarchitektur</h2>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
interface ComponentCardProps {
|
||||
icon: string
|
||||
title: string
|
||||
description: string
|
||||
specs: Array<{ label: string; value: string }>
|
||||
}
|
||||
|
||||
function ComponentCard({ icon, title, description, specs }: ComponentCardProps) {
|
||||
return (
|
||||
<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>{icon}</span> {title}
|
||||
</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
{specs.map((spec) => (
|
||||
<div key={spec.label} className="flex justify-between">
|
||||
<span className="text-slate-500">{spec.label}</span>
|
||||
<span className="text-slate-900">{spec.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-slate-500 text-sm mt-4">{description}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DATA_FLOW_STEPS = [
|
||||
{
|
||||
num: 1,
|
||||
color: 'bg-blue-100 text-blue-600',
|
||||
title: 'Lokale Header-Extraktion',
|
||||
desc: 'TrOCR erkennt Schuelernamen, Klasse und Fach direkt im Browser/PWA (offline-faehig)',
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
color: 'bg-purple-100 text-purple-600',
|
||||
title: 'Pseudonymisierung',
|
||||
desc: 'Namen werden durch QR-Code Tokens ersetzt, Mapping bleibt lokal',
|
||||
},
|
||||
{
|
||||
num: 3,
|
||||
color: 'bg-green-100 text-green-600',
|
||||
title: 'Cloud-Korrektur',
|
||||
desc: 'Nur pseudonymisierte Dokument-Tokens werden an die KI gesendet',
|
||||
},
|
||||
{
|
||||
num: 4,
|
||||
color: 'bg-yellow-100 text-yellow-600',
|
||||
title: 'Re-Identifikation',
|
||||
desc: 'Ergebnisse werden lokal mit dem Mapping wieder den echten Namen zugeordnet',
|
||||
},
|
||||
]
|
||||
|
||||
function DataFlowCard() {
|
||||
return (
|
||||
<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">
|
||||
{DATA_FLOW_STEPS.map((step) => (
|
||||
<div key={step.num} className="flex items-start gap-4 bg-slate-50 rounded-lg p-4">
|
||||
<div className={`w-8 h-8 rounded-full ${step.color} flex items-center justify-center font-bold`}>
|
||||
{step.num}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">{step.title}</div>
|
||||
<div className="text-sm text-slate-500">{step.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
'use client'
|
||||
|
||||
import { BatchUploader } from '@/components/ai/BatchUploader'
|
||||
import { API_BASE } from '../types'
|
||||
|
||||
export function TabBatch() {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import { SkeletonText } from '@/components/common/SkeletonText'
|
||||
import type { TrOCRStatus } from '../types'
|
||||
|
||||
interface TabOverviewProps {
|
||||
status: TrOCRStatus | null
|
||||
loading: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function TabOverview({ status, loading, onRefresh }: TabOverviewProps) {
|
||||
return (
|
||||
<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={onRefresh}
|
||||
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">
|
||||
{WORKFLOW_STEPS.map((step, i) => (
|
||||
<WorkflowStep key={step.title} step={step} showArrow={i < WORKFLOW_STEPS.length - 1} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const WORKFLOW_STEPS = [
|
||||
{ icon: '📄', title: '1. Upload', desc: '25 Klausuren hochladen' },
|
||||
{ icon: '🔍', title: '2. Analyse', desc: 'Lokale OCR in 5-10 Sek' },
|
||||
{ icon: '✅', title: '3. Bestaetigung', desc: 'Klasse, Schueler, Fach' },
|
||||
{ icon: '🤖', title: '4. KI-Korrektur', desc: 'Cloud mit Pseudonymisierung' },
|
||||
{ icon: '📊', title: '5. Integration', desc: 'Notenbuch, Zeugnisse' },
|
||||
]
|
||||
|
||||
function WorkflowStep({ step, showArrow }: { step: typeof WORKFLOW_STEPS[number]; showArrow: boolean }) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 bg-slate-50 rounded-lg px-4 py-3">
|
||||
<span className="text-2xl">{step.icon}</span>
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">{step.title}</div>
|
||||
<div className="text-slate-500">{step.desc}</div>
|
||||
</div>
|
||||
</div>
|
||||
{showArrow && <div className="text-slate-400">→</div>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
'use client'
|
||||
|
||||
import type { MagicSettings } from '../types'
|
||||
import { DEFAULT_SETTINGS } from '../types'
|
||||
|
||||
interface TabSettingsProps {
|
||||
settings: MagicSettings
|
||||
settingsSaved: boolean
|
||||
onUpdateSettings: (settings: MagicSettings) => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
export function TabSettings({ settings, settingsSaved, onUpdateSettings, onSave }: TabSettingsProps) {
|
||||
const update = (partial: Partial<MagicSettings>) => {
|
||||
onUpdateSettings({ ...settings, ...partial })
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<CheckboxSetting
|
||||
label="Automatische Zeilenerkennung"
|
||||
description="Erkennt und verarbeitet einzelne Zeilen separat"
|
||||
checked={settings.autoDetectLines}
|
||||
onChange={(v) => update({ autoDetectLines: v })}
|
||||
/>
|
||||
|
||||
<CheckboxSetting
|
||||
label="Live-Vorschau"
|
||||
description="OCR startet automatisch nach Bild-Upload"
|
||||
checked={settings.livePreview}
|
||||
onChange={(v) => update({ livePreview: v })}
|
||||
/>
|
||||
|
||||
<CheckboxSetting
|
||||
label="Sound-Feedback"
|
||||
description="Akustisches Feedback bei erfolgreicher Erkennung"
|
||||
checked={settings.soundFeedback}
|
||||
onChange={(v) => update({ soundFeedback: v })}
|
||||
/>
|
||||
|
||||
<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) => update({ 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) => update({ 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>
|
||||
|
||||
<CheckboxSetting
|
||||
label="Ergebnis-Cache aktivieren"
|
||||
description="Speichert OCR-Ergebnisse fuer identische Bilder"
|
||||
checked={settings.enableCache}
|
||||
onChange={(v) => update({ enableCache: v })}
|
||||
/>
|
||||
</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) => update({ 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) => update({ 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) => update({ 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) => update({ 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) => update({ 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={() => onUpdateSettings(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={onSave}
|
||||
className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
{settingsSaved ? '\u2713 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>
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function CheckboxSetting({
|
||||
label,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
label: string
|
||||
description: string
|
||||
checked: boolean
|
||||
onChange: (value: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
className="w-5 h-5 rounded bg-slate-100 border-slate-300"
|
||||
/>
|
||||
<div>
|
||||
<div className="text-slate-900 font-medium">{label}</div>
|
||||
<div className="text-sm text-slate-500">{description}</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
304
admin-lehrer/app/(admin)/ai/magic-help/_components/TabTest.tsx
Normal file
304
admin-lehrer/app/(admin)/ai/magic-help/_components/TabTest.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
'use client'
|
||||
|
||||
import { SkeletonOCRResult, SkeletonDots } from '@/components/common/SkeletonText'
|
||||
import { ConfidenceHeatmap } from '@/components/ai/ConfidenceHeatmap'
|
||||
import type { OCRResult, MagicSettings } from '../types'
|
||||
|
||||
interface TabTestProps {
|
||||
ocrResult: OCRResult | null
|
||||
ocrLoading: boolean
|
||||
imagePreview: string | null
|
||||
uploadedImage: File | null
|
||||
settings: MagicSettings
|
||||
showHeatmap: boolean
|
||||
onToggleHeatmap: () => void
|
||||
onFileUpload: (file: File) => void
|
||||
onManualOCR: () => void
|
||||
onClearImage: () => void
|
||||
onSendToTraining: () => void
|
||||
}
|
||||
|
||||
function getConfidenceColor(confidence: number) {
|
||||
if (confidence >= 0.9) return 'bg-green-500'
|
||||
if (confidence >= 0.7) return 'bg-yellow-500'
|
||||
return 'bg-red-500'
|
||||
}
|
||||
|
||||
export function TabTest({
|
||||
ocrResult,
|
||||
ocrLoading,
|
||||
imagePreview,
|
||||
uploadedImage,
|
||||
settings,
|
||||
showHeatmap,
|
||||
onToggleHeatmap,
|
||||
onFileUpload,
|
||||
onManualOCR,
|
||||
onClearImage,
|
||||
onSendToTraining,
|
||||
}: TabTestProps) {
|
||||
return (
|
||||
<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 */}
|
||||
<UploadArea
|
||||
imagePreview={imagePreview}
|
||||
uploadedImage={uploadedImage}
|
||||
ocrLoading={ocrLoading}
|
||||
livePreview={settings.livePreview}
|
||||
onFileUpload={onFileUpload}
|
||||
onManualOCR={onManualOCR}
|
||||
onClearImage={onClearImage}
|
||||
/>
|
||||
|
||||
{/* Results Area */}
|
||||
<ResultsArea
|
||||
ocrResult={ocrResult}
|
||||
ocrLoading={ocrLoading}
|
||||
onSendToTraining={onSendToTraining}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Confidence Heatmap */}
|
||||
{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={onToggleHeatmap}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Sub-components */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
interface UploadAreaProps {
|
||||
imagePreview: string | null
|
||||
uploadedImage: File | null
|
||||
ocrLoading: boolean
|
||||
livePreview: boolean
|
||||
onFileUpload: (file: File) => void
|
||||
onManualOCR: () => void
|
||||
onClearImage: () => void
|
||||
}
|
||||
|
||||
function UploadArea({ imagePreview, uploadedImage, ocrLoading, livePreview, onFileUpload, onManualOCR, onClearImage }: UploadAreaProps) {
|
||||
return (
|
||||
<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/')) onFileUpload(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()
|
||||
onClearImage()
|
||||
}}
|
||||
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) onFileUpload(file)
|
||||
}}
|
||||
/>
|
||||
|
||||
{uploadedImage && !livePreview && (
|
||||
<button
|
||||
onClick={onManualOCR}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
interface ResultsAreaProps {
|
||||
ocrResult: OCRResult | null
|
||||
ocrLoading: boolean
|
||||
onSendToTraining: () => void
|
||||
}
|
||||
|
||||
function ResultsArea({ ocrResult, ocrLoading, onSendToTraining }: ResultsAreaProps) {
|
||||
if (ocrLoading) return <SkeletonOCRResult />
|
||||
|
||||
if (!ocrResult) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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>
|
||||
|
||||
{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={onSendToTraining}
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { SkeletonDots } from '@/components/common/SkeletonText'
|
||||
import { TrainingMetrics } from '@/components/ai/TrainingMetrics'
|
||||
import type { TrOCRStatus, TrainingExample, MagicSettings } from '../types'
|
||||
import { API_BASE } from '../types'
|
||||
|
||||
interface TabTrainingProps {
|
||||
status: TrOCRStatus | null
|
||||
examples: TrainingExample[]
|
||||
trainingImage: File | null
|
||||
trainingText: string
|
||||
fineTuning: boolean
|
||||
settings: MagicSettings
|
||||
showTrainingDashboard: boolean
|
||||
onSetTrainingImage: (file: File | null) => void
|
||||
onSetTrainingText: (text: string) => void
|
||||
onAddExample: () => void
|
||||
onFineTune: () => void
|
||||
onToggleDashboard: () => void
|
||||
}
|
||||
|
||||
export function TabTraining({
|
||||
status,
|
||||
examples,
|
||||
trainingImage,
|
||||
trainingText,
|
||||
fineTuning,
|
||||
settings,
|
||||
showTrainingDashboard,
|
||||
onSetTrainingImage,
|
||||
onSetTrainingText,
|
||||
onAddExample,
|
||||
onFineTune,
|
||||
onToggleDashboard,
|
||||
}: TabTrainingProps) {
|
||||
const exampleCount = status?.training_examples_count || 0
|
||||
const progressPct = Math.min(100, (exampleCount / 10) * 100)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Training Overview */}
|
||||
<TrainingOverviewCard
|
||||
status={status}
|
||||
settings={settings}
|
||||
exampleCount={exampleCount}
|
||||
progressPct={progressPct}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Add Training Example */}
|
||||
<AddExampleCard
|
||||
trainingImage={trainingImage}
|
||||
trainingText={trainingText}
|
||||
onSetTrainingImage={onSetTrainingImage}
|
||||
onSetTrainingText={onSetTrainingText}
|
||||
onAddExample={onAddExample}
|
||||
/>
|
||||
|
||||
{/* Fine-Tuning */}
|
||||
<FineTuningCard
|
||||
settings={settings}
|
||||
fineTuning={fineTuning}
|
||||
exampleCount={exampleCount}
|
||||
hasLoraAdapter={status?.has_lora_adapter || false}
|
||||
onFineTune={onFineTune}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Training Examples List */}
|
||||
{examples.length > 0 && (
|
||||
<ExamplesListCard examples={examples} />
|
||||
)}
|
||||
|
||||
{/* Training Dashboard Demo */}
|
||||
<TrainingDashboardCard
|
||||
showDashboard={showTrainingDashboard}
|
||||
onToggle={onToggleDashboard}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function TrainingOverviewCard({
|
||||
status,
|
||||
settings,
|
||||
exampleCount,
|
||||
progressPct,
|
||||
}: {
|
||||
status: TrOCRStatus | null
|
||||
settings: MagicSettings
|
||||
exampleCount: number
|
||||
progressPct: number
|
||||
}) {
|
||||
return (
|
||||
<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">{exampleCount}</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 ? '\u2713' : '\u2717'}</div>
|
||||
<div className="text-xs text-slate-500">Adapter aktiv</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">{progressPct.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: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AddExampleCard({
|
||||
trainingImage,
|
||||
trainingText,
|
||||
onSetTrainingImage,
|
||||
onSetTrainingText,
|
||||
onAddExample,
|
||||
}: {
|
||||
trainingImage: File | null
|
||||
trainingText: string
|
||||
onSetTrainingImage: (file: File | null) => void
|
||||
onSetTrainingText: (text: string) => void
|
||||
onAddExample: () => void
|
||||
}) {
|
||||
return (
|
||||
<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) => onSetTrainingImage(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) => onSetTrainingText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={onAddExample}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
function FineTuningCard({
|
||||
settings,
|
||||
fineTuning,
|
||||
exampleCount,
|
||||
hasLoraAdapter,
|
||||
onFineTune,
|
||||
}: {
|
||||
settings: MagicSettings
|
||||
fineTuning: boolean
|
||||
exampleCount: number
|
||||
hasLoraAdapter: boolean
|
||||
onFineTune: () => void
|
||||
}) {
|
||||
return (
|
||||
<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={onFineTune}
|
||||
disabled={fineTuning || exampleCount < 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>
|
||||
|
||||
{exampleCount < 10 && (
|
||||
<p className="text-xs text-yellow-600 mt-2 text-center">
|
||||
Noch {10 - exampleCount} Beispiele benoetigt
|
||||
</p>
|
||||
)}
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
function ExamplesListCard({ examples }: { examples: TrainingExample[] }) {
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
function TrainingDashboardCard({
|
||||
showDashboard,
|
||||
onToggle,
|
||||
}: {
|
||||
showDashboard: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<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={onToggle}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
showDashboard
|
||||
? 'bg-red-600 hover:bg-red-700 text-white'
|
||||
: 'bg-purple-600 hover:bg-purple-700 text-white'
|
||||
}`}
|
||||
>
|
||||
{showDashboard ? 'Demo stoppen' : 'Demo starten'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showDashboard ? (
|
||||
<TrainingMetrics
|
||||
apiBase={API_BASE}
|
||||
simulateMode={true}
|
||||
onComplete={onToggle}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export { GlobalDragOverlay, KeyboardShortcutsModal } from './GlobalOverlays'
|
||||
export { TabOverview } from './TabOverview'
|
||||
export { TabTest } from './TabTest'
|
||||
export { TabBatch } from './TabBatch'
|
||||
export { TabTraining } from './TabTraining'
|
||||
export { TabArchitecture } from './TabArchitecture'
|
||||
export { TabSettings } from './TabSettings'
|
||||
Reference in New Issue
Block a user