Changeset 0.8.5 (#48)
This commit is contained in:
@@ -1013,6 +1013,8 @@ button { font-family: var(--font); cursor: pointer; }
|
||||
.admin-model-toggle:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.admin-model-toggle.enabled { border-color: var(--success); color: var(--success); }
|
||||
.admin-model-toggle.team { border-color: #60a5fa; color: #60a5fa; }
|
||||
.model-list-item.model-hidden { opacity: 0.5; }
|
||||
.model-list-item.model-hidden .model-name { text-decoration: line-through; }
|
||||
|
||||
.admin-provider-row {
|
||||
display: flex; align-items: center; gap: 12px; padding: 10px 0;
|
||||
|
||||
@@ -305,8 +305,24 @@
|
||||
</div>
|
||||
<!-- Models Tab -->
|
||||
<div class="settings-tab-content" id="settingsModelsTab" style="display:none">
|
||||
<p class="section-hint">Models available from your providers and global configs.</p>
|
||||
<div id="userModelList" class="model-list-grid"><div class="empty-hint">Loading models...</div></div>
|
||||
<section class="settings-section">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||||
<h3 style="font-size:14px;margin:0">Available Models</h3>
|
||||
<div>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(true)" title="Show all models in selector">Show All</button>
|
||||
<button class="btn-small" onclick="bulkSetUserModelVisibility(false)" title="Hide all models from selector">Hide All</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="section-hint" style="margin-bottom:8px">Toggle visibility in your model selector. Hidden models are still accessible through presets.</p>
|
||||
<div id="userModelList" class="model-list-grid"><div class="empty-hint">Loading models...</div></div>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h3 style="font-size:14px;margin-bottom:4px">My Presets</h3>
|
||||
<p class="section-hint" style="margin-bottom:8px">Personal model presets with custom system prompts and settings.</p>
|
||||
<div id="userPresetList"><div class="empty-hint">Loading...</div></div>
|
||||
<div id="userAddPresetForm" style="display:none;margin-top:8px" class="admin-inline-form"></div>
|
||||
<button class="btn-small" id="userAddPresetBtn" style="margin-top:6px">+ New Preset</button>
|
||||
</section>
|
||||
</div>
|
||||
<!-- Teams Tab (visible to team admins) -->
|
||||
<div class="settings-tab-content" id="settingsTeamsTab" style="display:none">
|
||||
|
||||
@@ -214,6 +214,15 @@ const API = {
|
||||
// ── Models ───────────────────────────────
|
||||
|
||||
listEnabledModels() { return this._get('/api/v1/models/enabled'); },
|
||||
getModelPreferences() { return this._get('/api/v1/models/preferences'); },
|
||||
setModelPreference(modelId, hidden) { return this._put('/api/v1/models/preferences', { model_id: modelId, hidden }); },
|
||||
bulkSetModelPreferences(modelIds, hidden) { return this._post('/api/v1/models/preferences/bulk', { model_ids: modelIds, hidden }); },
|
||||
|
||||
// User presets
|
||||
listUserPresets() { return this._get('/api/v1/presets'); },
|
||||
createUserPreset(preset) { return this._post('/api/v1/presets', preset); },
|
||||
updateUserPreset(id, updates) { return this._put(`/api/v1/presets/${id}`, updates); },
|
||||
deleteUserPreset(id) { return this._del(`/api/v1/presets/${id}`); },
|
||||
listAllModels() { return this._get('/api/v1/models'); },
|
||||
|
||||
// ── API Configs (user providers) ─────────
|
||||
|
||||
@@ -6,6 +6,7 @@ const App = {
|
||||
chats: [],
|
||||
currentChatId: null,
|
||||
models: [],
|
||||
hiddenModels: new Set(),
|
||||
isGenerating: false,
|
||||
abortController: null,
|
||||
serverSettings: {},
|
||||
@@ -200,10 +201,16 @@ function resolveCapabilities(backendCaps, modelId) {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
const data = await API.listEnabledModels();
|
||||
// Load user model preferences
|
||||
try {
|
||||
const prefData = await API.getModelPreferences();
|
||||
App.hiddenModels = new Set(
|
||||
(prefData.preferences || []).filter(p => p.hidden).map(p => p.model_id)
|
||||
);
|
||||
} catch (e) { App.hiddenModels = new Set(); }
|
||||
|
||||
App.models = (data.models || []).map(m => {
|
||||
const isPreset = !!m.is_preset;
|
||||
// Presets: use preset_id as selector value, base model for caps
|
||||
// Regular: model_id is both
|
||||
const id = isPreset ? (m.preset_id || m.id) : (m.model_id || m.id);
|
||||
const baseModelId = m.model_id || m.id;
|
||||
return {
|
||||
@@ -218,6 +225,7 @@ async function fetchModels() {
|
||||
presetScope: m.preset_scope || null,
|
||||
presetAvatar: m.preset_avatar || null,
|
||||
presetTeamName: m.preset_team_name || null,
|
||||
hidden: !isPreset && App.hiddenModels.has(baseModelId),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -233,7 +241,7 @@ async function fetchModels() {
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPreset).length} presets)`);
|
||||
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPreset).length} presets, ${App.models.filter(m => m.hidden).length} hidden)`);
|
||||
} catch (e) { console.warn('Model fetch failed:', e.message); }
|
||||
UI.updateModelSelector();
|
||||
UI.updateCapabilityBadges();
|
||||
@@ -1092,6 +1100,49 @@ function initListeners() {
|
||||
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||
});
|
||||
|
||||
// User — personal presets
|
||||
var _userPresetForm = null;
|
||||
document.getElementById('userAddPresetBtn')?.addEventListener('click', () => {
|
||||
const container = document.getElementById('userAddPresetForm');
|
||||
container.style.display = container.style.display === 'none' ? '' : 'none';
|
||||
if (!_userPresetForm) {
|
||||
_userPresetForm = renderPresetForm(container, {
|
||||
prefix: 'userPreset',
|
||||
showAvatar: true,
|
||||
showProviderConfig: false,
|
||||
onSubmit: async (vals) => {
|
||||
if (!vals.name) return UI.toast('Name required', 'error');
|
||||
if (!vals.base_model_id) return UI.toast('Select a base model', 'error');
|
||||
try {
|
||||
const result = await API.createUserPreset(vals);
|
||||
const presetId = result?.id;
|
||||
if (presetId && vals._pendingAvatar) {
|
||||
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||
catch (e) { console.warn('User preset avatar upload failed:', e.message); }
|
||||
}
|
||||
container.style.display = 'none';
|
||||
_userPresetForm.clearForm();
|
||||
UI.toast('Preset created');
|
||||
await UI.loadUserPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onCancel: () => { container.style.display = 'none'; _userPresetForm.clearForm(); }
|
||||
});
|
||||
}
|
||||
// Populate model dropdown from user's available models
|
||||
const sel = _userPresetForm.getModelSelect();
|
||||
if (sel) {
|
||||
sel.innerHTML = '<option value="">Select base model...</option>';
|
||||
App.models.filter(m => !m.isPreset).forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.baseModelId || m.id;
|
||||
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin — banner controls
|
||||
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
|
||||
document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none';
|
||||
@@ -1569,6 +1620,46 @@ async function bulkSetVisibility(visibility) {
|
||||
} catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
|
||||
}
|
||||
|
||||
// ── User Model Preferences ──────────────────
|
||||
|
||||
async function toggleUserModelVisibility(modelId, currentlyHidden) {
|
||||
try {
|
||||
await API.setModelPreference(modelId, !currentlyHidden);
|
||||
if (currentlyHidden) App.hiddenModels.delete(modelId);
|
||||
else App.hiddenModels.add(modelId);
|
||||
await UI.loadUserModels();
|
||||
await fetchModels(); // refresh model selector
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function bulkSetUserModelVisibility(show) {
|
||||
const models = App.models.filter(m => !m.isPreset);
|
||||
if (!models.length) return;
|
||||
const shouldHide = !show;
|
||||
// Only update models not already in the desired state
|
||||
const toUpdate = models
|
||||
.map(m => m.baseModelId || m.id)
|
||||
.filter(mid => App.hiddenModels.has(mid) !== shouldHide);
|
||||
if (!toUpdate.length) { UI.toast(show ? 'All already visible' : 'All already hidden'); return; }
|
||||
try {
|
||||
await API.bulkSetModelPreferences(toUpdate, shouldHide);
|
||||
toUpdate.forEach(mid => shouldHide ? App.hiddenModels.add(mid) : App.hiddenModels.delete(mid));
|
||||
await UI.loadUserModels();
|
||||
await fetchModels();
|
||||
UI.toast(show ? `${toUpdate.length} model(s) shown` : `${toUpdate.length} model(s) hidden`);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function deleteUserPreset(id, name) {
|
||||
if (!confirm(`Delete preset "${name}"?`)) return;
|
||||
try {
|
||||
await API.deleteUserPreset(id);
|
||||
UI.toast('Preset deleted');
|
||||
await UI.loadUserPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ── Admin Presets ────────────────────────────
|
||||
|
||||
var _adminPresetForm = null;
|
||||
|
||||
52
src/js/ui.js
52
src/js/ui.js
@@ -533,7 +533,7 @@ const UI = {
|
||||
const globalPresets = App.models.filter(m => m.isPreset && m.presetScope === 'global');
|
||||
const teamPresets = App.models.filter(m => m.isPreset && m.presetScope === 'team');
|
||||
const personalPresets = App.models.filter(m => m.isPreset && m.presetScope === 'personal');
|
||||
const models = App.models.filter(m => !m.isPreset);
|
||||
const models = App.models.filter(m => !m.isPreset && !m.hidden);
|
||||
|
||||
const addGroup = (label, items) => {
|
||||
if (items.length === 0) return;
|
||||
@@ -573,7 +573,7 @@ const UI = {
|
||||
const settingsSel = document.getElementById('settingsModel');
|
||||
if (settingsSel) {
|
||||
settingsSel.innerHTML = '';
|
||||
App.models.forEach(m => {
|
||||
App.models.filter(m => !m.hidden).forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = (m.name || m.id) + (m.provider ? ` (${m.provider})` : '');
|
||||
@@ -781,7 +781,7 @@ const UI = {
|
||||
UI.loadProviderList();
|
||||
UI.checkUserProvidersAllowed();
|
||||
}
|
||||
if (tab === 'models') UI.loadUserModels();
|
||||
if (tab === 'models') { UI.loadUserModels(); UI.loadUserPresets(); }
|
||||
if (tab === 'appearance') UI.loadAppearanceSettings();
|
||||
if (tab === 'teams') UI.loadTeamsTab();
|
||||
},
|
||||
@@ -1469,14 +1469,24 @@ const UI = {
|
||||
if (!el) return;
|
||||
el.innerHTML = '<div class="loading">Loading models...</div>';
|
||||
try {
|
||||
// Load preferences if not already loaded
|
||||
if (!App.hiddenModels) {
|
||||
try {
|
||||
const prefData = await API.getModelPreferences();
|
||||
App.hiddenModels = new Set(
|
||||
(prefData.preferences || []).filter(p => p.hidden).map(p => p.model_id)
|
||||
);
|
||||
} catch (e) { App.hiddenModels = new Set(); }
|
||||
}
|
||||
|
||||
const data = await API.listEnabledModels();
|
||||
// Only show base models, not presets
|
||||
const models = (data.models || []).filter(m => !m.is_preset);
|
||||
if (!models.length) {
|
||||
el.innerHTML = '<div class="empty-hint">No models available</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = models.map(m => {
|
||||
const mid = m.model_id || m.id;
|
||||
const caps = m.capabilities || {};
|
||||
const badges = [];
|
||||
if (caps.max_output_tokens > 0) badges.push(`<span class="cap-badge cap-context">${(caps.max_output_tokens/1000).toFixed(0)}K</span>`);
|
||||
@@ -1484,11 +1494,41 @@ const UI = {
|
||||
if (caps.vision) badges.push('<span class="cap-badge cap-accent">👁</span>');
|
||||
if (caps.thinking) badges.push('<span class="cap-badge cap-accent">💭</span>');
|
||||
const src = m.source === 'personal' ? '<span class="badge-user" style="font-size:10px">personal</span>' : '';
|
||||
return `<div class="model-list-item">
|
||||
<span class="model-name">${esc(m.model_id || m.id)}</span>
|
||||
const hidden = App.hiddenModels.has(mid);
|
||||
const toggleCls = hidden ? '' : 'enabled';
|
||||
const toggleLabel = hidden ? 'Hidden' : '✓ Visible';
|
||||
return `<div class="model-list-item ${hidden ? 'model-hidden' : ''}">
|
||||
<span class="model-name">${esc(mid)}</span>
|
||||
<span class="model-caps-inline">${badges.join('')}</span>
|
||||
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||
${src}
|
||||
<button class="admin-model-toggle ${toggleCls}" onclick="toggleUserModelVisibility('${esc(mid)}', ${hidden})" title="${hidden ? 'Show in selector' : 'Hide from selector'}">${toggleLabel}</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
},
|
||||
|
||||
async loadUserPresets() {
|
||||
const el = document.getElementById('userPresetList');
|
||||
if (!el) return;
|
||||
try {
|
||||
const data = await API.listUserPresets();
|
||||
// Only show personal presets owned by user
|
||||
const presets = (data.presets || []).filter(p => p.scope === 'personal');
|
||||
if (!presets.length) {
|
||||
el.innerHTML = '<div class="empty-hint">No personal presets yet</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = presets.map(p => {
|
||||
const avatarEl = p.avatar ? `<img src="${p.avatar}" class="preset-row-avatar" alt="">` : '';
|
||||
return `<div class="admin-preset-row" style="padding:6px 0">
|
||||
<div class="preset-info">
|
||||
<strong>${avatarEl}${esc(p.name)}</strong>
|
||||
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||
</div>
|
||||
<div class="preset-actions">
|
||||
<button class="btn-delete" onclick="deleteUserPreset('${p.id}', '${esc(p.name)}')" title="Delete">✕</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
|
||||
Reference in New Issue
Block a user