'use client' import { useState, useEffect, useCallback, useRef } from 'react' // ============================================================================ // TYPES // ============================================================================ interface TrainingJob { id: string name: string model_type: 'zeugnis' | 'klausur' | 'general' status: 'queued' | 'preparing' | 'training' | 'validating' | 'completed' | 'failed' | 'paused' progress: number current_epoch: number total_epochs: number loss: number val_loss: number learning_rate: number documents_processed: number total_documents: number started_at: string | null estimated_completion: string | null error_message: string | null metrics: TrainingMetrics config: TrainingConfig } interface TrainingMetrics { precision: number recall: number f1_score: number accuracy: number loss_history: number[] val_loss_history: number[] confusion_matrix?: number[][] } interface TrainingConfig { batch_size: number learning_rate: number epochs: number warmup_steps: number weight_decay: number gradient_accumulation: number mixed_precision: boolean bundeslaender: string[] } interface DatasetStats { total_documents: number total_chunks: number training_allowed: number by_bundesland: Record by_doc_type: Record } interface ModelVersion { id: string version: string created_at: string metrics: TrainingMetrics is_active: boolean size_mb: number } // ============================================================================ // MOCK DATA (Replace with real API calls) // ============================================================================ const MOCK_JOBS: TrainingJob[] = [ { id: 'job-1', name: 'Zeugnis-RAG v2.1', model_type: 'zeugnis', status: 'training', progress: 67, current_epoch: 7, total_epochs: 10, loss: 0.234, val_loss: 0.289, learning_rate: 0.00002, documents_processed: 423, total_documents: 632, started_at: new Date(Date.now() - 3600000).toISOString(), estimated_completion: new Date(Date.now() + 1800000).toISOString(), error_message: null, metrics: { precision: 0.89, recall: 0.85, f1_score: 0.87, accuracy: 0.91, loss_history: [0.8, 0.6, 0.45, 0.35, 0.28, 0.25, 0.234], val_loss_history: [0.85, 0.65, 0.5, 0.4, 0.32, 0.3, 0.289], }, config: { batch_size: 16, learning_rate: 0.00005, epochs: 10, warmup_steps: 500, weight_decay: 0.01, gradient_accumulation: 4, mixed_precision: true, bundeslaender: ['ni', 'by', 'nw', 'he', 'bw'], }, }, ] const MOCK_STATS: DatasetStats = { total_documents: 632, total_chunks: 8547, training_allowed: 489, by_bundesland: { ni: 87, by: 92, nw: 78, he: 65, bw: 71, rp: 43, sn: 38, sh: 34, th: 29, }, by_doc_type: { verordnung: 312, schulordnung: 156, handreichung: 98, erlass: 66, }, } // ============================================================================ // API FUNCTIONS // ============================================================================ async function fetchJobs(): Promise { try { const response = await fetch('/api/admin/training?action=jobs') if (!response.ok) throw new Error('Failed to fetch jobs') return await response.json() } catch (error) { console.error('Error fetching jobs:', error) return MOCK_JOBS // Fallback to mock data } } async function fetchDatasetStats(): Promise { try { const response = await fetch('/api/admin/training?action=dataset-stats') if (!response.ok) throw new Error('Failed to fetch stats') return await response.json() } catch (error) { console.error('Error fetching stats:', error) return MOCK_STATS // Fallback to mock data } } async function createTrainingJob(config: Partial): Promise<{id: string, status: string}> { const response = await fetch('/api/admin/training?action=create-job', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: `Zeugnis-RAG ${new Date().toLocaleDateString('de-DE')}`, model_type: 'zeugnis', bundeslaender: config.bundeslaender || [], batch_size: config.batch_size || 16, learning_rate: config.learning_rate || 0.00005, epochs: config.epochs || 10, warmup_steps: config.warmup_steps || 500, weight_decay: config.weight_decay || 0.01, gradient_accumulation: config.gradient_accumulation || 4, mixed_precision: config.mixed_precision ?? true, }), }) if (!response.ok) { const error = await response.json() throw new Error(error.detail || 'Failed to create job') } return await response.json() } async function pauseJob(jobId: string): Promise { const response = await fetch(`/api/admin/training?action=pause&job_id=${jobId}`, { method: 'POST', }) if (!response.ok) throw new Error('Failed to pause job') } async function resumeJob(jobId: string): Promise { const response = await fetch(`/api/admin/training?action=resume&job_id=${jobId}`, { method: 'POST', }) if (!response.ok) throw new Error('Failed to resume job') } async function cancelJob(jobId: string): Promise { const response = await fetch(`/api/admin/training?action=cancel&job_id=${jobId}`, { method: 'POST', }) if (!response.ok) throw new Error('Failed to cancel job') } // ============================================================================ // COMPONENTS // ============================================================================ // Progress Ring Component function ProgressRing({ progress, size = 120, strokeWidth = 8, color = '#10B981' }: { progress: number size?: number strokeWidth?: number color?: string }) { const radius = (size - strokeWidth) / 2 const circumference = radius * 2 * Math.PI const offset = circumference - (progress / 100) * circumference return (
{Math.round(progress)}%
) } // Mini Line Chart Component function MiniChart({ data, color = '#10B981', height = 60 }: { data: number[] color?: string height?: number }) { if (!data.length) return null const max = Math.max(...data) const min = Math.min(...data) const range = max - min || 1 const width = 200 const padding = 4 const points = data.map((value, i) => { const x = padding + (i / (data.length - 1)) * (width - 2 * padding) const y = padding + (1 - (value - min) / range) * (height - 2 * padding) return `${x},${y}` }).join(' ') return ( {/* Latest point */} {data.length > 0 && ( )} ) } // Status Badge function StatusBadge({ status }: { status: TrainingJob['status'] }) { const styles = { queued: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', preparing: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', training: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', validating: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200', completed: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', failed: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', paused: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200', } const labels = { queued: 'In Warteschlange', preparing: 'Vorbereitung', training: 'Training läuft', validating: 'Validierung', completed: 'Abgeschlossen', failed: 'Fehlgeschlagen', paused: 'Pausiert', } return ( {status === 'training' && ( )} {labels[status]} ) } // Metric Card function MetricCard({ label, value, unit, trend, color }: { label: string value: number | string unit?: string trend?: 'up' | 'down' | 'neutral' color?: string }) { return (

{label}

{typeof value === 'number' ? value.toFixed(3) : value} {unit && {unit}} {trend && ( {trend === 'up' ? '↑' : trend === 'down' ? '↓' : '→'} )}
) } // Training Job Card function TrainingJobCard({ job, onPause, onResume, onStop, onViewDetails }: { job: TrainingJob onPause: () => void onResume: () => void onStop: () => void onViewDetails: () => void }) { const isActive = ['training', 'preparing', 'validating'].includes(job.status) return (
{/* Header */}

{job.name}

Modell: {job.model_type.charAt(0).toUpperCase() + job.model_type.slice(1)}

{/* Progress Section */}
{/* Epoch Progress */}
Epoche {job.current_epoch} / {job.total_epochs}
{/* Documents Progress */}
Dokumente {job.documents_processed.toLocaleString()} / {job.total_documents.toLocaleString()}
{/* Metrics */}
{/* Loss Chart */}
Loss-Verlauf
Training Validation
{/* Time Info */}
Gestartet: {job.started_at ? new Date(job.started_at).toLocaleTimeString('de-DE') : '-'} Geschätzte Fertigstellung: {job.estimated_completion ? new Date(job.estimated_completion).toLocaleTimeString('de-DE') : '-' }
{/* Actions */}
{isActive && ( <> )}
) } // Dataset Overview Component function DatasetOverview({ stats }: { stats: DatasetStats }) { const maxBundesland = Math.max(...Object.values(stats.by_bundesland)) return (

Datensatz-Übersicht

{/* Stats Grid */}

{stats.total_documents.toLocaleString()}

Dokumente

{stats.total_chunks.toLocaleString()}

Chunks

{stats.training_allowed.toLocaleString()}

Training erlaubt

{/* Bundesland Distribution */}

Verteilung nach Bundesland

{Object.entries(stats.by_bundesland) .sort((a, b) => b[1] - a[1]) .map(([code, count]) => (
{code}
{count}
))}
) } // New Training Modal function NewTrainingModal({ isOpen, onClose, onSubmit }: { isOpen: boolean onClose: () => void onSubmit: (config: Partial) => void }) { const [step, setStep] = useState(1) const [config, setConfig] = useState>({ batch_size: 16, learning_rate: 0.00005, epochs: 10, warmup_steps: 500, weight_decay: 0.01, gradient_accumulation: 4, mixed_precision: true, bundeslaender: [], }) if (!isOpen) return null const bundeslaender = [ { code: 'ni', name: 'Niedersachsen', allowed: true }, { code: 'by', name: 'Bayern', allowed: true }, { code: 'nw', name: 'NRW', allowed: true }, { code: 'he', name: 'Hessen', allowed: true }, { code: 'bw', name: 'Baden-Württemberg', allowed: true }, { code: 'rp', name: 'Rheinland-Pfalz', allowed: true }, { code: 'sn', name: 'Sachsen', allowed: true }, { code: 'sh', name: 'Schleswig-Holstein', allowed: true }, { code: 'th', name: 'Thüringen', allowed: true }, { code: 'be', name: 'Berlin', allowed: false }, { code: 'bb', name: 'Brandenburg', allowed: false }, { code: 'hb', name: 'Bremen', allowed: false }, { code: 'hh', name: 'Hamburg', allowed: false }, { code: 'mv', name: 'Mecklenburg-Vorpommern', allowed: false }, { code: 'sl', name: 'Saarland', allowed: false }, { code: 'st', name: 'Sachsen-Anhalt', allowed: false }, ] return (
{/* Header */}

Neues Training starten

Schritt {step} von 3

{/* Progress Steps */}
{[1, 2, 3].map((s) => (
{s < step ? '✓' : s}
{s < 3 && (
)}
))}
Daten Parameter Bestätigen
{/* Content */}
{step === 1 && (

Wählen Sie die Bundesländer für das Training

Nur Bundesländer mit Training-Erlaubnis können ausgewählt werden.

{bundeslaender.map((bl) => ( ))}
)} {step === 2 && (

Training-Parameter konfigurieren

setConfig({ ...config, batch_size: parseInt(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700" />
setConfig({ ...config, learning_rate: parseFloat(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700" />
setConfig({ ...config, epochs: parseInt(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700" />
setConfig({ ...config, warmup_steps: parseInt(e.target.value) })} className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700" />
setConfig({ ...config, mixed_precision: e.target.checked })} className="w-4 h-4 text-blue-600 rounded" />
)} {step === 3 && (

Training-Konfiguration bestätigen

Bundesländer {config.bundeslaender?.length || 0} ausgewählt
Epochen {config.epochs}
Batch Size {config.batch_size}
Learning Rate {config.learning_rate}
Mixed Precision {config.mixed_precision ? 'Aktiviert' : 'Deaktiviert'}

Hinweis: Das Training kann je nach Datenmenge und Konfiguration mehrere Stunden dauern. Sie können den Fortschritt jederzeit überwachen.

)}
{/* Footer */}
) } // ============================================================================ // MAIN PAGE // ============================================================================ export default function TrainingDashboardPage() { const [jobs, setJobs] = useState([]) const [stats, setStats] = useState(MOCK_STATS) const [showNewTrainingModal, setShowNewTrainingModal] = useState(false) const [selectedJob, setSelectedJob] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) // Load initial data useEffect(() => { async function loadData() { setIsLoading(true) try { const [jobsData, statsData] = await Promise.all([ fetchJobs(), fetchDatasetStats(), ]) setJobs(jobsData) setStats(statsData) setError(null) } catch (err) { console.error('Failed to load data:', err) setError('Verbindung zum Backend fehlgeschlagen') // Use mock data as fallback setJobs(MOCK_JOBS) setStats(MOCK_STATS) } finally { setIsLoading(false) } } loadData() }, []) // Real-time updates for active training jobs useEffect(() => { const hasActiveJob = jobs.some(j => j.status === 'training' || j.status === 'preparing') if (!hasActiveJob) return const interval = setInterval(async () => { try { const updatedJobs = await fetchJobs() setJobs(updatedJobs) } catch (err) { console.error('Failed to refresh jobs:', err) } }, 2000) return () => clearInterval(interval) }, [jobs]) const handleStartTraining = async (config: Partial) => { try { const result = await createTrainingJob(config) // Refresh jobs list const updatedJobs = await fetchJobs() setJobs(updatedJobs) setShowNewTrainingModal(false) } catch (err) { console.error('Failed to start training:', err) setError(err instanceof Error ? err.message : 'Training konnte nicht gestartet werden') } } const handlePauseJob = async (jobId: string) => { try { await pauseJob(jobId) const updatedJobs = await fetchJobs() setJobs(updatedJobs) } catch (err) { console.error('Failed to pause job:', err) } } const handleResumeJob = async (jobId: string) => { try { await resumeJob(jobId) const updatedJobs = await fetchJobs() setJobs(updatedJobs) } catch (err) { console.error('Failed to resume job:', err) } } const handleCancelJob = async (jobId: string) => { try { await cancelJob(jobId) const updatedJobs = await fetchJobs() setJobs(updatedJobs) } catch (err) { console.error('Failed to cancel job:', err) } } return (
{/* Header */}

Training Dashboard

Überwachen und steuern Sie KI-Trainingsprozesse für Zeugnisse

{/* Error Banner */} {error && (
{error}
)} {/* Loading State */} {isLoading ? (

Lade Trainingsdaten...

) : (
{/* Training Jobs */}
{jobs.length === 0 ? (

Kein aktives Training

Starten Sie ein neues Training, um das Zeugnis-Modell zu verbessern.

) : ( jobs.map(job => ( handlePauseJob(job.id)} onResume={() => handleResumeJob(job.id)} onStop={() => handleCancelJob(job.id)} onViewDetails={() => setSelectedJob(job)} /> )) )}
{/* Sidebar */}
{/* Quick Actions */}

Schnellaktionen

)}
{/* New Training Modal */} setShowNewTrainingModal(false)} onSubmit={handleStartTraining} />
) }