Changeset 0.10.5 (#61)

This commit is contained in:
2026-02-25 00:18:03 +00:00
parent e5ee78c498
commit d2ec55b16d
16 changed files with 1643 additions and 801 deletions

View File

@@ -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