import { store } from '../store.js';
import { DEFAULT_CAPABILITIES } from '../capabilities.js';
import { fetchModels } from '../api.js';
import {
DEFAULT_CONTEXT_LIMIT_TOKENS,
DEFAULT_CONTEXT_RESET_THRESHOLD_PERCENT,
} from '../context.js';
import { icon } from '../icons.js';
export class SettingsModal {
constructor() {
this.el = null;
this.visible = false;
this._fetchedModels = [];
}
render() {
const el = document.createElement('div');
el.innerHTML = this._template();
this.el = el.firstElementChild;
this._bindEvents();
return this.el;
}
_template() {
return `
Settings
Model Capabilities
`;
}
_bindEvents() {
this.el.querySelector('#settings-close').addEventListener('click', () => this.hide());
this.el.addEventListener('mousedown', (e) => {
if (e.target === this.el) {
this._backdropMouseDown = true;
} else {
this._backdropMouseDown = false;
}
});
this.el.addEventListener('click', (e) => {
if (e.target === this.el && this._backdropMouseDown) this.hide();
this._backdropMouseDown = false;
});
this.el.querySelector('#settings-save').addEventListener('click', () => this._saveSettings());
this.el.querySelector('#fetch-models-btn').addEventListener('click', () => this._fetchModels());
// Load current values
this._loadValues();
this._renderModelCaps();
}
_loadValues() {
const settings = store.getSettings();
this.el.querySelector('#settings-base-url').value = settings.baseUrl || '';
this.el.querySelector('#settings-api-key').value = settings.apiKey || '';
this.el.querySelector('#settings-context-limit').value = settings.contextLimitTokens || '';
this.el.querySelector('#settings-context-threshold').value =
settings.contextResetThresholdPercent || DEFAULT_CONTEXT_RESET_THRESHOLD_PERCENT;
this._fetchedModels = store.getAvailableModels(settings.baseUrl);
}
_saveSettings() {
const previousBaseUrl = store.getSettings().baseUrl;
const baseUrl = this.el.querySelector('#settings-base-url').value.trim() || 'https://api.openai.com';
const apiKey = this.el.querySelector('#settings-api-key').value.trim();
const rawContextLimit = this.el.querySelector('#settings-context-limit').value.trim();
const contextLimitTokens = rawContextLimit
? Math.max(1024, Math.round(Number(rawContextLimit) || DEFAULT_CONTEXT_LIMIT_TOKENS))
: DEFAULT_CONTEXT_LIMIT_TOKENS;
const rawThreshold = Number(this.el.querySelector('#settings-context-threshold').value);
const contextResetThresholdPercent = Number.isFinite(rawThreshold)
? Math.min(95, Math.max(50, Math.round(rawThreshold)))
: DEFAULT_CONTEXT_RESET_THRESHOLD_PERCENT;
store.saveSettings({
...store.getSettings(),
baseUrl,
apiKey,
contextLimitTokens,
contextResetThresholdPercent,
});
this._fetchedModels = store.getAvailableModels(baseUrl);
const availableModels = store.getAvailableModels(baseUrl);
const currentModel = store.getCurrentModel(baseUrl);
if (previousBaseUrl !== baseUrl && !availableModels.length) {
store.setCurrentModel(baseUrl, '');
document.dispatchEvent(new CustomEvent('model:changed', { detail: { model: '' } }));
} else if (currentModel && !availableModels.includes(currentModel)) {
store.setCurrentModel(baseUrl, '');
document.dispatchEvent(new CustomEvent('model:changed', { detail: { model: '' } }));
}
const btn = this.el.querySelector('#settings-save');
btn.textContent = 'Saved!';
setTimeout(() => { btn.textContent = 'Save Settings'; }, 2000);
document.dispatchEvent(new CustomEvent('settings:changed'));
document.dispatchEvent(new CustomEvent('models:changed'));
}
async _fetchModels() {
const btn = this.el.querySelector('#fetch-models-btn');
const settings = store.getSettings();
btn.disabled = true;
btn.innerHTML = `Fetching...`;
try {
const models = await fetchModels(settings.baseUrl, settings.apiKey);
this._fetchedModels = models;
store.saveAvailableModels(settings.baseUrl, models);
const currentModel = store.getCurrentModel(settings.baseUrl);
if (currentModel && !models.includes(currentModel)) {
store.setCurrentModel(settings.baseUrl, '');
document.dispatchEvent(new CustomEvent('model:changed', { detail: { model: '' } }));
}
this._renderModelCaps(models);
document.dispatchEvent(new CustomEvent('models:changed'));
} catch (err) {
this.el.querySelector('#model-caps-list').innerHTML =
`Failed to fetch: ${err.message}
`;
} finally {
btn.disabled = false;
btn.innerHTML = `${icon('refresh')} Fetch Models`;
}
}
_renderModelCaps(extraModels = []) {
const container = this.el.querySelector('#model-caps-list');
const userCaps = store.getModelCapabilities();
const allModelIds = new Set(extraModels);
const rows = [...allModelIds].sort().map(modelId => {
const base = DEFAULT_CAPABILITIES[modelId] || { text: true, image: false, audio: false };
const override = userCaps[modelId] || {};
const caps = { ...base, ...override };
return `
`;
});
container.innerHTML = rows.join('') || 'No models found. Fetch models or use defaults.
';
// Bind changes
container.querySelectorAll('.cap-check').forEach(cb => {
cb.addEventListener('change', () => this._saveCapChange(cb));
});
}
_saveCapChange(checkbox) {
const row = checkbox.closest('[data-model]');
const modelId = row.dataset.model;
const cap = checkbox.dataset.cap;
const userCaps = store.getModelCapabilities();
if (!userCaps[modelId]) {
const base = DEFAULT_CAPABILITIES[modelId] || { text: true, image: false, audio: false };
userCaps[modelId] = { ...base };
}
userCaps[modelId][cap] = checkbox.checked;
store.saveModelCapabilities(store.getSettings().baseUrl, userCaps);
document.dispatchEvent(new CustomEvent('caps:changed'));
}
show() {
if (!this.el) return;
this._loadValues();
this._renderModelCaps(this._fetchedModels);
document.body.appendChild(this.el);
this.visible = true;
}
hide() {
if (this.el && this.el.parentNode) {
this.el.parentNode.removeChild(this.el);
}
this.visible = false;
}
toggle() {
this.visible ? this.hide() : this.show();
}
}