Changeset 0.5.0 (#35)

This commit is contained in:
2026-02-19 15:03:20 +00:00
parent a93a6b9635
commit 30d0c11219
65 changed files with 5345 additions and 8070 deletions

View File

@@ -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');
}