/** * 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(null) const [simulationInterval, setSimulationInterval] = useState(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 (
{error}
) } if (!status) { return (
Warte auf Training-Daten...
) } return (
{/* Header */}

Training Dashboard

{status.status === 'running' && ( )} {status.status === 'running' ? 'Laeuft' : status.status === 'completed' ? 'Abgeschlossen' : status.status === 'failed' ? 'Fehlgeschlagen' : status.status}
{connected && ( Live )}
{simulateMode && status.status === 'running' && ( )}
{/* Main content */}
{/* Progress section */}
Epoche {status.current_epoch} / {status.total_epochs}
Schritt {status.current_step} / {status.total_steps}
{/* Loss chart */}

Loss-Verlauf

{/* Metrics grid */}
{status.metrics.loss.toFixed(4)}
Aktueller Loss
{status.metrics.val_loss !== undefined && (
{status.metrics.val_loss.toFixed(4)}
Validation Loss
)} {status.metrics.accuracy !== undefined && (
{(status.metrics.accuracy * 100).toFixed(1)}%
Genauigkeit
)}
{status.metrics.learning_rate.toExponential(1)}
Learning Rate
{/* Time info */}
Vergangen: {formatDuration(status.elapsed_time_ms)}
{status.status === 'running' && (
Geschaetzt: {formatDuration(status.estimated_remaining_ms)} verbleibend
)}
) } /** * 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 (
Epoche {currentEpoch}/{totalEpochs} {status === 'running' ? 'Laeuft' : status === 'completed' ? 'Fertig' : 'Fehler'}
Loss: {loss.toFixed(4)}
) } export default TrainingMetrics