// ========================================== // Chat Switchboard — ModelSelector Component // ========================================== // v0.25.0: Extracted from ui-core.js. // Grouped dropdown: personas (global, team, personal), models, BYOK. // Pattern: Go template partial (model-selector.html) + JS factory (this file). // // Usage: // const sel = ModelSelector.create({ id: 'main', onChange: (id, label) => { ... } }); // sel.setModels(App.models); // sel.select(modelId); // sel.getSelected(); // → current model ID // sel.destroy(); const ModelSelector = { ...createComponentRegistry('ModelSelector'), create(opts) { const pfx = opts.id || ''; const instance = componentMixin({ id: pfx, dropdownEl: document.getElementById(pfx + 'modelDropdown'), btnEl: document.getElementById(pfx + 'modelDropdownBtn'), labelEl: document.getElementById(pfx + 'modelDropdownLabel'), menuEl: document.getElementById(pfx + 'modelDropdownMenu'), capsEl: document.getElementById(pfx + 'modelCaps'), onChange: opts.onChange || null, _value: '', _models: [], // ── Selection ─────────────────────── getSelected() { return this._value; }, select(id, label) { this._value = id; if (this.labelEl) this.labelEl.textContent = label || id || 'Select a model'; // Highlight if (this.menuEl) { this.menuEl.querySelectorAll('.model-dropdown-item').forEach(el => { el.classList.toggle('selected', el.dataset.value === id); }); } this._updateCaps(); if (this.onChange) this.onChange(id, label); }, // ── Model list ────────────────────── setModels(models) { this._models = models || []; this._rebuild(); }, _rebuild() { if (!this.menuEl) return; this.menuEl.innerHTML = ''; if (this._models.length === 0) { this.menuEl.innerHTML = '
No models loaded
'; this.select('', 'No models loaded'); return; } const globalPersonas = this._models.filter(m => m.isPersona && m.personaScope === 'global'); const teamPersonas = this._models.filter(m => m.isPersona && m.personaScope === 'team'); const personalPersonas = this._models.filter(m => m.isPersona && m.personaScope === 'personal'); const models = this._models.filter(m => !m.isPersona && !m.hidden); const addGroup = (label, items) => { if (items.length === 0) return; const hdr = document.createElement('div'); hdr.className = 'model-dropdown-group'; hdr.textContent = label; this.menuEl.appendChild(hdr); items.forEach(m => this.menuEl.appendChild(this._createItem(m))); }; addGroup('\u26A1 Personas', globalPersonas); // Group team personas by team name const teamGroups = {}; teamPersonas.forEach(m => { const tn = m.personaTeamName || 'Team'; (teamGroups[tn] = teamGroups[tn] || []).push(m); }); Object.keys(teamGroups).sort().forEach(tn => { addGroup('\uD83D\uDC65 ' + tn, teamGroups[tn]); }); addGroup('\uD83D\uDD27 My Personas', personalPersonas); addGroup('Models', models.filter(m => m.source !== 'personal')); addGroup('\uD83D\uDD11 My Providers', models.filter(m => m.source === 'personal')); }, _createItem(m) { const self = this; const div = document.createElement('div'); div.className = 'model-dropdown-item'; div.dataset.value = m.id; const avatar = m.personaAvatar ? '' : ''; const handle = m.isPersona && m.personaHandle ? '@' + esc(m.personaHandle) + '' : ''; const teamBadge = m.source === 'team' && m.teamName ? '\uD83D\uDC65 ' + esc(m.teamName) + '' : ''; const provider = m.provider ? '' + esc(m.provider) + '' : ''; div.innerHTML = avatar + '' + esc(m.name || m.id) + handle + '' + teamBadge + provider; div.addEventListener('click', () => { self.select(m.id, (m.name || m.id) + (m.provider ? ' (' + m.provider + ')' : '')); self.close(); }); return div; }, // ── Restore selection ─────────────── /** * Restore selection from a preferred ID, falling back through * admin default → first visible. */ restore(preferredId, adminDefault) { const visible = this._models.filter(m => !m.hidden); if (visible.length === 0) { this.select('', 'No visible models'); return; } const byId = (id) => visible.find(m => m.id === id || m.baseModelId === id); const match = byId(preferredId) || byId(adminDefault) || visible[0]; this.select(match.id, (match.name || match.id) + (match.provider ? ' (' + match.provider + ')' : '')); }, // ── Dropdown toggle ───────────────── open() { if (this.menuEl) this.menuEl.classList.add('open'); }, close() { if (this.menuEl) this.menuEl.classList.remove('open'); }, toggle() { if (this.menuEl) this.menuEl.classList.toggle('open'); }, // ── Capability badges ─────────────── _updateCaps() { if (!this.capsEl) return; if (!this._value) { this.capsEl.innerHTML = ''; return; } const m = this._models.find(m => m.id === this._value); const caps = m?.capabilities; if (!caps || Object.keys(caps).length === 0) { this.capsEl.innerHTML = ''; return; } // Use global renderCapBadges if available (from ui-core.js) if (typeof renderCapBadges === 'function') { this.capsEl.innerHTML = renderCapBadges(caps); } }, getSelectedCaps() { const m = this._models.find(m => m.id === this._value); return m?.capabilities || {}; }, // ── Event wiring ──────────────────── bind() { this._on(this.btnEl, 'click', (e) => { e.stopPropagation(); this.toggle(); }); this._on(document, 'click', (e) => { if (!e.target.closest('#' + pfx + 'modelDropdown')) this.close(); }); }, }, ModelSelector); ModelSelector._register(pfx, instance); return instance; }, };