Changeset 0.10.5 (#61)
This commit is contained in:
@@ -85,83 +85,78 @@ async function handleChangePassword() {
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function handleCreateProvider() {
|
||||
const name = document.getElementById('providerName').value.trim();
|
||||
const provider = document.getElementById('providerType').value;
|
||||
const endpoint = document.getElementById('providerEndpoint').value.trim();
|
||||
const apiKey = document.getElementById('providerApiKey').value.trim();
|
||||
const model = document.getElementById('providerDefaultModel').value.trim();
|
||||
// ── User BYOK Provider — primitive instances (initialized in _initSettingsListeners) ──
|
||||
var _userProvForm = null;
|
||||
var _userProvList = null;
|
||||
|
||||
const editId = UI._editingProviderId;
|
||||
function _initUserProviderPrimitives() {
|
||||
const formEl = document.getElementById('providerAddForm');
|
||||
const listEl = document.getElementById('providerList');
|
||||
if (!formEl || !listEl) return;
|
||||
|
||||
if (editId) {
|
||||
// Update mode — api_key optional (blank = keep existing)
|
||||
if (!name || !endpoint) return UI.toast('Fill in required fields', 'warning');
|
||||
const patch = { name, provider, endpoint, model_default: model };
|
||||
if (apiKey) patch.api_key = apiKey;
|
||||
try {
|
||||
await API.updateConfig(editId, patch);
|
||||
UI.toast('Provider updated', 'success');
|
||||
UI._editingProviderId = null;
|
||||
document.getElementById('providerApiKey').placeholder = 'API key';
|
||||
UI.hideProviderForm();
|
||||
UI.loadProviderList();
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
} else {
|
||||
// Create mode
|
||||
if (!name || !endpoint || !apiKey) return UI.toast('Fill in required fields', 'warning');
|
||||
try {
|
||||
const result = await API.createConfig(name, provider, endpoint, apiKey, model);
|
||||
if (result.warning) {
|
||||
UI.toast(`Provider added — ${result.warning}`, 'warning');
|
||||
} else {
|
||||
const count = result.models_fetched || 0;
|
||||
UI.toast(`Provider added — ${count} model${count !== 1 ? 's' : ''} synced`, 'success');
|
||||
}
|
||||
UI.hideProviderForm();
|
||||
UI.loadProviderList();
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
}
|
||||
_userProvForm = renderProviderForm(formEl, {
|
||||
prefix: 'userProv',
|
||||
showPrivate: false,
|
||||
showDefaultModel: true,
|
||||
onSubmit: async (vals, isEdit) => {
|
||||
try {
|
||||
if (isEdit) {
|
||||
if (!vals.name || !vals.endpoint) return UI.toast('Fill in required fields', 'warning');
|
||||
const patch = { name: vals.name, provider: vals.provider, endpoint: vals.endpoint, model_default: vals.model_default };
|
||||
if (vals.api_key) patch.api_key = vals.api_key;
|
||||
await API.updateConfig(vals._editId, patch);
|
||||
UI.toast('Provider updated', 'success');
|
||||
} else {
|
||||
if (!vals.name || !vals.endpoint || !vals.api_key) return UI.toast('Fill in required fields', 'warning');
|
||||
const result = await API.createConfig(vals.name, vals.provider, vals.endpoint, vals.api_key, vals.model_default);
|
||||
if (result.warning) UI.toast(`Provider added — ${result.warning}`, 'warning');
|
||||
else {
|
||||
const count = result.models_fetched || 0;
|
||||
UI.toast(`Provider added — ${count} model${count !== 1 ? 's' : ''} synced`, 'success');
|
||||
}
|
||||
}
|
||||
formEl.style.display = 'none';
|
||||
_userProvForm.setCreateMode();
|
||||
_userProvList.refresh();
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onCancel: () => {
|
||||
formEl.style.display = 'none';
|
||||
_userProvForm.setCreateMode();
|
||||
},
|
||||
});
|
||||
|
||||
async function deleteProvider(id, name) {
|
||||
if (!confirm(`Remove provider "${name}"?`)) return;
|
||||
try {
|
||||
await API.deleteConfig(id);
|
||||
UI.toast('Provider removed', 'success');
|
||||
UI.loadProviderList();
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function refreshProviderModels(id, name) {
|
||||
try {
|
||||
UI.toast(`Fetching models for ${name}...`, 'info');
|
||||
const result = await API.fetchProviderModels(id);
|
||||
const total = result.total || 0;
|
||||
UI.toast(`${name}: ${total} model${total !== 1 ? 's' : ''} synced`, 'success');
|
||||
fetchModels(); // refresh the model selector
|
||||
} catch (e) {
|
||||
UI.toast(`Failed to fetch models: ${e.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function editProvider(id) {
|
||||
try {
|
||||
const cfg = await API.getConfig(id);
|
||||
// Populate the add form with existing values
|
||||
document.getElementById('providerName').value = cfg.name || '';
|
||||
document.getElementById('providerType').value = cfg.provider || 'openai';
|
||||
document.getElementById('providerEndpoint').value = cfg.endpoint || '';
|
||||
document.getElementById('providerApiKey').value = ''; // Don't expose key
|
||||
document.getElementById('providerApiKey').placeholder = cfg.has_key ? '(unchanged — leave blank to keep)' : 'API key';
|
||||
document.getElementById('providerDefaultModel').value = cfg.model_default || '';
|
||||
// Show form and switch to edit mode
|
||||
UI.showProviderForm();
|
||||
UI._editingProviderId = id;
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
_userProvList = renderProviderList(listEl, {
|
||||
apiFetch: () => API.listConfigs(),
|
||||
showRefresh: true,
|
||||
emptyMsg: 'No providers configured.',
|
||||
onEdit: async (prov) => {
|
||||
try {
|
||||
const cfg = await API.getConfig(prov.id);
|
||||
_userProvForm.setEditMode(cfg.id, cfg);
|
||||
formEl.style.display = '';
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onDelete: async (prov) => {
|
||||
if (!await showConfirm(`Remove provider "${prov.name}"?`)) return;
|
||||
try {
|
||||
await API.deleteConfig(prov.id);
|
||||
UI.toast('Provider removed', 'success');
|
||||
_userProvList.refresh();
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
},
|
||||
onRefresh: async (prov) => {
|
||||
try {
|
||||
UI.toast(`Fetching models for ${prov.name}...`, 'info');
|
||||
const result = await API.fetchProviderModels(prov.id);
|
||||
const total = result.total || 0;
|
||||
UI.toast(`${prov.name}: ${total} model${total !== 1 ? 's' : ''} synced`, 'success');
|
||||
fetchModels();
|
||||
} catch (e) { UI.toast(`Failed to fetch models: ${e.message}`, 'error'); }
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleSaveAdminSettings() {
|
||||
@@ -209,67 +204,50 @@ async function handleSaveAdminSettings() {
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ── User Model Role Functions ─────────────────
|
||||
// ── User Model Role — primitive instance (initialized in _initSettingsListeners) ──
|
||||
var _userRoleConfig = null;
|
||||
|
||||
async function userRoleProviderChanged(role) {
|
||||
const provSel = document.getElementById(`userRole-${role}-provider`);
|
||||
const modelSel = document.getElementById(`userRole-${role}-model`);
|
||||
if (!provSel || !modelSel) return;
|
||||
function _initUserRolePrimitive() {
|
||||
const el = document.getElementById('userRolesConfig');
|
||||
if (!el) return;
|
||||
|
||||
const providerId = provSel.value;
|
||||
modelSel.innerHTML = '<option value="">— select model —</option>';
|
||||
if (!providerId) return;
|
||||
|
||||
// Filter by provider AND model type matching the role
|
||||
const typeForRole = role === 'embedding' ? 'embedding' : 'chat';
|
||||
const allModels = App.models || [];
|
||||
const provModels = allModels.filter(m =>
|
||||
m.configId === providerId &&
|
||||
(m.model_type || 'chat') === typeForRole
|
||||
);
|
||||
provModels.forEach(m => {
|
||||
modelSel.innerHTML += `<option value="${m.baseModelId}">${esc(m.name || m.baseModelId)}</option>`;
|
||||
_userRoleConfig = renderRoleConfig(el, {
|
||||
scope: 'personal',
|
||||
showFallback: false,
|
||||
modelIdField: 'baseModelId',
|
||||
modelNameField: 'name',
|
||||
providerIdField: 'configId',
|
||||
noneLabel: '— use org default —',
|
||||
fetchProviders: async () => {
|
||||
const configs = await API.listConfigs();
|
||||
const list = configs.configs || configs.data || [];
|
||||
return list.filter(c => c.scope === 'personal');
|
||||
},
|
||||
fetchModels: async () => {
|
||||
return App.models || [];
|
||||
},
|
||||
fetchRoles: async () => {
|
||||
const settings = await API.getSettings();
|
||||
return settings.model_roles || {};
|
||||
},
|
||||
onSave: async (roleId, config) => {
|
||||
if (!config.primary) throw new Error('Select both a provider and model');
|
||||
const settings = await API.getSettings();
|
||||
const roles = settings.model_roles || {};
|
||||
roles[roleId] = config;
|
||||
await API.updateSettings({ model_roles: roles });
|
||||
UI.toast(`${roleId} role override saved`, 'success');
|
||||
},
|
||||
onClear: async (roleId) => {
|
||||
const settings = await API.getSettings();
|
||||
const roles = settings.model_roles || {};
|
||||
delete roles[roleId];
|
||||
await API.updateSettings({ model_roles: Object.keys(roles).length > 0 ? roles : null });
|
||||
UI.toast(`${roleId} role override removed — using org default`, 'success');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function saveUserRole(role) {
|
||||
const provSel = document.getElementById(`userRole-${role}-provider`);
|
||||
const modelSel = document.getElementById(`userRole-${role}-model`);
|
||||
if (!provSel || !modelSel) return;
|
||||
|
||||
const providerId = provSel.value;
|
||||
const modelId = modelSel.value;
|
||||
|
||||
if (!providerId || !modelId) {
|
||||
UI.toast('Select both a provider and model', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const settings = await API.getSettings();
|
||||
const roles = settings.model_roles || {};
|
||||
roles[role] = { primary: { provider_config_id: providerId, model_id: modelId } };
|
||||
await API.updateSettings({ model_roles: roles });
|
||||
UI.toast(`${role} role override saved`, 'success');
|
||||
UI.loadUserRoles();
|
||||
} catch (e) {
|
||||
UI.toast(e.message || 'Failed to save role', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function clearUserRole(role) {
|
||||
try {
|
||||
const settings = await API.getSettings();
|
||||
const roles = settings.model_roles || {};
|
||||
delete roles[role];
|
||||
await API.updateSettings({ model_roles: Object.keys(roles).length > 0 ? roles : null });
|
||||
UI.toast(`${role} role override removed — using org default`, 'success');
|
||||
UI.loadUserRoles();
|
||||
} catch (e) {
|
||||
UI.toast(e.message || 'Failed to clear role', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command Palette ──────────────────────────
|
||||
|
||||
const _cmdCommands = [
|
||||
@@ -473,20 +451,13 @@ function _initSettingsListeners() {
|
||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||
});
|
||||
|
||||
document.getElementById('providerShowAddBtn').addEventListener('click', UI.showProviderForm);
|
||||
document.getElementById('providerCancelBtn').addEventListener('click', UI.hideProviderForm);
|
||||
document.getElementById('providerCreateBtn').addEventListener('click', handleCreateProvider);
|
||||
document.getElementById('providerType').addEventListener('change', function() {
|
||||
const endpoints = {
|
||||
openai: 'https://api.openai.com/v1',
|
||||
anthropic: 'https://api.anthropic.com',
|
||||
venice: 'https://api.venice.ai/api/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
};
|
||||
const ep = document.getElementById('providerEndpoint');
|
||||
if (!ep.value || Object.values(endpoints).includes(ep.value)) {
|
||||
ep.value = endpoints[this.value] || '';
|
||||
}
|
||||
// User BYOK provider primitives
|
||||
_initUserProviderPrimitives();
|
||||
_initUserRolePrimitive();
|
||||
document.getElementById('providerShowAddBtn').addEventListener('click', () => {
|
||||
const formEl = document.getElementById('providerAddForm');
|
||||
if (_userProvForm) _userProvForm.setCreateMode();
|
||||
formEl.style.display = '';
|
||||
});
|
||||
|
||||
// Settings — Team management (team admin self-service)
|
||||
@@ -538,83 +509,12 @@ function _initSettingsListeners() {
|
||||
UI.loadTeamPresetModelDropdown(UI._managingTeamId);
|
||||
});
|
||||
|
||||
// Team — providers
|
||||
const defaultEndpoints = {
|
||||
openai: 'https://api.openai.com/v1',
|
||||
anthropic: 'https://api.anthropic.com',
|
||||
venice: 'https://api.venice.ai/api/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
};
|
||||
// Team — providers (primitive-based, initialized lazily via UI.loadTeamManageProviders)
|
||||
document.getElementById('settingsTeamAddProviderBtn')?.addEventListener('click', () => {
|
||||
const form = document.getElementById('settingsTeamAddProvider');
|
||||
form.style.display = form.style.display === 'none' ? '' : 'none';
|
||||
const sel = document.getElementById('teamProviderType');
|
||||
if (sel && sel.options.length === 0) {
|
||||
['openai', 'anthropic', 'venice', 'openrouter'].forEach(p => {
|
||||
const labels = { openai: 'OpenAI-compatible', anthropic: 'Anthropic', venice: 'Venice.ai', openrouter: 'OpenRouter' };
|
||||
const opt = document.createElement('option');
|
||||
opt.value = p;
|
||||
opt.textContent = labels[p] || p;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
});
|
||||
document.getElementById('teamProviderType')?.addEventListener('change', function() {
|
||||
const ep = document.getElementById('teamProviderEndpoint');
|
||||
if (!ep.value || Object.values(defaultEndpoints).includes(ep.value)) {
|
||||
ep.value = defaultEndpoints[this.value] || '';
|
||||
}
|
||||
});
|
||||
document.getElementById('settingsTeamCancelProvider')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsTeamAddProvider').style.display = 'none';
|
||||
});
|
||||
document.getElementById('settingsTeamAddProviderSubmit')?.addEventListener('click', async () => {
|
||||
const teamId = UI._managingTeamId;
|
||||
const name = document.getElementById('teamProviderName').value.trim();
|
||||
const provider = document.getElementById('teamProviderType').value;
|
||||
const endpoint = document.getElementById('teamProviderEndpoint').value.trim();
|
||||
const apiKey = document.getElementById('teamProviderKey').value;
|
||||
const isPrivate = document.getElementById('teamProviderPrivate').checked;
|
||||
if (!name) return UI.toast('Name required', 'error');
|
||||
if (!endpoint) return UI.toast('Endpoint required', 'error');
|
||||
try {
|
||||
await API.teamCreateProvider(teamId, { name, provider, endpoint, api_key: apiKey, is_private: isPrivate });
|
||||
document.getElementById('settingsTeamAddProvider').style.display = 'none';
|
||||
document.getElementById('teamProviderName').value = '';
|
||||
document.getElementById('teamProviderEndpoint').value = '';
|
||||
document.getElementById('teamProviderKey').value = '';
|
||||
document.getElementById('teamProviderPrivate').checked = false;
|
||||
UI.toast('Provider added');
|
||||
await UI.loadTeamManageProviders(teamId);
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
});
|
||||
|
||||
// Team — edit provider
|
||||
document.getElementById('settingsTeamCancelEditProvider')?.addEventListener('click', () => {
|
||||
document.getElementById('settingsTeamEditProvider').style.display = 'none';
|
||||
});
|
||||
document.getElementById('settingsTeamEditProviderSubmit')?.addEventListener('click', async () => {
|
||||
const teamId = UI._managingTeamId;
|
||||
const provId = UI._editingTeamProviderId;
|
||||
if (!provId) return;
|
||||
const updates = {};
|
||||
const name = document.getElementById('teamProviderEditName').value.trim();
|
||||
const endpoint = document.getElementById('teamProviderEditEndpoint').value.trim();
|
||||
const apiKey = document.getElementById('teamProviderEditKey').value;
|
||||
const defaultModel = document.getElementById('teamProviderEditDefault').value.trim();
|
||||
const isPrivate = document.getElementById('teamProviderEditPrivate').checked;
|
||||
if (name) updates.name = name;
|
||||
if (endpoint) updates.endpoint = endpoint;
|
||||
if (apiKey) updates.api_key = apiKey;
|
||||
if (defaultModel) updates.model_default = defaultModel;
|
||||
updates.is_private = isPrivate;
|
||||
try {
|
||||
await API.teamUpdateProvider(teamId, provId, updates);
|
||||
document.getElementById('settingsTeamEditProvider').style.display = 'none';
|
||||
UI.toast('Provider updated');
|
||||
await UI.loadTeamManageProviders(teamId);
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
const formEl = document.getElementById('settingsTeamProviderForm');
|
||||
if (!formEl) return;
|
||||
if (UI._teamProvForm) UI._teamProvForm.setCreateMode();
|
||||
formEl.style.display = formEl.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
|
||||
// User — personal presets
|
||||
|
||||
Reference in New Issue
Block a user