diff --git a/public/components/admin.html b/public/components/admin.html
index d116755..eb3bb5e 100644
--- a/public/components/admin.html
+++ b/public/components/admin.html
@@ -202,6 +202,64 @@
+
+
diff --git a/public/css/styles.css b/public/css/styles.css
index 1714f29..df4a3f0 100644
--- a/public/css/styles.css
+++ b/public/css/styles.css
@@ -269,6 +269,14 @@ textarea.full-input{resize:vertical;}
.auth-box{padding:24px;}
}
+/* TOGGLE SWITCH */
+.toggle-switch{position:relative;display:inline-block;width:36px;height:20px;vertical-align:middle;}
+.toggle-switch input{opacity:0;width:0;height:0;}
+.toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:var(--g300);border-radius:20px;transition:.2s;}
+.toggle-slider::before{content:'';position:absolute;height:16px;width:16px;left:2px;bottom:2px;background:white;border-radius:50%;transition:.2s;}
+.toggle-switch input:checked+.toggle-slider{background:var(--blue);}
+.toggle-switch input:checked+.toggle-slider::before{transform:translateX(16px);}
+
.editable-box::-webkit-scrollbar,.output-text::-webkit-scrollbar{width:5px;}
.editable-box::-webkit-scrollbar-thumb,.output-text::-webkit-scrollbar-thumb{background:var(--g300);border-radius:3px;}
diff --git a/public/js/admin.js b/public/js/admin.js
index 48a80cd..136bcf8 100644
--- a/public/js/admin.js
+++ b/public/js/admin.js
@@ -279,6 +279,8 @@
if (e.target.closest('#btn-clear-smtp')) clearSmtp();
if (e.target.closest('#btn-save-auto-delete')) saveAutoDelete();
if (e.target.closest('#btn-reset-all-defaults')) resetAllDefaults();
+ if (e.target.closest('#btn-add-custom-model')) addCustomModel();
+ if (e.target.closest('#btn-save-default-model')) saveDefaultModel();
});
// When email template selector changes, repopulate fields
@@ -326,6 +328,7 @@
loadPromptList();
loadSmtp();
+ loadModels();
}
// ---- ANNOUNCEMENT ----
@@ -557,6 +560,179 @@
.catch(function() { showToast('Request failed', 'error'); });
}
+ // ---- AI MODEL MANAGEMENT ----
+
+ var modelsLoaded = false;
+
+ function loadModels() {
+ if (modelsLoaded) return;
+ modelsLoaded = true;
+
+ fetch('/api/admin/config/models', { headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (!data.success) return;
+ window._adminModelsData = data;
+ var providerEl = document.getElementById('admin-model-provider');
+ if (providerEl) providerEl.textContent = (data.provider || 'openrouter').toUpperCase();
+ renderModelTable(data.models || []);
+ renderCustomModels(data.custom || []);
+ populateDefaultModelSelect(data.models || [], data.custom || []);
+ })
+ .catch(function(err) { console.error('[Admin] Models load failed:', err); });
+ }
+
+ function renderModelTable(models) {
+ var tbody = document.getElementById('admin-models-body');
+ if (!tbody) return;
+ if (models.length === 0) {
+ tbody.innerHTML = '
| No models |
';
+ return;
+ }
+
+ var catColors = { free: 'var(--green)', fast: 'var(--blue)', smart: 'var(--purple)', premium: 'var(--amber)' };
+
+ tbody.innerHTML = models.map(function(m) {
+ var catColor = catColors[m.category] || 'var(--g500)';
+ var checked = m.enabled ? 'checked' : '';
+ return '
' +
+ '' + esc(m.name) + ' ' + esc(m.id) + ' | ' +
+ '' + esc(m.category) + ' | ' +
+ '' + esc(m.cost || '?') + ' | ' +
+ '' + esc(m.tag || '') + ' | ' +
+ ' | ' +
+ '
';
+ }).join('');
+
+ // Bind toggle events
+ tbody.querySelectorAll('.model-toggle').forEach(function(cb) {
+ cb.addEventListener('change', function() {
+ var modelId = cb.dataset.modelId;
+ var enabled = cb.checked;
+ toggleModel(modelId, enabled);
+ });
+ });
+ }
+
+ function toggleModel(modelId, enabled) {
+ fetch('/api/admin/config/models/toggle', {
+ method: 'PUT',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ modelId: modelId, enabled: enabled })
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ showToast((enabled ? 'Enabled' : 'Disabled') + ': ' + modelId, 'success');
+ } else {
+ showToast(data.error || 'Toggle failed', 'error');
+ }
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ }
+
+ function renderCustomModels(custom) {
+ var container = document.getElementById('admin-custom-models-list');
+ if (!container) return;
+ if (custom.length === 0) {
+ container.innerHTML = '
No custom models added
';
+ return;
+ }
+ container.innerHTML = custom.map(function(m) {
+ return '
' +
+ '' + esc(m.name) + '' +
+ '' + esc(m.id) + '' +
+ '' + esc(m.cost || '?') + '' +
+ '' +
+ '
';
+ }).join('');
+
+ container.querySelectorAll('[data-delete-model]').forEach(function(btn) {
+ btn.addEventListener('click', function() {
+ var id = btn.dataset.deleteModel;
+ if (!confirm('Remove custom model "' + id + '"?')) return;
+ deleteCustomModel(id);
+ });
+ });
+ }
+
+ function populateDefaultModelSelect(models, custom) {
+ var sel = document.getElementById('admin-default-model');
+ if (!sel) return;
+ sel.innerHTML = '
';
+ var all = models.filter(function(m) { return m.enabled; }).concat(custom || []);
+ all.forEach(function(m) {
+ var opt = document.createElement('option');
+ opt.value = m.id;
+ opt.textContent = m.name + ' (' + (m.cost || '?') + ')';
+ sel.appendChild(opt);
+ });
+
+ // Load current default
+ fetch('/api/admin/config', { headers: getAuthHeaders() })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (!data.success) return;
+ var cfg = {};
+ (data.config || []).forEach(function(row) { cfg[row.key] = row.value; });
+ if (cfg['models.default']) sel.value = cfg['models.default'];
+ });
+ }
+
+ function addCustomModel() {
+ var id = document.getElementById('admin-custom-id').value.trim();
+ var name = document.getElementById('admin-custom-name').value.trim();
+ var cost = document.getElementById('admin-custom-cost').value.trim();
+ if (!id || !name) { showToast('Model ID and name required', 'error'); return; }
+
+ fetch('/api/admin/config/models/custom', {
+ method: 'POST',
+ headers: getAuthHeaders(),
+ body: JSON.stringify({ id: id, name: name, cost: cost || '?', category: 'smart' })
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ showToast('Custom model added: ' + name, 'success');
+ document.getElementById('admin-custom-id').value = '';
+ document.getElementById('admin-custom-name').value = '';
+ document.getElementById('admin-custom-cost').value = '';
+ modelsLoaded = false;
+ loadModels();
+ } else {
+ showToast(data.error || 'Add failed', 'error');
+ }
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ }
+
+ function deleteCustomModel(modelId) {
+ fetch('/api/admin/config/models/custom/' + encodeURIComponent(modelId), {
+ method: 'DELETE',
+ headers: getAuthHeaders()
+ })
+ .then(function(r) { return r.json(); })
+ .then(function(data) {
+ if (data.success) {
+ showToast('Model removed', 'info');
+ modelsLoaded = false;
+ loadModels();
+ } else {
+ showToast(data.error || 'Delete failed', 'error');
+ }
+ })
+ .catch(function() { showToast('Request failed', 'error'); });
+ }
+
+ function saveDefaultModel() {
+ var sel = document.getElementById('admin-default-model');
+ if (!sel) return;
+ var val = sel.value;
+ putConfig('models.default', val)
+ .then(function() { showToast('Default model saved' + (val ? ': ' + val : ' (provider default)'), 'success'); })
+ .catch(function() { showToast('Save failed', 'error'); });
+ }
+
// ---- HELPERS ----
function putConfig(key, value) {
diff --git a/public/js/app.js b/public/js/app.js
index 1ac2bae..683b31f 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -196,14 +196,21 @@ document.addEventListener('DOMContentLoaded', function() {
});
}
+ // Determine default model (admin override or first model)
+ var defaultModelId = data.defaultModel || (window._currentModels.length > 0 ? window._currentModels[0].id : '');
+
if (modelSelect && window._currentModels.length > 0) {
window._buildModelOptions(modelSelect);
- if (costBadge) costBadge.textContent = window._currentProvider.toUpperCase() + ' | ' + window._currentModels[0].cost;
+ // Select the admin-configured default
+ if (defaultModelId) modelSelect.value = defaultModelId;
+ var defaultM = window._currentModels.find(function(x) { return x.id === modelSelect.value; });
+ if (costBadge) costBadge.textContent = window._currentProvider.toUpperCase() + ' | ' + (defaultM ? defaultM.cost : '');
}
// Populate all per-tab model selectors already in DOM
document.querySelectorAll('.tab-model-select').forEach(function(sel) {
window._buildModelOptions(sel);
+ if (defaultModelId) sel.value = defaultModelId;
});
})
.catch(function(err) { console.warn('Models load failed:', err); });
diff --git a/server.js b/server.js
index b7fda0f..84ddbb2 100644
--- a/server.js
+++ b/server.js
@@ -121,10 +121,11 @@ app.use('/api/admin', require('./src/routes/adminConfig'));
const { getAvailableModels, activeProvider: modelsProvider } = require('./src/utils/models');
app.get('/api/models', async (req, res) => {
try {
- var { getAvailableModelsWithOverrides } = require('./src/utils/models');
+ var { getAvailableModelsWithOverrides, DEFAULT_MODEL } = require('./src/utils/models');
var db = require('./src/db/database');
var models = await getAvailableModelsWithOverrides(db);
- res.json({ models: models, provider: modelsProvider });
+ var defaultOverride = await db.getSetting('models.default');
+ res.json({ models: models, provider: modelsProvider, defaultModel: defaultOverride || DEFAULT_MODEL });
} catch(e) {
res.json({ models: getAvailableModels(), provider: modelsProvider });
}