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>
334 lines
11 KiB
TypeScript
334 lines
11 KiB
TypeScript
'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>
|
||
)
|
||
}
|