/** * Commander AI Lab — ML Training Dashboard * Manages training pipeline, data status, and evaluation display. */ const API_BASE = ''; // Same origin // ═══════════════════════════════════════════ // State // ═══════════════════════════════════════════ let pollingInterval = null; let isTraining = false; // ═══════════════════════════════════════════ // Init // ═══════════════════════════════════════════ document.addEventListener('DOMContentLoaded', () => { refreshDataStatus(); refreshTrainingStatus(); }); async function refreshAll() { const btn = document.getElementById('btn-refresh-data'); const icon = document.getElementById('refresh-icon'); btn.disabled = true; icon.style.display = 'inline-block'; icon.style.animation = 'spin 0.8s linear infinite'; try { await refreshDataStatus(); await refreshTrainingStatus(); } finally { btn.disabled = false; icon.style.animation = ''; } } // ═══════════════════════════════════════════ // Data Status // ═══════════════════════════════════════════ async function refreshDataStatus() { try { const res = await fetch(`${API_BASE}/api/ml/data/status`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); renderDataStatus(data); } catch (err) { console.error('Failed to fetch data status:', err); } } function renderDataStatus(data) { // Status cards const totalDecisions = data.totalDecisions || 0; document.getElementById('val-decisions').textContent = totalDecisions.toLocaleString(); document.getElementById('sub-decisions').textContent = data.decisionFiles.length > 0 ? `${data.decisionFiles.length} file${data.decisionFiles.length !== 1 ? 's' : ''}` : 'no decision logs'; const cardData = document.getElementById('card-data'); cardData.classList.toggle('active', totalDecisions > 0); // Dataset card const trainData = data.datasets.train; if (trainData) { document.getElementById('val-dataset').textContent = trainData.samples.toLocaleString(); document.getElementById('sub-dataset').textContent = `${trainData.features} features`; document.getElementById('card-dataset').classList.add('active'); } else { document.getElementById('val-dataset').textContent = '--'; document.getElementById('sub-dataset').textContent = 'not built'; } // Model card if (data.checkpoints.length > 0) { const latest = data.checkpoints[data.checkpoints.length - 1]; const sizeKB = (latest.size / 1024).toFixed(0); document.getElementById('val-model').textContent = `${sizeKB} KB`; document.getElementById('sub-model').textContent = latest.name; document.getElementById('card-model').classList.add('active'); } else { document.getElementById('val-model').textContent = '--'; document.getElementById('sub-model').textContent = 'no checkpoint'; } // Server card if (data.policyLoaded) { document.getElementById('val-server').textContent = 'Online'; document.getElementById('sub-server').textContent = 'ready for inference'; document.getElementById('card-server').classList.add('success'); } else { document.getElementById('val-server').textContent = 'Offline'; document.getElementById('sub-server').textContent = 'not loaded'; document.getElementById('card-server').classList.remove('success'); } // Data files list renderDataFiles(data); // Eval results if (data.evalResults) { renderEvaluation(data.evalResults); } // Checkpoints renderCheckpoints(data.checkpoints); } function renderDataFiles(data) { const container = document.getElementById('data-files-list'); if (data.decisionFiles.length === 0 && Object.keys(data.datasets).length === 0) { container.innerHTML = '
No training data found. Run batch simulations with ML logging enabled to generate decision data.
'; return; } let html = ''; // Decision log files for (const f of data.decisionFiles) { const sizeKB = (f.size / 1024).toFixed(1); html += `
${f.name} ${f.decisions.toLocaleString()} decisions ${sizeKB} KB
`; } // Dataset splits for (const [split, info] of Object.entries(data.datasets)) { const badgeClass = split === 'train' ? 'badge-train' : split === 'val' ? 'badge-val' : 'badge-test'; const sizeKB = (info.size / 1024).toFixed(1); html += `
${split}.npz ${split} ${(info.samples || 0).toLocaleString()} samples ${sizeKB} KB
`; } container.innerHTML = html; } function renderCheckpoints(checkpoints) { const container = document.getElementById('checkpoint-list'); if (!checkpoints || checkpoints.length === 0) { container.innerHTML = '
No checkpoints found. Train a model to generate checkpoints.
'; return; } let html = ''; for (const ckpt of checkpoints.reverse()) { const sizeKB = (ckpt.size / 1024).toFixed(0); const date = new Date(ckpt.modified); const timeStr = date.toLocaleString(); html += `
${ckpt.name} ${sizeKB} KB ${timeStr}
`; } container.innerHTML = html; } // ═══════════════════════════════════════════ // Evaluation Display // ═══════════════════════════════════════════ function renderEvaluation(results) { const container = document.getElementById('eval-results'); if (!results) { container.innerHTML = '
No evaluation results yet. Train a model first.
'; return; } const accuracy = results.accuracy || 0; const accClass = accuracy >= 0.5 ? 'good' : accuracy >= 0.3 ? 'ok' : 'bad'; const samples = results.total_samples || 0; const topK = results.top_3_accuracy || 0; let html = `
Test Accuracy
${(accuracy * 100).toFixed(1)}%
Top-3 Accuracy
${(topK * 100).toFixed(1)}%
Test Samples
${samples.toLocaleString()}
`; // Per-class accuracy if (results.per_class_accuracy) { html += '

Per-Action Accuracy

'; html += ''; const actions = results.per_class_accuracy; for (const [action, acc] of Object.entries(actions)) { const pct = (acc * 100).toFixed(1); const barColor = acc >= 0.5 ? 'var(--lab-success)' : acc >= 0.3 ? 'var(--lab-warning)' : 'var(--lab-error)'; const support = results.per_class_support ? (results.per_class_support[action] || '--') : '--'; html += ` `; } html += '
ActionAccuracySupport
${action} ${pct}%
${support}
'; } // Confusion matrix if (results.confusion_matrix && results.class_names) { html += '

Confusion Matrix

'; html += '
'; html += ''; for (const name of results.class_names) { const short = name.replace('cast_', '').replace('activate_', 'act_').substring(0, 8); html += ``; } html += ''; const matrix = results.confusion_matrix; for (let i = 0; i < matrix.length; i++) { const shortName = results.class_names[i].replace('cast_', '').replace('activate_', 'act_').substring(0, 8); html += ``; const rowMax = Math.max(...matrix[i]); for (let j = 0; j < matrix[i].length; j++) { const val = matrix[i][j]; const intensity = rowMax > 0 ? val / rowMax : 0; let bg = 'transparent'; if (i === j) { bg = `rgba(63, 185, 80, ${intensity * 0.4})`; } else if (val > 0) { bg = `rgba(248, 81, 73, ${intensity * 0.3})`; } html += ``; } html += ''; } html += '
${short}
${shortName}${val}
'; } container.innerHTML = html; } // ═══════════════════════════════════════════ // Training Control // ═══════════════════════════════════════════ async function startTraining() { const epochs = parseInt(document.getElementById('param-epochs').value) || 50; const lr = parseFloat(document.getElementById('param-lr').value) || 0.001; const batchSize = parseInt(document.getElementById('param-batch').value) || 256; const patience = parseInt(document.getElementById('param-patience').value) || 10; const rebuild = document.getElementById('param-rebuild').checked; const btn = document.getElementById('btn-train'); btn.disabled = true; btn.innerHTML = '⌛ Starting...'; try { const res = await fetch(`${API_BASE}/api/ml/train`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ epochs, lr, batchSize, patience, rebuildDataset: rebuild }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || `HTTP ${res.status}`); } isTraining = true; showProgress(); startPolling(); } catch (err) { alert('Failed to start training: ' + err.message); btn.disabled = false; btn.innerHTML = '▶ Start Training'; } } async function reloadModel() { try { const res = await fetch(`${API_BASE}/api/ml/reload`, { method: 'POST' }); const data = await res.json(); if (data.success) { alert('Policy model reloaded successfully.'); refreshDataStatus(); } else { alert('Model reload failed. Check if a checkpoint exists.'); } } catch (err) { alert('Reload failed: ' + err.message); } } // ═══════════════════════════════════════════ // Progress Polling // ═══════════════════════════════════════════ function showProgress() { document.getElementById('train-progress').classList.remove('hidden'); } function hideProgress() { // Keep visible so user can see final result } function startPolling() { if (pollingInterval) clearInterval(pollingInterval); pollingInterval = setInterval(pollTrainingStatus, 2000); } function stopPolling() { if (pollingInterval) { clearInterval(pollingInterval); pollingInterval = null; } } async function refreshTrainingStatus() { try { const res = await fetch(`${API_BASE}/api/ml/train/status`); const data = await res.json(); if (data.running) { isTraining = true; showProgress(); updateProgress(data); startPolling(); } else if (data.phase === 'done' || data.phase === 'error') { showProgress(); updateProgress(data); } } catch (err) { // Silently fail on initial check } } async function pollTrainingStatus() { try { const res = await fetch(`${API_BASE}/api/ml/train/status`); const data = await res.json(); updateProgress(data); if (!data.running) { stopPolling(); isTraining = false; const btn = document.getElementById('btn-train'); btn.disabled = false; btn.innerHTML = '▶ Start Training'; // Refresh all data refreshDataStatus(); } } catch (err) { console.error('Poll failed:', err); } } // ═══════════════════════════════════════════ // PPO Training // ═══════════════════════════════════════════ let ppoPollingInterval = null; async function startPPO() { const iterations = parseInt(document.getElementById('ppo-iterations').value) || 100; const episodesPerIter = parseInt(document.getElementById('ppo-episodes').value) || 64; const lr = parseFloat(document.getElementById('ppo-lr').value) || 3e-4; const opponent = document.getElementById('ppo-opponent').value; const loadSupervised = document.getElementById('ppo-load-supervised').checked ? 'ml/models/checkpoints/best_policy.pt' : ''; const btn = document.getElementById('btn-ppo'); btn.disabled = true; btn.innerHTML = '⌛ Starting...'; try { const res = await fetch(`${API_BASE}/api/ml/train/ppo`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ iterations, episodesPerIter, lr, opponent, loadSupervised }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || `HTTP ${res.status}`); } document.getElementById('ppo-progress').classList.remove('hidden'); startPPOPolling(); } catch (err) { alert('Failed to start PPO: ' + err.message); btn.disabled = false; btn.innerHTML = '▶ Start PPO Training'; } } function startPPOPolling() { if (ppoPollingInterval) clearInterval(ppoPollingInterval); ppoPollingInterval = setInterval(pollPPOStatus, 2000); } async function pollPPOStatus() { try { const res = await fetch(`${API_BASE}/api/ml/train/ppo/status`); const data = await res.json(); updatePPOProgress(data); if (!data.running && data.phase !== 'idle') { clearInterval(ppoPollingInterval); ppoPollingInterval = null; const btn = document.getElementById('btn-ppo'); btn.disabled = false; btn.innerHTML = '▶ Start PPO Training'; refreshDataStatus(); } } catch (err) { console.error('PPO poll error:', err); } } function updatePPOProgress(data) { const phase = data.phase || 'idle'; const phaseEl = document.getElementById('ppo-progress-phase'); phaseEl.textContent = phase.toUpperCase(); phaseEl.className = 'phase-badge ' + phase; document.getElementById('ppo-progress-message').textContent = data.message || ''; const bar = document.getElementById('ppo-progress-bar'); bar.className = 'progress-bar'; if (phase === 'done') { bar.style.width = '100%'; bar.classList.add('done'); } else if (phase === 'error') { bar.style.width = '100%'; bar.classList.add('error'); } else if (data.total_iterations > 0) { bar.style.width = Math.min(100, (data.iteration / data.total_iterations) * 100) + '%'; } const detail = document.getElementById('ppo-progress-detail'); if (data.metrics) { const m = data.metrics; detail.textContent = `Win Rate: ${((m.win_rate||0)*100).toFixed(0)}% | Policy Loss: ${(m.policy_loss||0).toFixed(4)} | Entropy: ${(m.entropy||0).toFixed(3)}`; } else if (phase === 'done' && data.result) { detail.textContent = `Best Win Rate: ${((data.result.best_win_rate||0)*100).toFixed(0)}% | ${data.result.total_steps} total steps`; } else if (phase === 'error') { detail.textContent = data.error || ''; detail.style.color = 'var(--lab-error)'; } else { detail.textContent = ''; } } // ═══════════════════════════════════════════ // Tournament // ═══════════════════════════════════════════ let tourneyPollingInterval = null; async function startTournament() { const episodes = parseInt(document.getElementById('tourney-episodes').value) || 50; const playstyle = document.getElementById('tourney-style').value; const btn = document.getElementById('btn-tourney'); btn.disabled = true; btn.innerHTML = '⌛ Running...'; try { const res = await fetch(`${API_BASE}/api/ml/tournament`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ episodes, playstyle }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.detail || `HTTP ${res.status}`); } document.getElementById('tourney-progress').classList.remove('hidden'); startTourneyPolling(); } catch (err) { alert('Failed to start tournament: ' + err.message); btn.disabled = false; btn.innerHTML = '🏆 Run Tournament'; } } function startTourneyPolling() { if (tourneyPollingInterval) clearInterval(tourneyPollingInterval); tourneyPollingInterval = setInterval(pollTourneyStatus, 2000); } async function pollTourneyStatus() { try { const res = await fetch(`${API_BASE}/api/ml/tournament/status`); const data = await res.json(); const phase = data.phase || 'idle'; const phaseEl = document.getElementById('tourney-progress-phase'); phaseEl.textContent = phase.toUpperCase(); phaseEl.className = 'phase-badge ' + phase; document.getElementById('tourney-progress-message').textContent = data.message || ''; const bar = document.getElementById('tourney-progress-bar'); bar.className = 'progress-bar'; if (phase === 'done') { bar.style.width = '100%'; bar.classList.add('done'); } else if (phase === 'error') { bar.style.width = '100%'; bar.classList.add('error'); } else if (phase === 'running') { bar.style.width = '50%'; } if (!data.running && data.phase !== 'idle') { clearInterval(tourneyPollingInterval); tourneyPollingInterval = null; const btn = document.getElementById('btn-tourney'); btn.disabled = false; btn.innerHTML = '🏆 Run Tournament'; if (data.result) { renderTournamentResults(data.result); } document.getElementById('tourney-progress').classList.add('hidden'); } } catch (err) { console.error('Tournament poll error:', err); } } function renderTournamentResults(data) { const container = document.getElementById('tourney-results'); if (!data || !data.win_rates) { container.innerHTML = '
No results available.
'; return; } // Sort by win rate const sorted = Object.entries(data.win_rates) .sort((a, b) => (b[1].win_rate || 0) - (a[1].win_rate || 0)); let html = '

Leaderboard

'; html += ''; sorted.forEach(([name, stats], i) => { const wr = ((stats.win_rate || 0) * 100).toFixed(1); const wrColor = stats.win_rate >= 0.6 ? 'var(--lab-success)' : stats.win_rate >= 0.4 ? 'var(--lab-warning)' : 'var(--lab-error)'; const medal = i === 0 ? '🥇' : i === 1 ? '🥈' : i === 2 ? '🥉' : (i+1); html += ``; }); html += '
RankPolicyWin RateWinsLossesDraws
${medal} ${name} ${wr}% ${stats.wins} ${stats.losses} ${stats.draws}
'; // Win matrix if (data.win_matrix && data.policies) { html += '

Head-to-Head Wins

'; html += '
'; data.policies.forEach(p => html += ``); html += ''; data.policies.forEach(row => { html += ``; data.policies.forEach(col => { if (row === col) { html += ''; } else { const val = data.win_matrix[row]?.[col] || 0; html += ``; } }); html += ''; }); html += '
${p}
${row}-${val}
'; } html += `
${data.total_matches} total matches in ${(data.total_time_s||0).toFixed(1)}s
`; container.innerHTML = html; } // Load tournament results on page load async function loadTournamentResults() { try { const res = await fetch(`${API_BASE}/api/ml/tournament/results`); const data = await res.json(); if (!data.error) { renderTournamentResults(data); } } catch (err) { /* ignore */ } } // Also check PPO status on load async function checkPPOStatus() { try { const res = await fetch(`${API_BASE}/api/ml/train/ppo/status`); const data = await res.json(); if (data.running) { document.getElementById('ppo-progress').classList.remove('hidden'); updatePPOProgress(data); startPPOPolling(); } else if (data.phase === 'done' || data.phase === 'error') { document.getElementById('ppo-progress').classList.remove('hidden'); updatePPOProgress(data); } } catch (err) { /* ignore */ } } // Add to init const _origDOMLoaded = document.addEventListener; document.addEventListener('DOMContentLoaded', () => { // These are called after the original DOMContentLoaded handler setTimeout(() => { checkPPOStatus(); loadTournamentResults(); }, 500); }); function updateProgress(data) { const phaseBadge = document.getElementById('progress-phase'); const message = document.getElementById('progress-message'); const bar = document.getElementById('progress-bar'); const detail = document.getElementById('progress-detail'); // Phase badge const phase = data.phase || 'idle'; phaseBadge.textContent = phase.toUpperCase(); phaseBadge.className = 'phase-badge ' + phase; // Message message.textContent = data.message || ''; // Progress bar bar.className = 'progress-bar'; if (phase === 'done') { bar.style.width = '100%'; bar.classList.add('done'); } else if (phase === 'error') { bar.style.width = '100%'; bar.classList.add('error'); } else if (phase === 'training' && data.total_epochs > 0) { const pct = Math.min(100, (data.current_epoch / data.total_epochs) * 100); bar.style.width = pct + '%'; } else if (phase === 'building') { bar.style.width = '15%'; } else if (phase === 'evaluating') { bar.style.width = '90%'; } else if (phase === 'starting') { bar.style.width = '5%'; } // Detail if (phase === 'training' && data.total_epochs > 0) { detail.textContent = `Epoch ${data.current_epoch} / ${data.total_epochs}`; } else if (phase === 'done' && data.result) { const r = data.result; const valAcc = r.training?.best_val_accuracy; const testAcc = r.evaluation?.accuracy; let parts = []; if (valAcc !== undefined) parts.push(`Val: ${(valAcc * 100).toFixed(1)}%`); if (testAcc !== undefined) parts.push(`Test: ${(testAcc * 100).toFixed(1)}%`); if (r.device) parts.push(`Device: ${r.device}`); detail.textContent = parts.join(' | '); } else if (phase === 'error') { detail.textContent = data.error || 'Unknown error'; detail.style.color = 'var(--lab-error)'; } else { detail.textContent = ''; detail.style.color = ''; } // Update train button const btn = document.getElementById('btn-train'); if (data.running) { btn.disabled = true; btn.innerHTML = '⌛ Training...'; } }