ai/neural/controllers/nn-playground-controller.js

/**
 * @fileoverview NNPlaygroundController — Orchestrierung des NN-TTT-Playgrounds.
 *
 * Phase 2: DRY Game-Engine, SVG-verschmolzene Boards, Trainer-Tab.
 *   - Tab-Switching in der Sidebar (5 Tabs)
 *   - Topologie-Preset-Auswahl
 *   - Board-Interaktion via TTTRegularBoard (DRY Game-Engine)
 *   - Lokaler Forward-Pass + Training
 *   - Gewichts-Filter-Slider
 *   - Trainer-Tab: Trainings-Beispiele visualisieren
 *   - Metriken: Loss/Accuracy-History für Charts
 *   - Receptive-Field-Hover via NNMLPVisualizer
 *
 * Convention: Adapter-Layer. Verbindet UI-Elemente mit Core (NeuralNetwork)
 *             und Rendering (NNMLPVisualizer). Keine DOM-Erzeugung (außer
 *             dynamische Updates). Kein direktes console.log.
 *
 * @author Alexander Wolf
 * @see ToDos/NN_PLAYGROUND_PLAN_v2.md
 */

/* global NeuralNetwork, NNMLPVisualizer, NNTrainingCharts,
          TTTRegularBoard, CELL_EMPTY, PLAYER1, PLAYER2, NONE, DRAW,
          generateTTTTrainingData, shuffleArray */

/**
 * Topology Presets for didactic purposes.
 * @type {Object<string, Object>}
 */
const NN_PRESETS = {
    trivial: {
        label: '🚫 Sinnlos (kein Hidden Layer)',
        topology: [9, 9],
        activations: ['softmax'],
        description: 'Direkte lineare Abbildung ohne Hidden Layer – kann TTT nicht lernen.',
        learningRate: 0.05,
        epochs: 5000
    },
    minimal: {
        label: '🤏 Minimal (9 Hidden)',
        topology: [9, 9, 9],
        activations: ['relu', 'softmax'],
        description: '9 Hidden-Neuronen = Feldanzahl. Minimale Kapazität – reicht das?',
        learningRate: 0.01,
        epochs: 10000
    },
    sweet_spot: {
        label: '✅ Der Sweetspot (36 Hidden)',
        topology: [9, 36, 9],
        activations: ['relu', 'softmax'],
        description: '36 Neuronen = 4× Feldanzahl. Genug Kapazität für Muster-Erkennung.',
        learningRate: 0.01,
        epochs: 20000
    },
    deep: {
        label: '🏗️ Deep (3×9 Hidden)',
        topology: [9, 9, 9, 9],
        activations: ['relu', 'relu', 'softmax'],
        description: '3 Hidden Layers à 9 Neuronen – testet Tiefe vs. Breite.',
        learningRate: 0.01,
        epochs: 15000
    },
    overkill: {
        label: '💥 Overkill (2×32 Hidden)',
        topology: [9, 32, 32, 9],
        activations: ['relu', 'relu', 'softmax'],
        description: '2 breite Hidden Layers – deutlich mehr Kapazität als nötig.',
        learningRate: 0.005,
        epochs: 20000
    }
};

class NNPlaygroundController {
    /**
     * @param {Object} config — ID-Mapping aller DOM-Elemente
     */
    constructor(config) {
        this.config = config;

        /** @type {string} Current preset key */
        this.currentPreset = 'sweet_spot';

        /** @type {NeuralNetwork|null} */
        this.network = null;

        /** @type {NNMLPVisualizer|null} */
        this.mlpViz = null;

        /** @type {NNTrainingCharts|null} */
        this.charts = null;

        /** @type {TTTRegularBoard} DRY Game Engine */
        this.board = new TTTRegularBoard();

        /** @type {boolean} */
        this.isTraining = false;

        /** @type {number} */
        this.epoch = 0;

        /** @type {number[]} Loss history for chart */
        this.lossHistory = [];

        /** @type {number[]} Accuracy history for chart */
        this.accuracyHistory = [];

        /** @type {number} Epochs per animation frame during training */
        this.trainingSpeed = 1;

        /** @type {number} Frame counter for throttled updates */
        this._frameCount = 0;

        /** @type {number|null} Animation frame ID */
        this._trainingRAF = null;

        /** @type {Object[]|null} Cached training data */
        this._trainingData = null;

        this._init();
    }

    /**
     * Initializes the controller: binds UI, creates network & visualizers.
     * @private
     */
    _init() {
        this._bindTabs();
        this._bindToolbar();
        this._bindPresetSelector();
        this._bindWeightFilter();

        // Create NN with default preset
        this._createNetwork(this.currentPreset);

        // Create visualizer (no more heatmap!)
        this._createVisualizer();

        // Initial forward pass to populate output
        this._runForwardPass();

        // Populate trainer tab with initial sample
        this._populateTrainerOnInit();
    }

    // ========================================================================
    // TAB SYSTEM
    // ========================================================================

    /**
     * Binds sidebar tab switching (5 tabs).
     * @private
     */
    _bindTabs() {
        const tabBtns = document.querySelectorAll('.nn-sidebar__tab-btn');
        const tabPanels = document.querySelectorAll('.nn-sidebar__tab-panel');

        tabBtns.forEach(btn => {
            btn.addEventListener('click', () => {
                const target = btn.dataset.tab;

                tabBtns.forEach(b => b.classList.remove('active'));
                tabPanels.forEach(p => p.classList.remove('active'));

                btn.classList.add('active');
                const panel = document.getElementById(target);
                if (panel) panel.classList.add('active');
            });
        });
    }

    // ========================================================================
    // TOOLBAR
    // ========================================================================

    /**
     * Binds toolbar buttons (Step, Train, Pause, Reset, Speed).
     * @private
     */
    _bindToolbar() {
        const stepBtn = document.getElementById(this.config.stepBtn);
        const trainBtn = document.getElementById(this.config.trainBtn);
        const pauseBtn = document.getElementById(this.config.pauseBtn);
        const resetBtn = document.getElementById(this.config.resetBtn);
        const speedSlider = document.getElementById(this.config.speedSlider);

        if (stepBtn) {
            stepBtn.addEventListener('click', () => this._trainStep());
        }

        if (trainBtn) {
            trainBtn.addEventListener('click', () => this._startTraining());
        }

        if (pauseBtn) {
            pauseBtn.addEventListener('click', () => this._pauseTraining());
        }

        if (resetBtn) {
            resetBtn.addEventListener('click', () => this._reset());
        }

        if (speedSlider) {
            speedSlider.addEventListener('input', (e) => {
                this.trainingSpeed = parseInt(e.target.value);
            });
        }
    }

    // ========================================================================
    // WEIGHT FILTER
    // ========================================================================

    /**
     * Binds the weight (connection) filter slider.
     * @private
     */
    _bindWeightFilter() {
        const slider = document.getElementById(this.config.weightFilter);
        const valueDisplay = document.getElementById(this.config.weightFilterValue);

        if (!slider) return;

        slider.addEventListener('input', (e) => {
            const pct = parseInt(e.target.value);
            if (valueDisplay) valueDisplay.textContent = pct + '%';
            if (this.mlpViz) {
                this.mlpViz.applyWeightFilter(pct);
            }
        });
    }

    // ========================================================================
    // PRESET SELECTOR
    // ========================================================================

    /**
     * Binds the topology preset dropdown in the Architecture tab.
     * @private
     */
    _bindPresetSelector() {
        const selector = document.getElementById(this.config.presetSelector);
        if (!selector) return;

        // Populate dropdown
        selector.innerHTML = '';
        for (const [key, preset] of Object.entries(NN_PRESETS)) {
            const option = document.createElement('option');
            option.value = key;
            option.textContent = preset.label;
            if (key === this.currentPreset) option.selected = true;
            selector.appendChild(option);
        }

        // Description
        const descEl = document.getElementById(this.config.presetDescription);
        if (descEl) {
            descEl.textContent = NN_PRESETS[this.currentPreset].description;
        }

        selector.addEventListener('change', (e) => {
            const newPreset = e.target.value;
            this.currentPreset = newPreset;
            this._createNetwork(newPreset);
            this._recreateVisualizer();
            this._resetBoard();
            this._runForwardPass();

            if (descEl) {
                descEl.textContent = NN_PRESETS[newPreset].description;
            }

            this._updateParamCount();
        });

        this._updateParamCount();
    }

    /**
     * Updates the parameter count display.
     * @private
     */
    _updateParamCount() {
        const el = document.getElementById(this.config.paramCountDisplay);
        if (el && this.network) {
            el.textContent = this.network.getTotalParameterCount().toLocaleString();
        }
    }

    // ========================================================================
    // NEURAL NETWORK
    // ========================================================================

    /**
     * Creates a new NeuralNetwork from a preset.
     * @param {string} presetKey
     * @private
     */
    _createNetwork(presetKey) {
        const preset = NN_PRESETS[presetKey];
        if (!preset) return;

        this.network = new NeuralNetwork({
            layers: preset.topology,
            activations: preset.activations,
            lossFunction: 'crossEntropy',
            learningRate: preset.learningRate
        });

        this.epoch = 0;
        this.isTraining = false;
        this.lossHistory = [];
        this.accuracyHistory = [];
        this._updateEpochDisplay();
        this._updateLossDisplay(null);
    }

    // ========================================================================
    // VISUALIZER
    // ========================================================================

    /**
     * Creates the MLP visualizer (SVG-merged, no separate heatmap).
     * @private
     */
    _createVisualizer() {
        const preset = NN_PRESETS[this.currentPreset];

        // MLP Visualizer with integrated input/output boards
        this.mlpViz = new NNMLPVisualizer({
            container: this.config.networkContainer,
            callbacks: {
                onInputCellClick: (index) => this._handleCellClick(index),
                onHiddenNeuronHover: (neuronIdx, weights) => {
                    this.mlpViz.highlightInputWeights(weights);
                },
                onHiddenNeuronLeave: () => {
                    this.mlpViz.resetInputHighlight(this.board.grid);
                }
            }
        });
        this.mlpViz.init(preset.topology);

        // Training charts (if container exists)
        const chartsContainer = document.getElementById(this.config.chartsContainer);
        if (chartsContainer && typeof NNTrainingCharts !== 'undefined') {
            this.charts = new NNTrainingCharts({ container: chartsContainer });
        }
    }

    /**
     * Destroys and recreates the visualizer (after topology change).
     * @private
     */
    _recreateVisualizer() {
        if (this.mlpViz) this.mlpViz.destroy();
        this._createVisualizer();
    }

    // ========================================================================
    // BOARD INTERACTION (DRY via TTTRegularBoard)
    // ========================================================================

    /**
     * Handles a cell click on the input board.
     * Uses TTTRegularBoard.makeMove() for proper game flow.
     * @param {number} index — Field index (0–8)
     * @private
     */
    _handleCellClick(index) {
        // Don't allow moves during training
        if (this.isTraining) return;

        // If game is over, reset and then make the move
        if (this.board.winner !== NONE) {
            this._resetBoard();
        }

        // Try to make the move via game engine
        const success = this.board.makeMove(index);
        if (!success) return;

        // Sync board to visualizer
        this._syncBoardDisplay();
        this._runForwardPass();
        this._updateGameStatus();
    }

    /**
     * Resets the board to empty state.
     * @private
     */
    _resetBoard() {
        this.board = new TTTRegularBoard();
        this._syncBoardDisplay();
        this._updateGameStatus();
    }

    /**
     * Syncs the current board state to the visualizer.
     * @private
     */
    _syncBoardDisplay() {
        if (this.mlpViz) {
            this.mlpViz.updateInputBoard(this.board.grid);
        }
    }

    /**
     * Updates the game status overlay (win/draw/turn info).
     * Game-over states show a clickable "Neues Spiel" indicator.
     * @private
     */
    _updateGameStatus() {
        const el = document.getElementById(this.config.gameStatus);
        if (!el) return;

        // Remove old click listener
        el.onclick = null;

        if (this.board.winner === PLAYER1) {
            el.innerHTML = '✕ gewinnt! <span class="nn-game-status__restart">↺ Neues Spiel</span>';
            el.className = 'nn-game-status nn-game-status--visible nn-game-status--win';
            el.style.pointerEvents = 'auto';
            el.style.cursor = 'pointer';
            el.onclick = () => { this._resetBoard(); this._runForwardPass(); };
        } else if (this.board.winner === PLAYER2) {
            el.innerHTML = '○ gewinnt! <span class="nn-game-status__restart">↺ Neues Spiel</span>';
            el.className = 'nn-game-status nn-game-status--visible nn-game-status--win';
            el.style.pointerEvents = 'auto';
            el.style.cursor = 'pointer';
            el.onclick = () => { this._resetBoard(); this._runForwardPass(); };
        } else if (this.board.winner === DRAW) {
            el.innerHTML = 'Unentschieden <span class="nn-game-status__restart">↺ Neues Spiel</span>';
            el.className = 'nn-game-status nn-game-status--visible nn-game-status--draw';
            el.style.pointerEvents = 'auto';
            el.style.cursor = 'pointer';
            el.onclick = () => { this._resetBoard(); this._runForwardPass(); };
        } else {
            el.textContent = '';
            el.className = 'nn-game-status';
            el.style.pointerEvents = 'none';
            el.style.cursor = '';
        }
    }

    // ========================================================================
    // BOARD ENCODING (DRY)
    // ========================================================================

    /**
     * Encodes the current board state for the neural network.
     * Current player's pieces -> +1, opponent -> -1, empty -> 0.
     * @returns {Float64Array} — 9 values in [-1, 0, +1]
     * @private
     */
    _encodeBoardForNetwork() {
        const currentPlayer = this.board.currentPlayer;
        return new Float64Array(this.board.grid.map(cell => {
            if (cell === CELL_EMPTY) return 0;
            return cell === currentPlayer ? 1 : -1;
        }));
    }

    /**
     * Creates a legal move mask from the current board.
     * @returns {number[]} — 1 for empty/legal, 0 for occupied
     * @private
     */
    _getLegalMask() {
        return this.board.grid.map(cell => cell === CELL_EMPTY ? 1 : 0);
    }

    // ========================================================================
    // FORWARD PASS
    // ========================================================================

    /**
     * Runs a forward pass through the network with the current board state
     * and updates the output heatmap in the SVG.
     * @private
     */
    _runForwardPass() {
        if (!this.network) return;

        const input = this._encodeBoardForNetwork();
        const legalMask = this._getLegalMask();
        const hasLegalMoves = legalMask.some(v => v === 1);

        let output;
        if (hasLegalMoves) {
            output = this.network.forwardMasked(input, legalMask);
        } else {
            output = this.network.forward(input);
        }

        // Update output heatmap in SVG
        if (this.mlpViz) {
            this.mlpViz.updateOutputHeatmap(output, this.board.grid);
            const snapshot = this.network.getSnapshot();
            this.mlpViz.updateFromSnapshot(snapshot);
        }
    }

    // ========================================================================
    // TRAINING
    // ========================================================================

    /**
     * Generates training data on-demand (cached).
     * @returns {TTTSample[]}
     * @private
     */
    _getTrainingData() {
        if (!this._trainingData) {
            this._trainingData = generateTTTTrainingData({ useSymmetries: false, bothPlayers: true });
            shuffleArray(this._trainingData);
        }
        return this._trainingData;
    }

    /**
     * Executes a single training step (1 epoch over all data).
     * @private
     */
    _trainStep() {
        if (!this.network) return;

        const data = this._getTrainingData();
        const result = this.network.trainEpoch(data);
        this.epoch++;

        this._recordMetrics(result.loss, data);
        this._updateEpochDisplay();
        this._updateLossDisplay(result.loss);
        this._updateTrainerTab(data);
        this._runForwardPass();
    }

    /**
     * Starts continuous training via requestAnimationFrame.
     * Live TF-Playground-like feel: update visualization EVERY frame.
     * @private
     */
    _startTraining() {
        if (this.isTraining) return;
        this.isTraining = true;
        this._frameCount = 0;
        this._updateTrainingButtons(true);

        const data = this._getTrainingData();
        const maxEpochs = NN_PRESETS[this.currentPreset].epochs;

        const trainLoop = () => {
            if (!this.isTraining) return;

            // Run trainingSpeed epochs per frame (1–10)
            const batchSize = Math.max(1, Math.min(this.trainingSpeed, 10));
            let lastResult = null;

            for (let i = 0; i < batchSize && this.epoch < maxEpochs; i++) {
                // trainEpoch shuffles internally — no external shuffle needed
                lastResult = this.network.trainEpoch(data);
                this.epoch++;
            }

            this._frameCount++;

            if (lastResult) {
                this._recordMetrics(lastResult.loss, data);
                this._updateEpochDisplay();
                this._updateLossDisplay(lastResult.loss);
            }

            // Update SVG weights every 5 frames (snapshot is expensive)
            if (this._frameCount % 5 === 0 && this.mlpViz) {
                const snapshot = this.network.getSnapshot();
                this.mlpViz.updateFromSnapshot(snapshot);
            }

            // Update output heatmap every frame
            if (this.mlpViz) {
                const input = this._encodeBoardForNetwork();
                const legalMask = this._getLegalMask();
                const hasLegalMoves = legalMask.some(v => v === 1);
                const output = hasLegalMoves
                    ? this.network.forwardMasked(input, legalMask)
                    : this.network.forward(input);
                this.mlpViz.updateOutputHeatmap(output, this.board.grid);
            }

            // Charts every 10 frames
            if (this._frameCount % 10 === 0) {
                this._updateCharts();
            }

            // Trainer tab every 30 frames (~0.5s)
            if (this._frameCount % 30 === 0) {
                this._updateTrainerTab(data);
            }

            if (this.epoch >= maxEpochs) {
                this._pauseTraining();
                return;
            }

            this._trainingRAF = requestAnimationFrame(trainLoop);
        };

        this._trainingRAF = requestAnimationFrame(trainLoop);
    }

    /**
     * Pauses training.
     * @private
     */
    _pauseTraining() {
        this.isTraining = false;
        if (this._trainingRAF) {
            cancelAnimationFrame(this._trainingRAF);
            this._trainingRAF = null;
        }
        this._updateTrainingButtons(false);
        this._runForwardPass();
        this._updateCharts();
    }

    /**
     * Full reset: new network, clear board, reset epoch.
     * @private
     */
    _reset() {
        this._pauseTraining();
        this._trainingData = null;
        this._createNetwork(this.currentPreset);
        this._resetBoard();
        this._recreateVisualizer();
        this._runForwardPass();
    }

    // ========================================================================
    // METRICS / CHARTS
    // ========================================================================

    /**
     * Records loss and accuracy for chart display.
     * @param {number} loss
     * @param {Object[]} data — Training data for accuracy calculation
     * @private
     */
    _recordMetrics(loss, data) {
        this.lossHistory.push(loss);

        // Sample accuracy every 50 epochs to avoid overhead
        if (this.epoch % 50 === 0 && data.length > 0) {
            let correct = 0;
            const sampleSize = Math.min(100, data.length);
            for (let i = 0; i < sampleSize; i++) {
                const sample = data[i];
                const output = this.network.forward(sample.input);
                const predicted = Array.from(output).indexOf(Math.max(...output));
                const expected = Array.from(sample.target).indexOf(Math.max(...sample.target));
                if (predicted === expected) correct++;
            }
            const acc = correct / sampleSize;
            this.accuracyHistory.push(acc);
            this._updateAccuracyDisplay(acc);
        }

        // Update progress bar
        this._updateProgressBar();
    }

    /**
     * Updates the training charts in the Metriken tab.
     * @private
     */
    _updateCharts() {
        if (this.charts) {
            this.charts.update(this.lossHistory, this.accuracyHistory);
        }
    }

    // ========================================================================
    // TRAINER TAB
    // ========================================================================

    /**
     * Updates the Trainer tab with a random training example.
     * Shows: input board, KI prediction, teacher answer, loss.
     * @param {Object[]} data — Training data
     * @private
     */
    _updateTrainerTab(data) {
        if (!data || data.length === 0) return;

        // Pick a random sample
        const idx = Math.floor(Math.random() * data.length);
        const sample = data[idx];

        // Index display
        const idxEl = document.getElementById('trainerSampleIdx');
        if (idxEl) idxEl.textContent = idx + 1;

        // Render input board (3×3 mini-grid)
        this._renderTrainerGrid('trainerInputGrid', sample.input, 'input');

        // Get KI prediction
        const output = this.network.forward(sample.input);
        this._renderTrainerGrid('trainerOutputGrid', output, 'output');

        // Teacher's correct answer
        const teacherIdx = Array.from(sample.target).indexOf(Math.max(...sample.target));
        const kiIdx = Array.from(output).indexOf(Math.max(...output));

        const teacherEl = document.getElementById('trainerTeacherMove');
        if (teacherEl) {
            const row = Math.floor(teacherIdx / 3) + 1;
            const col = (teacherIdx % 3) + 1;
            teacherEl.textContent = '✅ Lehrer sagt: Feld ' + row + '×' + col;
        }

        const kiEl = document.getElementById('trainerKIMove');
        if (kiEl) {
            const row = Math.floor(kiIdx / 3) + 1;
            const col = (kiIdx % 3) + 1;
            const correct = kiIdx === teacherIdx;
            kiEl.textContent = (correct ? '✅' : '❌') + ' KI wählte: Feld ' + row + '×' + col;
            kiEl.style.color = correct ? '#2ecc71' : '#e74c3c';
        }

        // Loss info
        const lossEl = document.getElementById('trainerLossDelta');
        if (lossEl && this.lossHistory.length > 0) {
            lossEl.textContent = 'Loss: ' + this.lossHistory[this.lossHistory.length - 1].toFixed(4);
        }
    }

    /**
     * Renders a 3×3 mini-grid in the Trainer tab.
     * @param {string} containerId — DOM element ID
     * @param {Float64Array|number[]} values — 9 values
     * @param {'input'|'output'} mode — Rendering mode
     * @private
     */
    _renderTrainerGrid(containerId, values, mode) {
        const container = document.getElementById(containerId);
        if (!container) return;

        container.innerHTML = '';

        for (let i = 0; i < 9; i++) {
            const cell = document.createElement('div');
            cell.className = 'nn-trainer__cell';
            const v = values[i];

            if (mode === 'input') {
                // Input encoding: 1 = current player, -1 = opponent, 0 = empty
                if (v > 0.5) {
                    cell.textContent = '✕';
                    cell.classList.add('nn-trainer__cell--x');
                } else if (v < -0.5) {
                    cell.textContent = '○';
                    cell.classList.add('nn-trainer__cell--o');
                }
            } else {
                // Output: softmax probabilities
                const pct = (v * 100).toFixed(0);
                cell.textContent = pct + '%';
                const intensity = Math.min(v * 3, 1); // Amplify for visibility
                cell.style.backgroundColor = 'rgba(46, 204, 113, ' + (0.05 + intensity * 0.6) + ')';
                cell.style.color = intensity > 0.3 ? '#fff' : 'rgba(255,255,255,0.5)';
            }

            container.appendChild(cell);
        }
    }

    // ========================================================================
    // UI UPDATES
    // ========================================================================

    /**
     * Updates the epoch counter display.
     * @private
     */
    _updateEpochDisplay() {
        const el = document.getElementById(this.config.epochDisplay);
        if (el) el.textContent = this.epoch.toLocaleString();
    }

    /**
     * Updates the loss display.
     * @param {number|null} loss
     * @private
     */
    _updateLossDisplay(loss) {
        const el = document.getElementById(this.config.lossDisplay);
        if (el) {
            el.textContent = loss !== null ? loss.toFixed(4) : '–';
        }
    }

    /**
     * Updates the accuracy display.
     * @param {number} accuracy — 0–1
     * @private
     */
    _updateAccuracyDisplay(accuracy) {
        const el = document.getElementById('accuracyDisplay');
        if (el) {
            el.textContent = (accuracy * 100).toFixed(0) + '%';
        }
    }

    /**
     * Updates the training progress bar.
     * @private
     */
    _updateProgressBar() {
        const bar = document.getElementById('trainingProgress');
        if (!bar) return;
        const maxEpochs = NN_PRESETS[this.currentPreset].epochs;
        const pct = Math.min(100, (this.epoch / maxEpochs) * 100);
        bar.style.width = pct.toFixed(1) + '%';
    }

    /**
     * Updates training button states.
     * @param {boolean} isTraining
     * @private
     */
    _updateTrainingButtons(isTraining) {
        const trainBtn = document.getElementById(this.config.trainBtn);
        const pauseBtn = document.getElementById(this.config.pauseBtn);
        const stepBtn = document.getElementById(this.config.stepBtn);

        if (trainBtn) {
            trainBtn.disabled = isTraining;
            trainBtn.classList.toggle('btn--active', isTraining);
        }
        if (pauseBtn) {
            pauseBtn.disabled = !isTraining;
        }
        if (stepBtn) {
            stepBtn.disabled = isTraining;
        }
    }

    /**
     * Populates the Trainer tab with an initial sample (before training).
     * Gives users a preview of what the teacher data looks like.
     * @private
     */
    _populateTrainerOnInit() {
        const data = this._getTrainingData();
        if (data && data.length > 0) {
            this._updateTrainerTab(data);
        }
    }

    /**
     * Destroys the controller and cleans up.
     */
    destroy() {
        this._pauseTraining();
        if (this.mlpViz) this.mlpViz.destroy();
        if (this.charts) this.charts.destroy();
    }
}