memlnaut-nisps/playground/js/ui/engine-switcher.js
w1n5t0n f1b6e1aba0 feat(playground): engine switcher UI component (meml-phg)
Adds EngineSwitcher module with clickable engine cards (active highlight,
coming-soon greyed state, loading bar animation, confirm-on-switch when
training data exists). Wired into the Engine drawer; C15 active,
Additive/FM marked coming-soon with toast. Dock title reflects active
engine; engineId persisted to/restored from localStorage.
2026-04-03 17:49:07 +01:00

134 lines
4.3 KiB
JavaScript

/**
* EngineSwitcher — engine selection UI for the Engine drawer.
*
* Usage:
* EngineSwitcher.init(containerEl, engines, onSwitch);
*
* @param {HTMLElement} containerEl — element to prepend the switcher into
* @param {Array<{id, displayName, paramCount, description, comingSoon?}>} engines
* @param {(engineId: string) => void} onSwitch — called when user confirms a switch
*/
export const EngineSwitcher = {
_containerEl: null,
_engines: [],
_activeId: null,
_onSwitch: null,
_hasTrainingData: null, // () => boolean
/**
* @param {HTMLElement} containerEl
* @param {Array<{id:string, displayName:string, paramCount:number, description:string, comingSoon?:boolean}>} engines
* @param {(engineId:string) => void} onSwitch
* @param {{ hasTrainingData?: () => boolean }} [opts]
*/
init(containerEl, engines, onSwitch, opts = {}) {
this._containerEl = containerEl;
this._engines = engines;
this._onSwitch = onSwitch;
this._hasTrainingData = opts.hasTrainingData || (() => false);
this._activeId = engines[0]?.id ?? null;
this._render();
},
/** Update which card appears active (call after engine swap completes). */
setActive(engineId) {
this._activeId = engineId;
const section = this._containerEl?.querySelector('.engine-switcher-section');
if (!section) return;
section.querySelectorAll('.engine-card').forEach(card => {
card.classList.toggle('active', card.dataset.engineId === engineId);
});
},
/** Show/hide the loading spinner on a card. */
setLoading(engineId, loading) {
const section = this._containerEl?.querySelector('.engine-switcher-section');
if (!section) return;
const card = section.querySelector(`[data-engine-id="${engineId}"]`);
if (card) card.classList.toggle('loading', loading);
},
_render() {
const container = this._containerEl;
if (!container) return;
// Remove any previous section if re-initialised
const existing = container.querySelector('.engine-switcher-section');
if (existing) existing.remove();
const section = document.createElement('div');
section.className = 'engine-switcher-section';
const heading = document.createElement('div');
heading.className = 'engine-switcher-heading';
heading.textContent = 'Synthesis Engine';
section.appendChild(heading);
const grid = document.createElement('div');
grid.className = 'engine-card-grid';
for (const eng of this._engines) {
const card = document.createElement('div');
card.className = 'engine-card';
card.dataset.engineId = eng.id;
if (eng.id === this._activeId) card.classList.add('active');
if (eng.comingSoon) card.classList.add('coming-soon');
const nameRow = document.createElement('div');
nameRow.className = 'engine-card-name-row';
const name = document.createElement('span');
name.className = 'engine-card-name';
name.textContent = eng.displayName;
const badge = document.createElement('span');
badge.className = 'engine-card-params';
badge.textContent = eng.comingSoon ? 'soon' : `${eng.paramCount} params`;
nameRow.appendChild(name);
nameRow.appendChild(badge);
const desc = document.createElement('span');
desc.className = 'engine-card-desc';
desc.textContent = eng.description;
card.appendChild(nameRow);
card.appendChild(desc);
grid.appendChild(card);
if (!eng.comingSoon) {
card.addEventListener('click', () => this._handleClick(eng));
}
}
section.appendChild(grid);
const divider = document.createElement('div');
divider.className = 'engine-switcher-divider';
section.appendChild(divider);
// Prepend before any existing drawer content (e.g. engine-tuning sliders)
container.insertBefore(section, container.firstChild);
},
_handleClick(eng) {
if (eng.id === this._activeId) return;
const proceed = () => {
this.setLoading(eng.id, true);
try {
this._onSwitch(eng.id);
} finally {
// Loading state cleared by caller via setLoading/setActive
}
};
if (this._hasTrainingData()) {
const msg = 'Switching engines will reset training examples. Network weights will be partially preserved. Continue?';
if (window.confirm(msg)) proceed();
} else {
proceed();
}
},
};