Changeset 0.7.0 (#37)

This commit is contained in:
2026-02-21 12:43:33 +00:00
parent 925b70f98c
commit a95e246cd7
18 changed files with 1424 additions and 115 deletions

View File

@@ -173,32 +173,36 @@ async function fetchModels() {
try {
const data = await API.listEnabledModels();
App.models = (data.models || []).map(m => {
const id = m.model_id || m.id;
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 {
id,
name: m.display_name || id,
baseModelId,
name: m.display_name || baseModelId,
provider: m.provider_name || m.provider || '',
configId: m.config_id || null,
capabilities: resolveCapabilities(m.capabilities, id),
capabilities: resolveCapabilities(m.capabilities, baseModelId),
isPreset,
presetId: m.preset_id || null,
presetScope: m.preset_scope || null,
};
});
if (App.models.length === 0) {
const raw = await API.listAllModels();
App.models = (raw.models || []).map(m => {
const id = m.id || m.name;
return {
id,
name: id,
provider: m.owned_by || m.provider || '',
configId: m.config_id || null,
capabilities: resolveCapabilities(m.capabilities, id),
};
});
}
App.models.sort((a, b) => a.id.localeCompare(b.id));
console.log(`📋 Loaded ${App.models.length} models`);
// Sort: presets first (global before personal), then regular models
App.models.sort((a, b) => {
if (a.isPreset && !b.isPreset) return -1;
if (!a.isPreset && b.isPreset) return 1;
if (a.isPreset && b.isPreset) {
if (a.presetScope !== b.presetScope) {
return a.presetScope === 'global' ? -1 : 1;
}
}
return a.name.localeCompare(b.name);
});
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPreset).length} presets)`);
} catch (e) { console.warn('Model fetch failed:', e.message); }
UI.updateModelSelector();
UI.updateCapabilityBadges();
@@ -227,6 +231,11 @@ async function loadChats() {
async function selectChat(chatId) {
App.currentChatId = chatId;
UI.renderChatList();
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('collapsed');
const ov = document.getElementById('sidebarOverlay');
if (ov) ov.style.display = 'none';
}
const chat = App.chats.find(c => c.id === chatId);
if (!chat) return;
@@ -253,6 +262,11 @@ async function newChat() {
UI.showEmptyState();
UI.showRegenerate(false);
document.getElementById('messageInput').focus();
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('collapsed');
const ov = document.getElementById('sidebarOverlay');
if (ov) ov.style.display = 'none';
}
}
async function deleteChat(chatId) {
@@ -275,7 +289,11 @@ async function sendMessage() {
input.value = '';
input.style.height = 'auto';
const model = document.getElementById('modelSelect').value || App.settings.model;
const selectedId = UI.getModelValue();
const modelInfo = App.models.find(m => m.id === 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;
if (!App.currentChatId) {
try {
@@ -298,11 +316,10 @@ async function sendMessage() {
App.abortController = new AbortController();
UI.setGenerating(true);
const modelInfo = App.models.find(m => m.id === model);
try {
const resp = await API.streamCompletion(App.currentChatId, text, model, App.abortController.signal, modelInfo?.configId);
const assistantContent = await UI.streamResponse(resp, chat.messages);
chat.messages.push({ role: 'assistant', content: assistantContent, model, timestamp: new Date().toISOString() });
const resp = await API.streamCompletion(App.currentChatId, text, model, App.abortController.signal, modelInfo?.configId, presetId);
const assistantContent = await UI.streamResponse(resp, chat.messages, modelInfo?.name);
chat.messages.push({ role: 'assistant', content: assistantContent, model, modelName: modelInfo?.name || model, timestamp: new Date().toISOString() });
chat.messageCount = chat.messages.length;
UI.renderMessages(chat.messages);
UI.showRegenerate(true);
@@ -343,16 +360,18 @@ async function regenerate() {
const lastUser = chat.messages[chat.messages.length - 1];
if (lastUser.role !== 'user') return;
const model = document.getElementById('modelSelect').value || App.settings.model;
const modelInfo = App.models.find(m => m.id === model);
const selectedId = UI.getModelValue();
const modelInfo = App.models.find(m => m.id === selectedId);
const model = modelInfo?.baseModelId || selectedId;
const presetId = modelInfo?.isPreset ? modelInfo.presetId : null;
App.isGenerating = true;
App.abortController = new AbortController();
UI.setGenerating(true);
try {
const resp = await API.streamCompletion(App.currentChatId, lastUser.content, model, App.abortController.signal, modelInfo?.configId);
const content = await UI.streamResponse(resp, chat.messages);
chat.messages.push({ role: 'assistant', content, model, timestamp: new Date().toISOString() });
const resp = await API.streamCompletion(App.currentChatId, lastUser.content, model, App.abortController.signal, modelInfo?.configId, presetId);
const content = await UI.streamResponse(resp, chat.messages, modelInfo?.name);
chat.messages.push({ role: 'assistant', content, model, modelName: modelInfo?.name || model, timestamp: new Date().toISOString() });
UI.renderMessages(chat.messages);
UI.showRegenerate(true);
} catch (e) {
@@ -494,10 +513,16 @@ function initListeners() {
document.getElementById('stopBtn').addEventListener('click', stopGeneration);
document.getElementById('regenerateBtn').addEventListener('click', regenerate);
// Model selector
document.getElementById('modelSelect').addEventListener('change', function() {
if (this.value) { App.settings.model = this.value; saveSettings(); }
UI.updateCapabilityBadges();
// Model selector (custom dropdown)
UI.initModelDropdown();
UI.initAppearance();
// Mobile hamburger
document.getElementById('mobileMenuBtn')?.addEventListener('click', UI.toggleSidebar);
document.getElementById('sidebarOverlay')?.addEventListener('click', () => {
document.getElementById('sidebar').classList.add('collapsed');
document.getElementById('sidebarOverlay').style.display = 'none';
localStorage.setItem('sb_sidebar', '1');
});
document.getElementById('fetchModelsBtn')?.addEventListener('click', async () => {
await fetchModels();
@@ -557,15 +582,47 @@ function initListeners() {
// Admin — providers
document.getElementById('adminAddProviderBtn')?.addEventListener('click', () => {
_editingProviderId = null;
document.getElementById('adminCreateProvBtn').textContent = 'Save';
document.getElementById('adminProvKey').placeholder = 'sk-...';
document.getElementById('adminProvName').value = '';
document.getElementById('adminProvEndpoint').value = '';
document.getElementById('adminProvKey').value = '';
document.getElementById('adminProvModel').value = '';
const f = document.getElementById('adminAddProviderForm');
f.style.display = f.style.display === 'none' ? '' : 'none';
});
document.getElementById('adminCancelProvBtn')?.addEventListener('click', () => { document.getElementById('adminAddProviderForm').style.display = 'none'; });
document.getElementById('adminCancelProvBtn')?.addEventListener('click', () => {
document.getElementById('adminAddProviderForm').style.display = 'none';
_editingProviderId = null;
document.getElementById('adminCreateProvBtn').textContent = 'Save';
document.getElementById('adminProvKey').placeholder = 'sk-...';
});
document.getElementById('adminCreateProvBtn')?.addEventListener('click', createGlobalProvider);
// Admin — models
document.getElementById('adminFetchModelsBtn')?.addEventListener('click', fetchAdminModels);
// Admin — presets
document.getElementById('adminAddPresetBtn')?.addEventListener('click', () => {
_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 = '';
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';
});
document.getElementById('adminCreatePresetBtn')?.addEventListener('click', createAdminPreset);
// Admin — banner controls
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none';
@@ -740,6 +797,23 @@ async function deleteGlobalProvider(id) {
try { await API.adminDeleteGlobalConfig(id); UI.toast('Provider deleted', 'success'); await UI.loadAdminProviders(); }
catch (e) { UI.toast(e.message, 'error'); }
}
var _editingProviderId = null;
function editGlobalProvider(id) {
const c = UI._providerCache?.[id];
if (!c) return;
_editingProviderId = id;
const form = document.getElementById('adminAddProviderForm');
form.style.display = '';
document.getElementById('adminProvName').value = c.name || '';
document.getElementById('adminProvType').value = c.provider || 'openai';
document.getElementById('adminProvEndpoint').value = c.endpoint || '';
document.getElementById('adminProvKey').value = '';
document.getElementById('adminProvKey').placeholder = c.has_key ? '(unchanged)' : 'sk-...';
document.getElementById('adminProvModel').value = c.model_default || '';
document.getElementById('adminCreateProvBtn').textContent = 'Update';
}
async function createGlobalProvider() {
try {
const name = document.getElementById('adminProvName').value.trim();
@@ -747,10 +821,26 @@ async function createGlobalProvider() {
const ep = document.getElementById('adminProvEndpoint').value.trim();
const key = document.getElementById('adminProvKey').value;
const model = document.getElementById('adminProvModel').value.trim();
if (!name || !ep || !key) { UI.toast('Name, endpoint, and API key required', 'warning'); return; }
await API.adminCreateGlobalConfig(name, prov, ep, key, model);
if (_editingProviderId) {
// Update mode
const updates = {};
if (name) updates.name = name;
if (ep) updates.endpoint = ep;
if (key) updates.api_key = key;
updates.model_default = model;
await API.adminUpdateGlobalConfig(_editingProviderId, updates);
UI.toast('Provider updated', 'success');
} else {
// Create mode
if (!name || !ep || !key) { UI.toast('Name, endpoint, and API key required', 'warning'); return; }
await API.adminCreateGlobalConfig(name, prov, ep, key, model);
UI.toast('Provider added', 'success');
}
_editingProviderId = null;
document.getElementById('adminAddProviderForm').style.display = 'none';
UI.toast('Provider added', 'success');
document.getElementById('adminCreateProvBtn').textContent = 'Save';
document.getElementById('adminProvKey').placeholder = 'sk-...';
await UI.loadAdminProviders();
} catch (e) { UI.toast(e.message, 'error'); }
}
@@ -781,6 +871,93 @@ async function bulkToggleModels(enabled) {
} catch (e) { UI.toast(e.message, 'error'); hint.textContent = 'Failed'; }
}
// ── Admin Presets ────────────────────────────
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 || '';
document.getElementById('adminCreatePresetBtn').textContent = '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; }
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,
};
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;
try {
if (_editingPresetId) {
await API.adminUpdatePreset(_editingPresetId, preset);
UI.toast('Preset updated', 'success');
} else {
await API.adminCreatePreset(preset);
UI.toast('Preset created', 'success');
}
_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 = '';
await UI.loadAdminPresets();
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
async function toggleAdminPreset(id, active) {
try {
const el = _adminScroll(), pos = el?.scrollTop || 0;
await API.adminUpdatePreset(id, { is_active: active });
await UI.loadAdminPresets(true);
_restoreScroll(el, pos);
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
async function deleteAdminPreset(id, name) {
if (!confirm(`Delete preset "${name}"?`)) return;
try {
await API.adminDeletePreset(id);
UI.toast('Preset deleted', 'success');
await UI.loadAdminPresets();
await fetchModels();
} catch (e) { UI.toast(e.message, 'error'); }
}
// ── Banners ──────────────────────────────────
async function initBanners() {