Changeset 0.5.0 (#35)
This commit is contained in:
138
src/js/app.js
138
src/js/app.js
@@ -1,5 +1,5 @@
|
||||
// ==========================================
|
||||
// Chat Switchboard – Application (v0.5.2)
|
||||
// Chat Switchboard – Application (v0.5.4)
|
||||
// ==========================================
|
||||
|
||||
const App = {
|
||||
@@ -14,7 +14,7 @@ const App = {
|
||||
stream: true,
|
||||
showThinking: true,
|
||||
systemPrompt: '',
|
||||
maxTokens: 4096,
|
||||
maxTokens: 0, // 0 = auto from model capabilities
|
||||
temperature: 0.7,
|
||||
},
|
||||
};
|
||||
@@ -64,6 +64,14 @@ async function startApp() {
|
||||
UI.updateUser();
|
||||
UI.showAdminButton(API.isAdmin);
|
||||
initListeners();
|
||||
|
||||
// Connect EventBus WebSocket (non-blocking, graceful if /ws not available yet)
|
||||
try {
|
||||
Events.connect('/ws');
|
||||
} catch (e) {
|
||||
console.warn('EventBus WebSocket not available:', e.message);
|
||||
}
|
||||
|
||||
console.log('✅ Chat Switchboard ready');
|
||||
}
|
||||
|
||||
@@ -94,28 +102,104 @@ async function saveSettings() {
|
||||
|
||||
// ── Models ───────────────────────────────────
|
||||
|
||||
// Client-side known model capabilities — used when backend hasn't synced yet.
|
||||
// Mirrors server/providers/capabilities.go knownModels table.
|
||||
const KNOWN_MODELS = {
|
||||
'claude-opus-4': { streaming:true, tool_calling:true, vision:true, thinking:true, max_context:200000, max_output_tokens:32000 },
|
||||
'claude-sonnet-4': { streaming:true, tool_calling:true, vision:true, thinking:true, max_context:200000, max_output_tokens:16000 },
|
||||
'claude-3-5-sonnet': { streaming:true, tool_calling:true, vision:true, max_context:200000, max_output_tokens:8192 },
|
||||
'claude-3-5-haiku': { streaming:true, tool_calling:true, vision:true, max_context:200000, max_output_tokens:8192 },
|
||||
'claude-3-opus': { streaming:true, tool_calling:true, vision:true, max_context:200000, max_output_tokens:4096 },
|
||||
'claude-3-haiku': { streaming:true, tool_calling:true, vision:true, max_context:200000, max_output_tokens:4096 },
|
||||
'gpt-4o': { streaming:true, tool_calling:true, vision:true, max_context:128000, max_output_tokens:16384 },
|
||||
'gpt-4o-mini': { streaming:true, tool_calling:true, vision:true, max_context:128000, max_output_tokens:16384 },
|
||||
'gpt-4-turbo': { streaming:true, tool_calling:true, vision:true, max_context:128000, max_output_tokens:4096 },
|
||||
'o1': { streaming:true, reasoning:true, max_context:200000, max_output_tokens:100000 },
|
||||
'o3': { streaming:true, tool_calling:true, reasoning:true, max_context:200000, max_output_tokens:100000 },
|
||||
'o3-mini': { streaming:true, tool_calling:true, reasoning:true, max_context:200000, max_output_tokens:65536 },
|
||||
'o4-mini': { streaming:true, tool_calling:true, reasoning:true, max_context:200000, max_output_tokens:100000 },
|
||||
'gemini-2.5-pro': { streaming:true, tool_calling:true, vision:true, thinking:true, max_context:1048576, max_output_tokens:65536 },
|
||||
'gemini-2.5-flash': { streaming:true, tool_calling:true, vision:true, thinking:true, max_context:1048576, max_output_tokens:65536 },
|
||||
'gemini-2.0-flash': { streaming:true, tool_calling:true, vision:true, max_context:1048576, max_output_tokens:8192 },
|
||||
'deepseek-r1': { streaming:true, reasoning:true, max_context:65536, max_output_tokens:8192 },
|
||||
'deepseek-v3': { streaming:true, tool_calling:true, max_context:65536, max_output_tokens:8192 },
|
||||
'deepseek-chat': { streaming:true, tool_calling:true, max_context:65536, max_output_tokens:8192 },
|
||||
'llama-3.1-405b': { streaming:true, tool_calling:true, max_context:131072, max_output_tokens:4096 },
|
||||
'llama-3.1-70b': { streaming:true, tool_calling:true, max_context:131072, max_output_tokens:4096 },
|
||||
'llama-3.3-70b': { streaming:true, tool_calling:true, max_context:131072, max_output_tokens:4096 },
|
||||
'llama-4-maverick': { streaming:true, tool_calling:true, vision:true, max_context:1048576, max_output_tokens:16384 },
|
||||
'llama-4-scout': { streaming:true, tool_calling:true, vision:true, max_context:524288, max_output_tokens:16384 },
|
||||
'mistral-large': { streaming:true, tool_calling:true, max_context:131072, max_output_tokens:8192 },
|
||||
'codestral': { streaming:true, tool_calling:true, code_optimized:true, max_context:262144, max_output_tokens:8192 },
|
||||
'qwen-2.5-72b': { streaming:true, tool_calling:true, max_context:131072, max_output_tokens:8192 },
|
||||
'qwq-32b': { streaming:true, reasoning:true, max_context:131072, max_output_tokens:8192 },
|
||||
};
|
||||
|
||||
// Look up client-side capabilities by model ID with prefix matching
|
||||
function lookupKnownCaps(modelId) {
|
||||
const id = modelId.toLowerCase().replace(/^[^/]+\//, ''); // strip provider prefix
|
||||
// Exact match
|
||||
if (KNOWN_MODELS[id]) return { ...KNOWN_MODELS[id] };
|
||||
// Prefix match (longest wins)
|
||||
let best = null, bestLen = 0;
|
||||
for (const key of Object.keys(KNOWN_MODELS)) {
|
||||
if (id.startsWith(key) && key.length > bestLen) {
|
||||
best = key; bestLen = key.length;
|
||||
}
|
||||
}
|
||||
return best ? { ...KNOWN_MODELS[best] } : null;
|
||||
}
|
||||
|
||||
// Merge: backend/provider caps are authoritative, client-side fills gaps only
|
||||
function resolveCapabilities(backendCaps, modelId) {
|
||||
const known = lookupKnownCaps(modelId) || {};
|
||||
if (!backendCaps || Object.keys(backendCaps).length === 0) return known;
|
||||
|
||||
// Backend is authoritative — start with it
|
||||
const caps = { ...backendCaps };
|
||||
|
||||
// Fill gaps (fields backend didn't report) from known table
|
||||
for (const [k, v] of Object.entries(known)) {
|
||||
if (caps[k] === undefined || caps[k] === null) {
|
||||
caps[k] = v;
|
||||
}
|
||||
}
|
||||
return caps;
|
||||
}
|
||||
|
||||
async function fetchModels() {
|
||||
try {
|
||||
const data = await API.listEnabledModels();
|
||||
App.models = (data.models || []).map(m => ({
|
||||
id: m.model_id || m.id,
|
||||
provider: m.provider_name || m.provider || '',
|
||||
configId: m.config_id || null
|
||||
}));
|
||||
App.models = (data.models || []).map(m => {
|
||||
const id = m.model_id || m.id;
|
||||
return {
|
||||
id,
|
||||
name: m.display_name || id,
|
||||
provider: m.provider_name || m.provider || '',
|
||||
configId: m.config_id || null,
|
||||
capabilities: resolveCapabilities(m.capabilities, id),
|
||||
};
|
||||
});
|
||||
|
||||
if (App.models.length === 0) {
|
||||
const raw = await API.listAllModels();
|
||||
App.models = (raw.models || []).map(m => ({
|
||||
id: m.id || m.name,
|
||||
provider: m.owned_by || m.provider || '',
|
||||
configId: m.config_id || null
|
||||
}));
|
||||
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`);
|
||||
} catch (e) { console.warn('Model fetch failed:', e.message); }
|
||||
UI.updateModelSelector();
|
||||
UI.updateCapabilityBadges();
|
||||
}
|
||||
|
||||
// ── Chat Management ──────────────────────────
|
||||
@@ -216,6 +300,7 @@ async function sendMessage() {
|
||||
const assistantContent = await UI.streamResponse(resp, chat.messages);
|
||||
chat.messages.push({ role: 'assistant', content: assistantContent, model, timestamp: new Date().toISOString() });
|
||||
chat.messageCount = chat.messages.length;
|
||||
UI.renderMessages(chat.messages);
|
||||
UI.showRegenerate(true);
|
||||
} catch (e) {
|
||||
if (e.name === 'AbortError') {
|
||||
@@ -263,6 +348,7 @@ async function regenerate() {
|
||||
const resp = await API.streamCompletion(App.currentChatId, lastUser.content, model, App.abortController.signal);
|
||||
const content = await UI.streamResponse(resp, chat.messages);
|
||||
chat.messages.push({ role: 'assistant', content, model, timestamp: new Date().toISOString() });
|
||||
UI.renderMessages(chat.messages);
|
||||
UI.showRegenerate(true);
|
||||
} catch (e) {
|
||||
if (e.name !== 'AbortError') {
|
||||
@@ -317,6 +403,8 @@ async function handleRegister() {
|
||||
|
||||
async function handleLogout() {
|
||||
if (!confirm('Sign out?')) return;
|
||||
Events.disconnect();
|
||||
Events.clear();
|
||||
await API.logout();
|
||||
App.chats = [];
|
||||
App.currentChatId = null;
|
||||
@@ -375,6 +463,7 @@ function initListeners() {
|
||||
// Model selector
|
||||
document.getElementById('modelSelect').addEventListener('change', function() {
|
||||
if (this.value) { App.settings.model = this.value; saveSettings(); }
|
||||
UI.updateCapabilityBadges();
|
||||
});
|
||||
document.getElementById('fetchModelsBtn').addEventListener('click', async () => {
|
||||
await fetchModels();
|
||||
@@ -384,6 +473,16 @@ function initListeners() {
|
||||
// Settings modal
|
||||
document.getElementById('settingsCloseBtn').addEventListener('click', UI.closeSettings);
|
||||
document.getElementById('settingsSaveBtn').addEventListener('click', handleSaveSettings);
|
||||
document.getElementById('settingsModel').addEventListener('change', function() {
|
||||
const model = App.models.find(m => m.id === this.value);
|
||||
const caps = model?.capabilities || lookupKnownCaps(this.value) || {};
|
||||
const hint = document.getElementById('settingsMaxHint');
|
||||
if (hint) {
|
||||
hint.textContent = caps.max_output_tokens > 0
|
||||
? `(model max: ${(caps.max_output_tokens / 1000).toFixed(0)}K)`
|
||||
: '';
|
||||
}
|
||||
});
|
||||
document.getElementById('profileChangePwBtn').addEventListener('click', () => {
|
||||
document.getElementById('profileChangePwForm').style.display = '';
|
||||
});
|
||||
@@ -391,6 +490,18 @@ function initListeners() {
|
||||
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] || '';
|
||||
}
|
||||
});
|
||||
|
||||
// Admin modal
|
||||
document.getElementById('adminCloseBtn').addEventListener('click', UI.closeAdmin);
|
||||
@@ -427,11 +538,12 @@ function initListeners() {
|
||||
function handleSaveSettings() {
|
||||
App.settings.model = document.getElementById('settingsModel').value || App.settings.model;
|
||||
App.settings.systemPrompt = document.getElementById('settingsSystemPrompt').value.trim();
|
||||
App.settings.maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 4096;
|
||||
App.settings.maxTokens = parseInt(document.getElementById('settingsMaxTokens').value) || 0; // 0 = auto
|
||||
App.settings.temperature = parseFloat(document.getElementById('settingsTemp').value) || 0.7;
|
||||
App.settings.showThinking = document.getElementById('settingsThinking').checked;
|
||||
saveSettings();
|
||||
UI.updateModelSelector();
|
||||
UI.updateCapabilityBadges();
|
||||
UI.closeSettings();
|
||||
UI.toast('Settings saved', 'success');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user