// Control panel: unified action buttons, status display export class Controls { constructor(container, callbacks) { this.callbacks = callbacks; this.el = container; this.build(); } build() { this.el.innerHTML = `
Examples: 0 Noise: 0.05
Joystick 0.50, 0.50 Output Mean 0.000 Output Spread 0.000 Best Loss - Train Iters 0 Gamepad Disconnected Follow Off
`; // Action buttons this.el.querySelector('#btn-thumbsup').addEventListener('click', () => this.callbacks.onThumbsUp?.()); this.el.querySelector('#btn-thumbsdown').addEventListener('click', () => this.callbacks.onThumbsDown?.()); this.el.querySelector('#btn-add').addEventListener('click', () => this.callbacks.onAddExample?.()); this.el.querySelector('#btn-train').addEventListener('click', () => this.callbacks.onTrain?.()); this.el.querySelector('#btn-randomize').addEventListener('click', () => this.callbacks.onRandomize?.()); this.el.querySelector('#btn-clear-examples').addEventListener('click', () => this.callbacks.onClearExamples?.()); this.el.querySelector('#btn-clear').addEventListener('click', () => this.callbacks.onClear?.()); } updateStatus(exampleCount, loss, noiseLevel) { this.el.querySelector('#status-examples').textContent = `Examples: ${exampleCount}`; if (loss !== null && loss !== undefined) { this.el.querySelector('#status-loss').textContent = `Loss: ${loss.toFixed(5)}`; } else { this.el.querySelector('#status-loss').textContent = 'Loss: -'; } if (noiseLevel !== undefined) { this.el.querySelector('#status-noise').textContent = `Noise: ${noiseLevel.toFixed(3)}`; } } updateMetrics(metrics = {}) { const set = (id, value) => { const el = this.el.querySelector(id); if (el) el.textContent = value; }; if (metrics.joystickX !== undefined && metrics.joystickY !== undefined) { set('#metric-joystick', `${metrics.joystickX.toFixed(2)}, ${metrics.joystickY.toFixed(2)}`); } if (metrics.outputMean !== undefined) set('#metric-mean', metrics.outputMean.toFixed(3)); if (metrics.outputSpread !== undefined) set('#metric-spread', metrics.outputSpread.toFixed(3)); if (metrics.bestLoss !== undefined && metrics.bestLoss !== null) { set('#metric-best-loss', metrics.bestLoss.toFixed(5)); } if (metrics.totalTrainingIterations !== undefined) { set('#metric-train-iters', String(metrics.totalTrainingIterations)); } if (metrics.gamepadConnected !== undefined) { set('#metric-gamepad', metrics.gamepadConnected ? 'Connected' : 'Disconnected'); } if (metrics.followMode !== undefined) { set('#metric-follow', metrics.followMode ? 'On' : 'Off'); } } updateLossPlot(lossHistory = []) { const canvas = this.el.querySelector('#loss-plot'); if (!canvas) return; const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; ctx.clearRect(0, 0, width, height); ctx.fillStyle = '#121212'; ctx.fillRect(0, 0, width, height); ctx.strokeStyle = '#2b2b2b'; ctx.strokeRect(0.5, 0.5, width - 1, height - 1); if (!lossHistory.length) { ctx.fillStyle = '#666'; ctx.font = '11px monospace'; ctx.fillText('Loss history appears after training.', 10, 18); return; } const points = lossHistory.slice(-420); let min = Infinity; let max = -Infinity; for (const v of points) { if (v < min) min = v; if (v > max) max = v; } const range = Math.max(max - min, 1e-9); const leftPad = 6; const rightPad = 6; const topPad = 6; const bottomPad = 14; const plotW = width - leftPad - rightPad; const plotH = height - topPad - bottomPad; ctx.strokeStyle = '#352418'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(leftPad, topPad + plotH / 2); ctx.lineTo(width - rightPad, topPad + plotH / 2); ctx.stroke(); ctx.strokeStyle = '#ff6a00'; ctx.lineWidth = 1.6; ctx.beginPath(); for (let i = 0; i < points.length; i++) { const x = leftPad + (i / Math.max(points.length - 1, 1)) * plotW; const yNorm = (points[i] - min) / range; const y = topPad + (1 - yNorm) * plotH; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); ctx.fillStyle = '#8a8a8a'; ctx.font = '10px monospace'; ctx.fillText(`min ${min.toFixed(5)}`, leftPad, height - 3); ctx.fillText(`max ${max.toFixed(5)}`, width - rightPad - 78, height - 3); } }