Changeset 0.8.6 (#49)

This commit is contained in:
2026-02-22 16:52:19 +00:00
parent 633421708f
commit 15be26c516
17 changed files with 870 additions and 152 deletions

View File

@@ -11,6 +11,16 @@ const App = {
abortController: null,
serverSettings: {},
// Find model by composite ID, with fallback to bare model_id match
findModel(id) {
if (!id) return null;
// Exact match on composite ID (configId:modelId)
let m = App.models.find(m => m.id === id);
if (m) return m;
// Fallback: bare model_id (backward compat with stored settings)
return App.models.find(m => m.baseModelId === id) || null;
},
settings: {
model: '',
stream: true,
@@ -211,8 +221,12 @@ async function fetchModels() {
App.models = (data.models || []).map(m => {
const isPreset = !!m.is_preset;
const id = isPreset ? (m.preset_id || m.id) : (m.model_id || m.id);
const baseModelId = m.model_id || m.id;
// Presets use preset_id; base models use configId:modelId to avoid
// collisions when the same model exists across team/personal/global providers
const id = isPreset
? (m.preset_id || m.id)
: (m.config_id ? `${m.config_id}:${baseModelId}` : baseModelId);
return {
id,
baseModelId,
@@ -225,6 +239,8 @@ async function fetchModels() {
presetScope: m.preset_scope || null,
presetAvatar: m.preset_avatar || null,
presetTeamName: m.preset_team_name || null,
source: m.source || (isPreset ? 'preset' : 'global'),
teamName: m.preset_team_name || null,
hidden: !isPreset && App.hiddenModels.has(baseModelId),
};
});
@@ -337,7 +353,7 @@ async function sendMessage() {
input.style.height = 'auto';
const selectedId = UI.getModelValue();
const modelInfo = App.models.find(m => m.id === selectedId);
const modelInfo = App.findModel(selectedId);
// For presets, send the base model ID; for regular models, send the model ID
const model = modelInfo?.baseModelId || selectedId;
const presetId = modelInfo?.isPreset ? modelInfo.presetId : null;
@@ -430,7 +446,7 @@ async function regenerateMessage(messageId) {
if (App.isGenerating || !App.currentChatId) return;
const selectedId = UI.getModelValue();
const modelInfo = App.models.find(m => m.id === selectedId);
const modelInfo = App.findModel(selectedId);
const model = modelInfo?.baseModelId || selectedId;
const presetId = modelInfo?.isPreset ? modelInfo.presetId : null;
@@ -501,7 +517,7 @@ async function submitEdit(messageId, newContent) {
if (!newContent.trim()) return;
const selectedId = UI.getModelValue();
const modelInfo = App.models.find(m => m.id === selectedId);
const modelInfo = App.findModel(selectedId);
const model = modelInfo?.baseModelId || selectedId;
const presetId = modelInfo?.isPreset ? modelInfo.presetId : null;
@@ -876,7 +892,7 @@ function initListeners() {
tab.addEventListener('click', () => UI.switchSettingsTab(tab.dataset.stab));
});
document.getElementById('settingsModel').addEventListener('change', function() {
const model = App.models.find(m => m.id === this.value);
const model = App.findModel(this.value);
const caps = model?.capabilities || lookupKnownCaps(this.value) || {};
const hint = document.getElementById('settingsMaxHint');
if (hint) {
@@ -1030,6 +1046,14 @@ function initListeners() {
UI.toast(this.checked ? 'Private providers required' : 'Private provider policy removed');
} catch (e) { UI.toast(e.message, 'error'); this.checked = !this.checked; }
});
document.getElementById('adminTeamAllowProviders')?.addEventListener('change', async function() {
try {
await API.adminUpdateTeam(UI._teamEditId, {
settings: JSON.stringify({ allow_team_providers: this.checked })
});
UI.toast(this.checked ? 'Team providers enabled' : 'Team providers disabled');
} catch (e) { UI.toast(e.message, 'error'); this.checked = !this.checked; }
});
document.getElementById('adminCancelMemberBtn')?.addEventListener('click', () => {
document.getElementById('adminAddMemberForm').style.display = 'none';
});
@@ -1100,6 +1124,86 @@ function initListeners() {
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',
};
document.getElementById('settingsTeamAddProviderBtn')?.addEventListener('click', () => {
const form = document.getElementById('settingsTeamAddProvider');
form.style.display = form.style.display === 'none' ? '' : 'none';
// Populate provider type dropdown
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'); }
});
// User — personal presets
var _userPresetForm = null;
document.getElementById('userAddPresetBtn')?.addEventListener('click', () => {
@@ -1876,6 +1980,37 @@ async function settingsDeleteTeamPreset(teamId, presetId, name) {
} catch (e) { UI.toast(e.message, 'error'); }
}
async function settingsToggleTeamProvider(teamId, providerId, currentlyActive) {
try {
await API.teamUpdateProvider(teamId, providerId, { is_active: !currentlyActive });
UI.toast(currentlyActive ? 'Provider deactivated' : 'Provider activated');
await UI.loadTeamManageProviders(teamId);
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
async function settingsDeleteTeamProvider(teamId, providerId, name) {
if (!confirm(`Delete team provider "${name}"? This will remove all models from this provider for your team.`)) return;
try {
await API.teamDeleteProvider(teamId, providerId);
UI.toast('Provider deleted');
await UI.loadTeamManageProviders(teamId);
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
function settingsEditTeamProvider(teamId, provId, name, endpoint, defaultModel, isPrivate) {
UI._editingTeamProviderId = provId;
document.getElementById('settingsTeamAddProvider').style.display = 'none';
const form = document.getElementById('settingsTeamEditProvider');
form.style.display = '';
document.getElementById('teamProviderEditName').value = name;
document.getElementById('teamProviderEditEndpoint').value = endpoint;
document.getElementById('teamProviderEditKey').value = '';
document.getElementById('teamProviderEditDefault').value = defaultModel;
document.getElementById('teamProviderEditPrivate').checked = isPrivate;
}
// ── Notes Panel ─────────────────────────────
var _editingNoteId = null;