Files
breakpilot-lehrer/admin-lehrer/components/ai/TrainingMetrics.tsx
Benjamin Admin b681ddb131 [split-required] Split 58 monoliths across Python, Go, TypeScript (Phases 1-3)
Phase 1 — Python (klausur-service): 5 monoliths → 36 files
- dsfa_corpus_ingestion.py (1,828 LOC → 5 files)
- cv_ocr_engines.py (2,102 LOC → 7 files)
- cv_layout.py (3,653 LOC → 10 files)
- vocab_worksheet_api.py (2,783 LOC → 8 files)
- grid_build_core.py (1,958 LOC → 6 files)

Phase 2 — Go (edu-search-service, school-service): 8 monoliths → 19 files
- staff_crawler.go (1,402 → 4), policy/store.go (1,168 → 3)
- policy_handlers.go (700 → 2), repository.go (684 → 2)
- search.go (592 → 2), ai_extraction_handlers.go (554 → 2)
- seed_data.go (591 → 2), grade_service.go (646 → 2)

Phase 3 — TypeScript (admin-lehrer): 45 monoliths → 220+ files
- sdk/types.ts (2,108 → 16 domain files)
- ai/rag/page.tsx (2,686 → 14 files)
- 22 page.tsx files split into _components/ + _hooks/
- 11 component files split into sub-components
- 10 SDK data catalogs added to loc-exceptions
- Deleted dead backup index_original.ts (4,899 LOC)

All original public APIs preserved via re-export facades.
Zero new errors: Python imports verified, Go builds clean,
TypeScript tsc --noEmit shows only pre-existing errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 17:28:57 +02:00

285 lines
9.5 KiB
TypeScript

/**
* Training Metrics Component
*
* Real-time visualization of training progress with loss curves and metrics.
* Supports SSE (Server-Sent Events) for live updates during LoRA fine-tuning.
*
* Phase 3.3: Training Dashboard with Live Metrics
*/
'use client'
import { useState, useEffect } from 'react'
import type { TrainingStatus, TrainingMetricsProps, TrainingDataPoint } from './training-metrics-types'
import { formatDuration } from './training-metrics-types'
import { useTrainingMetricsSSE } from './useTrainingMetricsSSE'
import { LossChart, ProgressRing } from './LossChart'
// Re-export public API so existing imports keep working
export { useTrainingMetricsSSE } from './useTrainingMetricsSSE'
export type { TrainingStatus, TrainingDataPoint, TrainingMetricsProps } from './training-metrics-types'
/**
* Training Metrics Component - Full Dashboard
*/
export function TrainingMetrics({
apiBase,
jobId = null,
simulateMode = false,
className = '',
onComplete,
onError
}: TrainingMetricsProps) {
const [status, setStatus] = useState<TrainingStatus | null>(null)
const [simulationInterval, setSimulationInterval] = useState<NodeJS.Timeout | null>(null)
// SSE hook for real connection
const { status: sseStatus, connected, error } = useTrainingMetricsSSE(
apiBase,
simulateMode ? null : jobId,
(newStatus) => {
if (newStatus.status === 'completed') {
onComplete?.(newStatus)
} else if (newStatus.status === 'failed' && newStatus.error) {
onError?.(newStatus.error)
}
}
)
// Use SSE status if available
useEffect(() => {
if (sseStatus) {
setStatus(sseStatus)
}
}, [sseStatus])
// Simulation mode for demo
useEffect(() => {
if (!simulateMode) return
let step = 0
const totalSteps = 100
const history: TrainingDataPoint[] = []
const interval = setInterval(() => {
step++
const epoch = Math.floor((step / totalSteps) * 3) + 1
const progress = (step / totalSteps) * 100
// Simulate decreasing loss with noise
const baseLoss = 2.5 * Math.exp(-step / 30) + 0.1
const noise = (Math.random() - 0.5) * 0.1
const loss = Math.max(0.05, baseLoss + noise)
const dataPoint: TrainingDataPoint = {
epoch,
step,
loss,
val_loss: step % 10 === 0 ? loss * (1 + Math.random() * 0.2) : undefined,
learning_rate: 0.00005 * Math.pow(0.95, epoch - 1),
timestamp: Date.now()
}
history.push(dataPoint)
const newStatus: TrainingStatus = {
job_id: 'simulation',
status: step >= totalSteps ? 'completed' : 'running',
progress,
current_epoch: epoch,
total_epochs: 3,
current_step: step,
total_steps: totalSteps,
elapsed_time_ms: step * 500,
estimated_remaining_ms: (totalSteps - step) * 500,
metrics: {
loss,
val_loss: dataPoint.val_loss,
accuracy: 0.7 + (step / totalSteps) * 0.25,
learning_rate: dataPoint.learning_rate
},
history: [...history]
}
setStatus(newStatus)
if (step >= totalSteps) {
clearInterval(interval)
onComplete?.(newStatus)
}
}, 500)
setSimulationInterval(interval)
return () => {
clearInterval(interval)
}
}, [simulateMode, onComplete])
// Stop simulation
const stopSimulation = () => {
if (simulationInterval) {
clearInterval(simulationInterval)
setSimulationInterval(null)
}
}
if (error) {
return (
<div className={`bg-red-50 border border-red-200 rounded-xl p-6 ${className}`}>
<div className="flex items-center gap-3 text-red-700">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{error}</span>
</div>
</div>
)
}
if (!status) {
return (
<div className={`bg-slate-50 rounded-xl p-6 ${className}`}>
<div className="flex items-center justify-center gap-3 text-slate-500">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-purple-600"></div>
<span>Warte auf Training-Daten...</span>
</div>
</div>
)
}
return (
<div className={`bg-white rounded-xl shadow-sm border ${className}`}>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<div className="flex items-center gap-3">
<h3 className="text-lg font-semibold text-slate-900">Training Dashboard</h3>
<div className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
status.status === 'running' ? 'bg-blue-100 text-blue-700' :
status.status === 'completed' ? 'bg-green-100 text-green-700' :
status.status === 'failed' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-700'
}`}>
{status.status === 'running' && (
<span className="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
)}
{status.status === 'running' ? 'Laeuft' :
status.status === 'completed' ? 'Abgeschlossen' :
status.status === 'failed' ? 'Fehlgeschlagen' :
status.status}
</div>
{connected && (
<span className="text-xs text-green-600 flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-green-500"></span>
Live
</span>
)}
</div>
{simulateMode && status.status === 'running' && (
<button
onClick={stopSimulation}
className="px-3 py-1 text-sm bg-slate-200 hover:bg-slate-300 text-slate-700 rounded transition-colors"
>
Stoppen
</button>
)}
</div>
{/* Main content */}
<div className="p-4 grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Progress section */}
<div className="flex flex-col items-center justify-center gap-4">
<ProgressRing progress={status.progress} size={120} strokeWidth={8} />
<div className="text-center">
<div className="text-sm text-slate-500">
Epoche {status.current_epoch} / {status.total_epochs}
</div>
<div className="text-xs text-slate-400">
Schritt {status.current_step} / {status.total_steps}
</div>
</div>
</div>
{/* Loss chart */}
<div className="lg:col-span-2">
<h4 className="text-sm font-medium text-slate-700 mb-2">Loss-Verlauf</h4>
<LossChart data={status.history} height={180} />
</div>
</div>
{/* Metrics grid */}
<div className="px-4 pb-4 grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-slate-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-slate-900">{status.metrics.loss.toFixed(4)}</div>
<div className="text-xs text-slate-500">Aktueller Loss</div>
</div>
{status.metrics.val_loss !== undefined && (
<div className="bg-slate-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-slate-900">{status.metrics.val_loss.toFixed(4)}</div>
<div className="text-xs text-slate-500">Validation Loss</div>
</div>
)}
{status.metrics.accuracy !== undefined && (
<div className="bg-green-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-green-700">{(status.metrics.accuracy * 100).toFixed(1)}%</div>
<div className="text-xs text-slate-500">Genauigkeit</div>
</div>
)}
<div className="bg-slate-50 rounded-lg p-3 text-center">
<div className="text-lg font-bold text-slate-900">{status.metrics.learning_rate.toExponential(1)}</div>
<div className="text-xs text-slate-500">Learning Rate</div>
</div>
</div>
{/* Time info */}
<div className="px-4 pb-4 flex items-center justify-between text-sm text-slate-500">
<div>Vergangen: {formatDuration(status.elapsed_time_ms)}</div>
{status.status === 'running' && (
<div>Geschaetzt: {formatDuration(status.estimated_remaining_ms)} verbleibend</div>
)}
</div>
</div>
)
}
/**
* Compact Training Metrics for inline display
*/
export function TrainingMetricsCompact({
progress,
currentEpoch,
totalEpochs,
loss,
status,
className = ''
}: {
progress: number
currentEpoch: number
totalEpochs: number
loss: number
status: 'running' | 'completed' | 'failed'
className?: string
}) {
return (
<div className={`flex items-center gap-4 ${className}`}>
<ProgressRing progress={progress} size={48} strokeWidth={4} />
<div className="flex-1">
<div className="flex items-center gap-2 text-sm">
<span className="font-medium text-slate-900">Epoche {currentEpoch}/{totalEpochs}</span>
<span className={`px-2 py-0.5 rounded-full text-xs ${
status === 'running' ? 'bg-blue-100 text-blue-700' :
status === 'completed' ? 'bg-green-100 text-green-700' :
'bg-red-100 text-red-700'
}`}>
{status === 'running' ? 'Laeuft' : status === 'completed' ? 'Fertig' : 'Fehler'}
</span>
</div>
<div className="text-xs text-slate-500 mt-1">
Loss: {loss.toFixed(4)}
</div>
</div>
</div>
)
}
export default TrainingMetrics