import { store } from '../store.js';
import { getCapabilities } from '../capabilities.js';
import { icon } from '../icons.js';
export class ModelPicker {
constructor() {
this.el = null;
this.dropdownEl = null;
this.open = false;
this._models = [];
this._currentModel = '';
}
render() {
const el = document.createElement('div');
el.className = 'relative';
el.innerHTML = this._buttonTemplate();
this.el = el;
this._bindEvents();
this._syncFromStore();
return this.el;
}
_buttonTemplate() {
const label = this._currentModel || 'Select a model';
const isPlaceholder = !this._currentModel;
const caps = this._currentModel ? getCapabilities(this._currentModel, store.getModelCapabilities()) : null;
const badges = caps ? [
caps.image ? 'Vision' : '',
caps.audio ? 'Audio' : '',
].filter(Boolean).join('') : '';
return `
`;
}
_buildDropdown() {
const div = document.createElement('div');
div.className = 'dropdown-enter absolute left-0 top-full mt-1.5 z-30 bg-[var(--c-card)] border border-[var(--c-bd)] rounded-xl shadow-2xl shadow-black/30 overflow-hidden min-w-48 max-w-xs';
div.setAttribute('role', 'listbox');
const allModels = this._getAllModels();
if (allModels.length === 0) {
div.innerHTML = `
No models found.
Save Settings, then fetch models.
`;
return div;
}
const userCaps = store.getModelCapabilities();
const items = allModels.map(modelId => {
const caps = getCapabilities(modelId, userCaps);
const active = modelId === this._currentModel;
const badges = [
caps.image ? 'Vision' : '',
caps.audio ? 'Audio' : '',
].filter(Boolean).join('');
return `
`;
});
div.innerHTML = `${items.join('')}
`;
div.querySelectorAll('[data-model]').forEach(btn => {
btn.addEventListener('click', () => {
this.setModel(btn.dataset.model);
this._closeDropdown();
});
});
return div;
}
_getAllModels() {
return [...new Set(this._models.filter(Boolean))].sort();
}
_bindEvents() {
const btn = this.el.querySelector('#model-picker-btn');
btn.addEventListener('click', (e) => {
e.stopPropagation();
this.open ? this._closeDropdown() : this._openDropdown();
});
document.addEventListener('click', () => {
if (this.open) this._closeDropdown();
});
document.addEventListener('caps:changed', () => this._updateButton());
document.addEventListener('settings:changed', () => this._syncFromStore());
document.addEventListener('models:changed', () => this._syncFromStore());
}
_syncFromStore() {
this._models = store.getAvailableModels();
const selected = store.getCurrentModel();
this._currentModel = this._models.includes(selected) ? selected : '';
this._updateButton();
}
_openDropdown() {
this._closeDropdown();
this.dropdownEl = this._buildDropdown();
this.el.appendChild(this.dropdownEl);
this.open = true;
this.el.querySelector('#model-picker-btn').setAttribute('aria-expanded', 'true');
}
_closeDropdown() {
if (this.dropdownEl && this.dropdownEl.parentNode) {
this.dropdownEl.parentNode.removeChild(this.dropdownEl);
}
this.dropdownEl = null;
this.open = false;
this.el?.querySelector('#model-picker-btn')?.setAttribute('aria-expanded', 'false');
}
_updateButton() {
const isPlaceholder = !this._currentModel;
const caps = this._currentModel ? getCapabilities(this._currentModel, store.getModelCapabilities()) : null;
const badges = caps ? [
caps.image ? 'Vision' : '',
caps.audio ? 'Audio' : '',
].filter(Boolean).join('') : '';
const nameEl = this.el?.querySelector('.model-name');
const badgesEl = this.el?.querySelector('.model-badges');
const btn = this.el?.querySelector('#model-picker-btn');
if (nameEl) nameEl.textContent = this._currentModel || 'Select a model';
if (badgesEl) badgesEl.innerHTML = badges;
if (btn) {
btn.classList.remove('text-[var(--c-tx3)]', 'text-[var(--c-tx2)]');
btn.classList.add(isPlaceholder ? 'text-[var(--c-tx3)]' : 'text-[var(--c-tx2)]');
}
}
setModel(modelId) {
this._currentModel = modelId;
store.setCurrentModel(store.getSettings().baseUrl, modelId);
this._updateButton();
document.dispatchEvent(new CustomEvent('model:changed', { detail: { model: modelId } }));
}
getModel() {
return this._currentModel;
}
setModels(models) {
this._models = [...new Set((models || []).filter(Boolean))].sort();
const selected = store.getCurrentModel();
if (selected && !this._models.includes(selected)) {
this._currentModel = '';
store.setCurrentModel(store.getSettings().baseUrl, '');
document.dispatchEvent(new CustomEvent('model:changed', { detail: { model: '' } }));
} else {
this._currentModel = this._models.includes(selected) ? selected : '';
}
if (this.open) {
this._closeDropdown();
this._openDropdown();
}
this._updateButton();
}
syncToConversation(conv) {
this._syncFromStore();
}
}