Changeset 0.8.4 (#47)
This commit is contained in:
294
src/js/app.js
294
src/js/app.js
@@ -130,32 +130,6 @@ function updateAvatarPreview(dataURI) {
|
||||
}
|
||||
}
|
||||
|
||||
// Preset avatar preview (admin form)
|
||||
function updatePresetAvatarPreview(dataURI) {
|
||||
const preview = document.getElementById('presetAvatarPreview');
|
||||
const placeholder = document.getElementById('presetAvatarPlaceholder');
|
||||
const removeBtn = document.getElementById('presetAvatarRemoveBtn');
|
||||
if (!preview) return;
|
||||
const existingImg = preview.querySelector('img');
|
||||
|
||||
if (dataURI) {
|
||||
placeholder.style.display = 'none';
|
||||
if (existingImg) {
|
||||
existingImg.src = dataURI;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = dataURI;
|
||||
img.alt = 'Preset avatar';
|
||||
preview.insertBefore(img, placeholder);
|
||||
}
|
||||
removeBtn.style.display = '';
|
||||
} else {
|
||||
if (existingImg) existingImg.remove();
|
||||
placeholder.style.display = '';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Models ───────────────────────────────────
|
||||
|
||||
// Client-side known model capabilities — used when backend hasn't synced yet.
|
||||
@@ -997,72 +971,16 @@ function initListeners() {
|
||||
// Admin — models
|
||||
document.getElementById('adminFetchModelsBtn')?.addEventListener('click', fetchAdminModels);
|
||||
|
||||
// Admin — presets
|
||||
// Admin — presets (shared form)
|
||||
document.getElementById('adminAddPresetBtn')?.addEventListener('click', () => {
|
||||
const form = ensureAdminPresetForm();
|
||||
if (!form) return;
|
||||
_editingPresetId = null;
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
||||
document.getElementById('adminPresetName').value = '';
|
||||
document.getElementById('adminPresetDesc').value = '';
|
||||
document.getElementById('adminPresetPrompt').value = '';
|
||||
document.getElementById('adminPresetIcon').value = '';
|
||||
document.getElementById('adminPresetTemp').value = '';
|
||||
document.getElementById('adminPresetMaxTokens').value = '';
|
||||
updatePresetAvatarPreview(null);
|
||||
form.setSubmitLabel('Create');
|
||||
form.clearForm();
|
||||
const f = document.getElementById('adminAddPresetForm');
|
||||
f.style.display = f.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
document.getElementById('adminCancelPresetBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
||||
_editingPresetId = null;
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
||||
updatePresetAvatarPreview(null);
|
||||
});
|
||||
document.getElementById('adminCreatePresetBtn')?.addEventListener('click', createAdminPreset);
|
||||
|
||||
// Admin — preset avatar
|
||||
document.getElementById('presetAvatarUploadBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('presetAvatarFileInput').click();
|
||||
});
|
||||
document.getElementById('presetAvatarFileInput')?.addEventListener('change', async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
if (!_editingPresetId) {
|
||||
// For new presets: show preview, upload after create
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => updatePresetAvatarPreview(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
// For existing presets: upload immediately
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
try {
|
||||
const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result);
|
||||
if (data.avatar) {
|
||||
updatePresetAvatarPreview(data.avatar);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar updated');
|
||||
}
|
||||
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
document.getElementById('presetAvatarRemoveBtn')?.addEventListener('click', async () => {
|
||||
if (!_editingPresetId) {
|
||||
updatePresetAvatarPreview(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await API.adminDeletePresetAvatar(_editingPresetId);
|
||||
updatePresetAvatarPreview(null);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar removed');
|
||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||
});
|
||||
|
||||
// Admin — Teams
|
||||
document.getElementById('adminAddTeamBtn')?.addEventListener('click', () => {
|
||||
@@ -1138,38 +1056,41 @@ function initListeners() {
|
||||
document.getElementById('settingsTeamCancelMember')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsTeamAddMember').style.display = 'none';
|
||||
});
|
||||
var _teamPresetForm = null;
|
||||
document.getElementById('settingsTeamAddPresetBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsTeamAddPreset').style.display = '';
|
||||
const container = document.getElementById('settingsTeamAddPreset');
|
||||
container.style.display = '';
|
||||
if (!_teamPresetForm) {
|
||||
_teamPresetForm = renderPresetForm(container, {
|
||||
prefix: 'teamPreset',
|
||||
showAvatar: true,
|
||||
showProviderConfig: false,
|
||||
onSubmit: async (vals) => {
|
||||
const teamId = UI._managingTeamId;
|
||||
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.teamCreatePreset(teamId, vals);
|
||||
// Upload pending avatar
|
||||
const presetId = result?.id;
|
||||
if (presetId && vals._pendingAvatar) {
|
||||
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||
catch (e) { console.warn('Team preset avatar upload failed:', e.message); }
|
||||
}
|
||||
container.style.display = 'none';
|
||||
_teamPresetForm.clearForm();
|
||||
UI.toast('Team preset created');
|
||||
await UI.loadTeamManagePresets(teamId);
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onCancel: () => { container.style.display = 'none'; _teamPresetForm.clearForm(); }
|
||||
});
|
||||
} else {
|
||||
_teamPresetForm.clearForm();
|
||||
}
|
||||
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||
});
|
||||
document.getElementById('settingsTeamCancelPreset')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
||||
});
|
||||
document.getElementById('settingsTeamCreatePresetBtn')?.addEventListener('click', async () => {
|
||||
const teamId = UI._managingTeamId;
|
||||
const name = document.getElementById('settingsTeamPresetName').value.trim();
|
||||
const baseModelId = document.getElementById('settingsTeamPresetModel').value;
|
||||
if (!name) return UI.toast('Name required', 'error');
|
||||
if (!baseModelId) return UI.toast('Select a base model', 'error');
|
||||
const preset = {
|
||||
name,
|
||||
base_model_id: baseModelId,
|
||||
description: document.getElementById('settingsTeamPresetDesc').value.trim(),
|
||||
icon: document.getElementById('settingsTeamPresetIcon').value.trim(),
|
||||
system_prompt: document.getElementById('settingsTeamPresetPrompt').value,
|
||||
temperature: parseFloat(document.getElementById('settingsTeamPresetTemp').value) || undefined,
|
||||
max_tokens: parseInt(document.getElementById('settingsTeamPresetMaxTokens').value) || undefined,
|
||||
};
|
||||
try {
|
||||
await API.teamCreatePreset(teamId, preset);
|
||||
document.getElementById('settingsTeamAddPreset').style.display = 'none';
|
||||
['settingsTeamPresetName','settingsTeamPresetDesc','settingsTeamPresetIcon','settingsTeamPresetPrompt','settingsTeamPresetTemp','settingsTeamPresetMaxTokens']
|
||||
.forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
||||
UI.toast('Team preset created');
|
||||
await UI.loadTeamManagePresets(teamId);
|
||||
await fetchModels(); // refresh model selector
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
// Admin — banner controls
|
||||
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
|
||||
@@ -1650,50 +1571,103 @@ async function bulkSetVisibility(visibility) {
|
||||
|
||||
// ── Admin Presets ────────────────────────────
|
||||
|
||||
var _adminPresetForm = null;
|
||||
function ensureAdminPresetForm() {
|
||||
if (_adminPresetForm) return _adminPresetForm;
|
||||
const container = document.getElementById('adminAddPresetForm');
|
||||
if (!container) return null;
|
||||
_adminPresetForm = renderPresetForm(container, {
|
||||
prefix: 'adminPreset',
|
||||
showAvatar: true,
|
||||
showProviderConfig: true,
|
||||
onSubmit: (vals) => createAdminPreset(vals),
|
||||
onCancel: () => {
|
||||
container.style.display = 'none';
|
||||
_editingPresetId = null;
|
||||
_adminPresetForm.setSubmitLabel('Create');
|
||||
_adminPresetForm.clearForm();
|
||||
}
|
||||
});
|
||||
// Override avatar handlers for edit-mode (upload immediately for existing presets)
|
||||
const fileInput = document.getElementById('adminPreset_avatarFileInput');
|
||||
if (fileInput) {
|
||||
// Remove default handler set by renderPresetForm, add edit-aware one
|
||||
const newInput = fileInput.cloneNode(true);
|
||||
fileInput.parentNode.replaceChild(newInput, fileInput);
|
||||
newInput.addEventListener('change', async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
if (_editingPresetId) {
|
||||
try {
|
||||
const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result);
|
||||
if (data.avatar) {
|
||||
_adminPresetForm.updateAvatarPreview(data.avatar);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar updated');
|
||||
}
|
||||
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
||||
} else {
|
||||
_adminPresetForm.updateAvatarPreview(reader.result);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
document.getElementById('adminPreset_avatarUploadBtn')?.addEventListener('click', () => newInput.click());
|
||||
}
|
||||
const removeBtn = document.getElementById('adminPreset_avatarRemoveBtn');
|
||||
if (removeBtn) {
|
||||
const newBtn = removeBtn.cloneNode(true);
|
||||
removeBtn.parentNode.replaceChild(newBtn, removeBtn);
|
||||
newBtn.addEventListener('click', async () => {
|
||||
if (_editingPresetId) {
|
||||
try {
|
||||
await API.adminDeletePresetAvatar(_editingPresetId);
|
||||
_adminPresetForm.updateAvatarPreview(null);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar removed');
|
||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||
} else {
|
||||
_adminPresetForm.updateAvatarPreview(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
return _adminPresetForm;
|
||||
}
|
||||
|
||||
var _editingPresetId = null;
|
||||
function editAdminPreset(id) {
|
||||
const p = UI._presetCache?.[id];
|
||||
if (!p) return;
|
||||
_editingPresetId = id;
|
||||
const form = document.getElementById('adminAddPresetForm');
|
||||
form.style.display = '';
|
||||
document.getElementById('adminPresetName').value = p.name || '';
|
||||
document.getElementById('adminPresetIcon').value = p.icon || '';
|
||||
document.getElementById('adminPresetDesc').value = p.description || '';
|
||||
document.getElementById('adminPresetPrompt').value = p.system_prompt || '';
|
||||
document.getElementById('adminPresetTemp').value = p.temperature != null ? p.temperature : '';
|
||||
document.getElementById('adminPresetMaxTokens').value = p.max_tokens != null ? p.max_tokens : '';
|
||||
// Set base model and config dropdowns if present
|
||||
const modelSel = document.getElementById('adminPresetModel');
|
||||
if (modelSel) modelSel.value = p.base_model_id || '';
|
||||
const cfgSel = document.getElementById('adminPresetConfig');
|
||||
if (cfgSel) cfgSel.value = p.api_config_id || '';
|
||||
// Avatar preview
|
||||
updatePresetAvatarPreview(p.avatar || null);
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Update';
|
||||
ensureAdminPresetForm();
|
||||
const form = _adminPresetForm;
|
||||
if (!form) return;
|
||||
document.getElementById('adminAddPresetForm').style.display = '';
|
||||
form.setValues(p);
|
||||
form.setSubmitLabel('Update');
|
||||
}
|
||||
|
||||
async function createAdminPreset() {
|
||||
const name = document.getElementById('adminPresetName').value.trim();
|
||||
const baseModelId = document.getElementById('adminPresetModel').value;
|
||||
if (!name || !baseModelId) { UI.toast('Name and base model are required', 'warning'); return; }
|
||||
async function createAdminPreset(vals) {
|
||||
if (!vals) {
|
||||
if (_adminPresetForm) vals = _adminPresetForm.getValues();
|
||||
else return;
|
||||
}
|
||||
if (!vals.name || !vals.base_model_id) { UI.toast('Name and base model are required', 'warning'); return; }
|
||||
|
||||
const preset = {
|
||||
name,
|
||||
base_model_id: baseModelId,
|
||||
description: document.getElementById('adminPresetDesc').value.trim(),
|
||||
icon: document.getElementById('adminPresetIcon').value.trim(),
|
||||
system_prompt: document.getElementById('adminPresetPrompt').value,
|
||||
name: vals.name,
|
||||
base_model_id: vals.base_model_id,
|
||||
description: vals.description || '',
|
||||
system_prompt: vals.system_prompt || '',
|
||||
};
|
||||
|
||||
const configId = document.getElementById('adminPresetConfig').value;
|
||||
if (configId) preset.api_config_id = configId;
|
||||
|
||||
const temp = parseFloat(document.getElementById('adminPresetTemp').value);
|
||||
if (!isNaN(temp)) preset.temperature = temp;
|
||||
|
||||
const maxTok = parseInt(document.getElementById('adminPresetMaxTokens').value);
|
||||
if (!isNaN(maxTok) && maxTok > 0) preset.max_tokens = maxTok;
|
||||
if (vals.api_config_id) preset.api_config_id = vals.api_config_id;
|
||||
if (vals.temperature != null) preset.temperature = vals.temperature;
|
||||
if (vals.max_tokens != null) preset.max_tokens = vals.max_tokens;
|
||||
|
||||
try {
|
||||
let presetId = _editingPresetId;
|
||||
@@ -1707,23 +1681,17 @@ async function createAdminPreset() {
|
||||
}
|
||||
|
||||
// Upload pending avatar for new presets
|
||||
const pendingImg = document.querySelector('#presetAvatarPreview img');
|
||||
if (!_editingPresetId && presetId && pendingImg?.src?.startsWith('data:')) {
|
||||
try {
|
||||
await API.adminUploadPresetAvatar(presetId, pendingImg.src);
|
||||
} catch (e) { console.warn('Preset avatar upload failed:', e.message); }
|
||||
if (!_editingPresetId && presetId && vals._pendingAvatar) {
|
||||
try { await API.adminUploadPresetAvatar(presetId, vals._pendingAvatar); }
|
||||
catch (e) { console.warn('Preset avatar upload failed:', e.message); }
|
||||
}
|
||||
|
||||
_editingPresetId = null;
|
||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
||||
document.getElementById('adminPresetName').value = '';
|
||||
document.getElementById('adminPresetDesc').value = '';
|
||||
document.getElementById('adminPresetPrompt').value = '';
|
||||
document.getElementById('adminPresetIcon').value = '';
|
||||
document.getElementById('adminPresetTemp').value = '';
|
||||
document.getElementById('adminPresetMaxTokens').value = '';
|
||||
updatePresetAvatarPreview(null);
|
||||
if (_adminPresetForm) {
|
||||
_adminPresetForm.setSubmitLabel('Create');
|
||||
_adminPresetForm.clearForm();
|
||||
}
|
||||
await UI.loadAdminPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
|
||||
176
src/js/ui.js
176
src/js/ui.js
@@ -22,6 +22,148 @@ function assistantAvatarURI(msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Shared Preset Form Component ────────────
|
||||
// Renders a preset creation/edit form into a container element.
|
||||
// Options: { prefix, showAvatar, showProviderConfig, onSubmit, onCancel }
|
||||
// Returns: { getValues(), setValues(preset), clearForm(), container }
|
||||
function renderPresetForm(containerEl, options = {}) {
|
||||
const pfx = options.prefix || 'pf';
|
||||
const showAvatar = options.showAvatar !== false;
|
||||
const showConfig = !!options.showProviderConfig;
|
||||
|
||||
containerEl.innerHTML = `
|
||||
<div class="form-group"><label>Name</label><input type="text" id="${pfx}_name" placeholder="Code Reviewer"></div>
|
||||
${showAvatar ? `
|
||||
<div class="avatar-upload-row" style="margin-bottom:0.75rem">
|
||||
<div class="avatar-preview avatar-preview-sm" id="${pfx}_avatarPreview">
|
||||
<span id="${pfx}_avatarPlaceholder">🤖</span>
|
||||
</div>
|
||||
<div class="avatar-actions">
|
||||
<button class="btn-small" type="button" id="${pfx}_avatarUploadBtn">Upload Avatar</button>
|
||||
<button class="btn-small btn-danger" type="button" id="${pfx}_avatarRemoveBtn" style="display:none">Remove</button>
|
||||
<input type="file" id="${pfx}_avatarFileInput" accept="image/png,image/jpeg,image/gif" style="display:none">
|
||||
<div class="form-hint">Optional · 128×128 · shown in chat</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
<div class="form-group"><label>Description</label><input type="text" id="${pfx}_desc" placeholder="Reviews code for best practices"></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Base Model</label><select id="${pfx}_model"></select></div>
|
||||
${showConfig ? `<div class="form-group"><label>Provider Config</label><select id="${pfx}_config"><option value="">Auto-resolve</option></select></div>` : ''}
|
||||
</div>
|
||||
<div class="form-group"><label>System Prompt</label><textarea id="${pfx}_prompt" rows="3" placeholder="You are a code reviewer..."></textarea></div>
|
||||
<div class="form-row">
|
||||
<div class="form-group"><label>Temperature</label><input type="number" id="${pfx}_temp" step="0.1" min="0" max="2" placeholder="default"></div>
|
||||
<div class="form-group"><label>Max Tokens</label><input type="number" id="${pfx}_maxTokens" placeholder="default"></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button class="btn-small btn-primary" id="${pfx}_submitBtn">Create</button>
|
||||
<button class="btn-small" id="${pfx}_cancelBtn">Cancel</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Avatar wiring
|
||||
if (showAvatar) {
|
||||
const uploadBtn = document.getElementById(`${pfx}_avatarUploadBtn`);
|
||||
const fileInput = document.getElementById(`${pfx}_avatarFileInput`);
|
||||
const removeBtn = document.getElementById(`${pfx}_avatarRemoveBtn`);
|
||||
uploadBtn?.addEventListener('click', () => fileInput.click());
|
||||
fileInput?.addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => form.updateAvatarPreview(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
removeBtn?.addEventListener('click', () => form.updateAvatarPreview(null));
|
||||
}
|
||||
|
||||
// Cancel wiring
|
||||
document.getElementById(`${pfx}_cancelBtn`)?.addEventListener('click', () => {
|
||||
if (options.onCancel) options.onCancel();
|
||||
});
|
||||
|
||||
// Submit wiring
|
||||
document.getElementById(`${pfx}_submitBtn`)?.addEventListener('click', () => {
|
||||
if (options.onSubmit) options.onSubmit(form.getValues());
|
||||
});
|
||||
|
||||
const form = {
|
||||
getValues() {
|
||||
const v = {
|
||||
name: document.getElementById(`${pfx}_name`)?.value.trim() || '',
|
||||
description: document.getElementById(`${pfx}_desc`)?.value.trim() || '',
|
||||
base_model_id: document.getElementById(`${pfx}_model`)?.value || '',
|
||||
system_prompt: document.getElementById(`${pfx}_prompt`)?.value || '',
|
||||
};
|
||||
if (showConfig) {
|
||||
const cfgId = document.getElementById(`${pfx}_config`)?.value;
|
||||
if (cfgId) v.api_config_id = cfgId;
|
||||
}
|
||||
const temp = parseFloat(document.getElementById(`${pfx}_temp`)?.value);
|
||||
if (!isNaN(temp)) v.temperature = temp;
|
||||
const maxTok = parseInt(document.getElementById(`${pfx}_maxTokens`)?.value);
|
||||
if (!isNaN(maxTok) && maxTok > 0) v.max_tokens = maxTok;
|
||||
// Pending avatar data URI
|
||||
const img = document.querySelector(`#${pfx}_avatarPreview img`);
|
||||
if (img?.src?.startsWith('data:')) v._pendingAvatar = img.src;
|
||||
return v;
|
||||
},
|
||||
setValues(p) {
|
||||
const el = id => document.getElementById(`${pfx}_${id}`);
|
||||
if (el('name')) el('name').value = p.name || '';
|
||||
if (el('desc')) el('desc').value = p.description || '';
|
||||
if (el('prompt')) el('prompt').value = p.system_prompt || '';
|
||||
if (el('temp')) el('temp').value = p.temperature != null ? p.temperature : '';
|
||||
if (el('maxTokens')) el('maxTokens').value = p.max_tokens != null ? p.max_tokens : '';
|
||||
if (el('model')) el('model').value = p.base_model_id || '';
|
||||
if (showConfig && el('config')) el('config').value = p.api_config_id || '';
|
||||
if (showAvatar) form.updateAvatarPreview(p.avatar || null);
|
||||
},
|
||||
clearForm() {
|
||||
['name','desc','prompt','temp','maxTokens'].forEach(f => {
|
||||
const el = document.getElementById(`${pfx}_${f}`);
|
||||
if (el) el.value = '';
|
||||
});
|
||||
const modelSel = document.getElementById(`${pfx}_model`);
|
||||
if (modelSel) modelSel.selectedIndex = 0;
|
||||
if (showConfig) {
|
||||
const cfgSel = document.getElementById(`${pfx}_config`);
|
||||
if (cfgSel) cfgSel.selectedIndex = 0;
|
||||
}
|
||||
if (showAvatar) form.updateAvatarPreview(null);
|
||||
},
|
||||
updateAvatarPreview(dataURI) {
|
||||
const preview = document.getElementById(`${pfx}_avatarPreview`);
|
||||
const placeholder = document.getElementById(`${pfx}_avatarPlaceholder`);
|
||||
const removeBtn = document.getElementById(`${pfx}_avatarRemoveBtn`);
|
||||
if (!preview) return;
|
||||
const existing = preview.querySelector('img');
|
||||
if (dataURI) {
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
if (existing) { existing.src = dataURI; }
|
||||
else {
|
||||
const img = document.createElement('img');
|
||||
img.src = dataURI; img.alt = 'Preset avatar';
|
||||
preview.insertBefore(img, placeholder);
|
||||
}
|
||||
if (removeBtn) removeBtn.style.display = '';
|
||||
} else {
|
||||
if (existing) existing.remove();
|
||||
if (placeholder) placeholder.style.display = '';
|
||||
if (removeBtn) removeBtn.style.display = 'none';
|
||||
}
|
||||
},
|
||||
setSubmitLabel(label) {
|
||||
const btn = document.getElementById(`${pfx}_submitBtn`);
|
||||
if (btn) btn.textContent = label;
|
||||
},
|
||||
getModelSelect() { return document.getElementById(`${pfx}_model`); },
|
||||
getConfigSelect() { return showConfig ? document.getElementById(`${pfx}_config`) : null; },
|
||||
};
|
||||
return form;
|
||||
}
|
||||
|
||||
const UI = {
|
||||
|
||||
// ── Sidebar ──────────────────────────────
|
||||
@@ -623,6 +765,7 @@ const UI = {
|
||||
|
||||
UI.loadProfileIntoSettings();
|
||||
UI.loadMyTeams();
|
||||
UI.checkUserProvidersAllowed();
|
||||
UI.switchSettingsTab('general');
|
||||
openModal('settingsModal');
|
||||
},
|
||||
@@ -693,10 +836,12 @@ const UI = {
|
||||
async checkUserProvidersAllowed() {
|
||||
const notice = document.getElementById('userProvidersDisabled');
|
||||
const addBtn = document.getElementById('providerShowAddBtn');
|
||||
const tabBtn = document.getElementById('settingsProvidersTabBtn');
|
||||
if (!notice) return;
|
||||
const allowed = App.serverSettings?.user_providers_enabled !== false;
|
||||
notice.style.display = allowed ? 'none' : '';
|
||||
if (addBtn) addBtn.style.display = allowed ? '' : 'none';
|
||||
if (tabBtn) tabBtn.style.display = allowed ? '' : 'none';
|
||||
},
|
||||
|
||||
async loadProfileIntoSettings() {
|
||||
@@ -823,7 +968,7 @@ const UI = {
|
||||
},
|
||||
|
||||
async loadTeamPresetModelDropdown(teamId) {
|
||||
const sel = document.getElementById('settingsTeamPresetModel');
|
||||
const sel = document.getElementById('teamPreset_model');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '<option value="">Loading models...</option>';
|
||||
try {
|
||||
@@ -1094,8 +1239,11 @@ const UI = {
|
||||
const data = await API.adminListPresets();
|
||||
const list = data.presets || [];
|
||||
|
||||
// Ensure form is initialized for dropdown population
|
||||
ensureAdminPresetForm();
|
||||
|
||||
// Populate model dropdown from admin model list (all synced models)
|
||||
const modelSel = document.getElementById('adminPresetModel');
|
||||
const modelSel = _adminPresetForm?.getModelSelect();
|
||||
if (modelSel) {
|
||||
modelSel.innerHTML = '<option value="">Select base model...</option>';
|
||||
try {
|
||||
@@ -1113,7 +1261,7 @@ const UI = {
|
||||
}
|
||||
|
||||
// Populate config dropdown
|
||||
const cfgSel = document.getElementById('adminPresetConfig');
|
||||
const cfgSel = _adminPresetForm?.getConfigSelect();
|
||||
if (cfgSel && cfgSel.options.length <= 1) {
|
||||
try {
|
||||
const cfgData = await API.adminListGlobalConfigs();
|
||||
@@ -1132,12 +1280,21 @@ const UI = {
|
||||
UI._presetCache[p.id] = p;
|
||||
const avatarEl = p.avatar
|
||||
? `<img src="${p.avatar}" class="preset-row-avatar" alt="">`
|
||||
: (p.icon ? `<span class="preset-row-icon">${p.icon}</span>` : '');
|
||||
const scope = p.scope === 'global' ? '<span class="badge-admin">global</span>' : '<span class="badge-user">personal</span>';
|
||||
: '';
|
||||
// Scope badge with attribution
|
||||
let scopeBadge = '';
|
||||
if (p.scope === 'global') {
|
||||
scopeBadge = '<span class="badge-admin">global</span>';
|
||||
} else if (p.scope === 'team' && p.team_name) {
|
||||
scopeBadge = `<span class="badge-team">👥 ${esc(p.team_name)}</span>`;
|
||||
} else if (p.scope === 'personal') {
|
||||
scopeBadge = '<span class="badge-user">personal</span>';
|
||||
}
|
||||
const creator = p.creator_name ? `<span class="text-muted" style="font-size:11px">by ${esc(p.creator_name)}</span>` : '';
|
||||
const status = p.is_active ? '' : '<span class="badge-pending">inactive</span>';
|
||||
return `<div class="admin-preset-row">
|
||||
<div class="preset-info">
|
||||
<strong>${avatarEl}${esc(p.name)}</strong> ${scope} ${status}
|
||||
<strong>${avatarEl}${esc(p.name)}</strong> ${scopeBadge} ${creator} ${status}
|
||||
<div class="preset-meta">${esc(p.base_model_id)} · ${esc(p.provider_name || 'auto')}</div>
|
||||
${p.description ? `<div class="preset-desc">${esc(p.description)}</div>` : ''}
|
||||
</div>
|
||||
@@ -1313,9 +1470,10 @@ const UI = {
|
||||
el.innerHTML = '<div class="loading">Loading models...</div>';
|
||||
try {
|
||||
const data = await API.listEnabledModels();
|
||||
const models = data.models || [];
|
||||
// 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 — add a provider in the section above</div>';
|
||||
el.innerHTML = '<div class="empty-hint">No models available</div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = models.map(m => {
|
||||
@@ -1325,10 +1483,12 @@ const UI = {
|
||||
if (caps.tool_calling) badges.push('<span class="cap-badge cap-accent">🔧</span>');
|
||||
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>
|
||||
<span class="model-caps-inline">${badges.join('')}</span>
|
||||
<span class="model-provider">${esc(m.provider_name || m.provider || '')}</span>
|
||||
${src}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) { el.innerHTML = `<div class="error-hint">${esc(e.message)}</div>`; }
|
||||
|
||||
Reference in New Issue
Block a user