170 lines
7.7 KiB
JavaScript
170 lines
7.7 KiB
JavaScript
// ==========================================
|
|
// 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 = '<div class="model-dropdown-item" style="color:var(--text-3);cursor:default">No models loaded</div>';
|
|
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 ? '<img src="' + esc(m.personaAvatar) + '" class="dropdown-avatar" alt="">' : '';
|
|
const handle = m.isPersona && m.personaHandle ? '<span class="item-handle">@' + esc(m.personaHandle) + '</span>' : '';
|
|
const teamBadge = m.source === 'team' && m.teamName ? '<span class="badge-team" style="font-size:9px;padding:0 4px">\uD83D\uDC65 ' + esc(m.teamName) + '</span>' : '';
|
|
const provider = m.provider ? '<span class="item-provider">' + esc(m.provider) + '</span>' : '';
|
|
div.innerHTML = avatar + '<span class="item-label">' + esc(m.name || m.id) + handle + '</span>' + 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;
|
|
},
|
|
};
|