feat(training): add intro banner and parameter tooltips to ML Training page
This commit is contained in:
1 parent
4e90337f54
commit
e6f18d4a61
3 files changed
+297
-13
No files matched your search
+179
@@ -358,3 +358,182 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
Intro / Getting Started Banner
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.intro-banner {
|
||||
background: color-mix(in srgb, var(--accent) 8%, var(--surface-1));
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--sp-4) var(--sp-5);
|
||||
margin-bottom: var(--sp-5);
|
||||
}
|
||||
|
||||
.intro-banner.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.intro-banner-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-2);
|
||||
margin-bottom: var(--sp-3);
|
||||
font-size: 15px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.intro-banner-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.intro-banner-header strong {
|
||||
flex: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.intro-banner-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
line-height: 1;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.intro-banner-close:hover {
|
||||
color: var(--text);
|
||||
background: var(--surface-3);
|
||||
}
|
||||
|
||||
.intro-banner-lead {
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 var(--sp-3) 0;
|
||||
}
|
||||
|
||||
.intro-banner-steps {
|
||||
margin: 0 0 var(--sp-4) 0;
|
||||
padding-left: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-2);
|
||||
}
|
||||
|
||||
.intro-banner-steps li {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.intro-banner-steps li strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.intro-banner-steps li em {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.intro-banner-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: var(--sp-3);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
Tooltip System (.tip-icon + data-tip)
|
||||
══════════════════════════════════════════════════════════════ */
|
||||
|
||||
.tip-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface-3);
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
flex-shrink: 0;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
/* prevent tooltip from triggering form-label hover states */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tip-icon:hover {
|
||||
background: var(--accent-dim);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* The tooltip bubble itself */
|
||||
.tip-icon::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--surface-0, #1a1a2e);
|
||||
color: var(--text, #e0e0e0);
|
||||
border: 1px solid var(--border, #333);
|
||||
border-radius: var(--radius-md, 6px);
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
white-space: normal;
|
||||
width: 240px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 1000;
|
||||
text-align: left;
|
||||
/* reset inherited label styles */
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
/* Arrow pointing down */
|
||||
.tip-icon::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: calc(100% + 3px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 5px solid transparent;
|
||||
border-top-color: var(--border, #333);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.tip-icon:hover::after,
|
||||
.tip-icon:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Flip tooltip below when near the top of the viewport */
|
||||
.tip-icon.tip-below::after {
|
||||
bottom: auto;
|
||||
top: calc(100% + 8px);
|
||||
}
|
||||
|
||||
.tip-icon.tip-below::before {
|
||||
bottom: auto;
|
||||
top: calc(100% + 3px);
|
||||
border-top-color: transparent;
|
||||
border-bottom-color: var(--border, #333);
|
||||
}
|
||||
+71
-13
@@ -18,9 +18,35 @@
|
||||
<h1>ML Training Dashboard</h1>
|
||||
<p>Train, evaluate, and deploy the AI opponent policy network</p>
|
||||
</div>
|
||||
<button id="btn-refresh-data" class="btn btn-secondary" onclick="refreshAll()" title="Refresh training data status">
|
||||
<span class="btn-icon" id="refresh-icon">↻</span> Refresh
|
||||
</button>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<button class="btn btn-ghost btn-sm" onclick="showIntroBanner()" title="Show getting started guide">❓ Help</button>
|
||||
<button id="btn-refresh-data" class="btn btn-secondary" onclick="refreshAll()" title="Refresh training data status">
|
||||
<span class="btn-icon" id="refresh-icon">↻</span> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Intro / Getting Started Banner -->
|
||||
<div id="training-intro-banner" class="intro-banner hidden">
|
||||
<div class="intro-banner-header">
|
||||
<span class="intro-banner-icon">🎓</span>
|
||||
<strong>Getting Started with ML Training</strong>
|
||||
<button class="intro-banner-close" onclick="dismissIntroBanner()" title="Dismiss">✕</button>
|
||||
</div>
|
||||
<p class="intro-banner-lead">This page trains a neural network to play Commander decisions. Follow these steps in order on your first run:</p>
|
||||
<ol class="intro-banner-steps">
|
||||
<li><strong>Run batch simulations</strong> with ML logging enabled (from the Simulator page) to generate decision log files.</li>
|
||||
<li><strong>Start Training</strong> below — leave “Rebuild dataset” checked so it parses your logs into a dataset automatically.</li>
|
||||
<li>Check <strong>Evaluation Results</strong> after training. Aim for >60% test accuracy before moving on.</li>
|
||||
<li><em>(Optional)</em> Run <strong>PPO reinforcement learning</strong> to fine-tune the policy via self-play. Only recommended after a solid supervised baseline.</li>
|
||||
<li>Run a <strong>Tournament</strong> to benchmark your model against heuristic and random agents and get a win-rate score.</li>
|
||||
</ol>
|
||||
<div class="intro-banner-footer">
|
||||
<label class="form-checkbox" style="font-size:12px;">
|
||||
<input type="checkbox" id="intro-dont-show" /> Don’t show this again
|
||||
</label>
|
||||
<button class="btn btn-primary btn-sm" onclick="dismissIntroBanner()">Got it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Overview Cards -->
|
||||
@@ -59,19 +85,31 @@
|
||||
<div class="train-controls">
|
||||
<div class="param-grid form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="param-epochs">Epochs</label>
|
||||
<label class="form-label" for="param-epochs">
|
||||
Epochs
|
||||
<span class="tip-icon" data-tip="Number of full passes over the training dataset. More epochs improve fit but risk overfitting. 50 is a safe starting point.">?</span>
|
||||
</label>
|
||||
<input type="number" id="param-epochs" class="form-input" value="50" min="1" max="500" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="param-lr">Learning Rate</label>
|
||||
<label class="form-label" for="param-lr">
|
||||
Learning Rate
|
||||
<span class="tip-icon" data-tip="Controls how large each weight update is. Too high causes instability; too low is very slow. 0.001 is a reliable default.">?</span>
|
||||
</label>
|
||||
<input type="number" id="param-lr" class="form-input" value="0.001" step="0.0001" min="0.00001" max="0.1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="param-batch">Batch Size</label>
|
||||
<label class="form-label" for="param-batch">
|
||||
Batch Size
|
||||
<span class="tip-icon" data-tip="Samples processed per gradient update. Larger batches are faster on GPU but use more VRAM. 256 is recommended for most setups.">?</span>
|
||||
</label>
|
||||
<input type="number" id="param-batch" class="form-input" value="256" min="16" max="2048" step="16" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="param-patience">Early Stop Patience</label>
|
||||
<label class="form-label" for="param-patience">
|
||||
Early Stop Patience
|
||||
<span class="tip-icon" data-tip="Stops training if validation loss doesn't improve for this many epochs in a row. Prevents wasted compute. 10 is a sensible default.">?</span>
|
||||
</label>
|
||||
<input type="number" id="param-patience" class="form-input" value="10" min="3" max="100" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,6 +117,7 @@
|
||||
<label class="form-checkbox checkbox-label">
|
||||
<input type="checkbox" id="param-rebuild" checked />
|
||||
Rebuild dataset from decision logs
|
||||
<span class="tip-icon" data-tip="Re-parses all .jsonl decision log files into a fresh dataset. Uncheck to reuse the last built dataset and save time on re-runs.">?</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="train-actions">
|
||||
@@ -132,19 +171,31 @@
|
||||
<div class="train-controls">
|
||||
<div class="param-grid form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="ppo-iterations">Iterations</label>
|
||||
<label class="form-label" for="ppo-iterations">
|
||||
Iterations
|
||||
<span class="tip-icon" data-tip="Each iteration runs N episodes, computes returns, and updates the policy. 100 iterations is a typical starting point.">?</span>
|
||||
</label>
|
||||
<input type="number" id="ppo-iterations" class="form-input" value="100" min="1" max="1000" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="ppo-episodes">Episodes/Iter</label>
|
||||
<label class="form-label" for="ppo-episodes">
|
||||
Episodes/Iter
|
||||
<span class="tip-icon" data-tip="Number of full game simulations per PPO update step. More episodes produce more stable gradients but slow down each iteration.">?</span>
|
||||
</label>
|
||||
<input type="number" id="ppo-episodes" class="form-input" value="64" min="8" max="512" step="8" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="ppo-lr">Learning Rate</label>
|
||||
<label class="form-label" for="ppo-lr">
|
||||
Learning Rate
|
||||
<span class="tip-icon" data-tip="PPO learning rate. Lower than supervised training is usually better — 0.0003 is a standard RL starting value.">?</span>
|
||||
</label>
|
||||
<input type="number" id="ppo-lr" class="form-input" value="0.0003" step="0.0001" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="ppo-opponent">Opponent</label>
|
||||
<label class="form-label" for="ppo-opponent">
|
||||
Opponent
|
||||
<span class="tip-icon" data-tip="Who the agent plays against during training. Heuristic is the best first opponent. Self-Play is more advanced and can be unstable early on.">?</span>
|
||||
</label>
|
||||
<select id="ppo-opponent" class="form-select">
|
||||
<option value="heuristic">Heuristic</option>
|
||||
<option value="random">Random</option>
|
||||
@@ -156,6 +207,7 @@
|
||||
<label class="form-checkbox checkbox-label">
|
||||
<input type="checkbox" id="ppo-load-supervised" />
|
||||
Initialize from supervised checkpoint (warm start)
|
||||
<span class="tip-icon" data-tip="Strongly recommended. Starts PPO from your best supervised model instead of random weights. PPO from scratch is very slow and unstable.">?</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="train-actions">
|
||||
@@ -184,11 +236,17 @@
|
||||
<div class="train-controls">
|
||||
<div class="param-grid form-row tourney-param-grid">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tourney-episodes">Games per Matchup</label>
|
||||
<label class="form-label" for="tourney-episodes">
|
||||
Games per Matchup
|
||||
<span class="tip-icon" data-tip="Number of games played per policy pair. Use at least 50–100 games for a statistically reliable win rate.">?</span>
|
||||
</label>
|
||||
<input type="number" id="tourney-episodes" class="form-input" value="50" min="10" max="500" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="tourney-style">Playstyle</label>
|
||||
<label class="form-label" for="tourney-style">
|
||||
Playstyle
|
||||
<span class="tip-icon" data-tip="The deck archetype used during tournament games. Try Midrange first — it's the most balanced and representative of typical Commander gameplay.">?</span>
|
||||
</label>
|
||||
<select id="tourney-style" class="form-select">
|
||||
<option value="midrange">Midrange</option>
|
||||
<option value="aggro">Aggro</option>
|
||||
|
||||
@@ -19,6 +19,8 @@ let isTraining = false;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
refreshDataStatus();
|
||||
refreshTrainingStatus();
|
||||
initIntroBanner();
|
||||
initTooltipFlip();
|
||||
});
|
||||
|
||||
async function refreshAll() {
|
||||
@@ -36,6 +38,51 @@ async function refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Intro Banner
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
const INTRO_STORAGE_KEY = 'trainingIntroSeen';
|
||||
|
||||
function initIntroBanner() {
|
||||
const seen = localStorage.getItem(INTRO_STORAGE_KEY);
|
||||
if (!seen) {
|
||||
showIntroBanner();
|
||||
}
|
||||
}
|
||||
|
||||
function showIntroBanner() {
|
||||
const banner = document.getElementById('training-intro-banner');
|
||||
if (banner) banner.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function dismissIntroBanner() {
|
||||
const banner = document.getElementById('training-intro-banner');
|
||||
const dontShow = document.getElementById('intro-dont-show');
|
||||
if (banner) banner.classList.add('hidden');
|
||||
if (dontShow && dontShow.checked) {
|
||||
localStorage.setItem(INTRO_STORAGE_KEY, '1');
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Tooltip flip — prevent overflow at top
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
function initTooltipFlip() {
|
||||
document.querySelectorAll('.tip-icon').forEach(el => {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
// If less than 120px above the element, flip tooltip below
|
||||
if (rect.top < 120) {
|
||||
el.classList.add('tip-below');
|
||||
} else {
|
||||
el.classList.remove('tip-below');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════
|
||||
// Data Status
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
Reference in new issue
Block a user