From 201f2e3df5179cbc64d0f330bf78af350214af29 Mon Sep 17 00:00:00 2001 From: koko210Serve Date: Wed, 20 May 2026 13:55:35 +0300 Subject: [PATCH] feat(ui): add model selection UI to LLM Settings tab - Three dropdowns for Regular Miku, Evil Miku, Japanese Mode models - GPU availability badges (Both GPUs / NVIDIA Only / AMD Only) - Refresh Models + Refresh Status buttons - Load models on tab switch with defensive checks - Bump cache-busting version for all JS files - Remove redundant Current Status section --- bot/static/index.html | 76 ++++++++++---- bot/static/js/core.js | 7 ++ bot/static/js/servers.js | 210 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 275 insertions(+), 18 deletions(-) diff --git a/bot/static/index.html b/bot/static/index.html index 815c75d..e44f742 100644 --- a/bot/static/index.html +++ b/bot/static/index.html @@ -663,15 +663,55 @@ - -
-

📊 Current Status

-
-

Language Mode: English

-

Active Model: llama3.1

-

Available Languages: English, 日本語 (Japanese)

+ +
+

🎛️ Model Selection

+

Choose which model each persona uses. Changes take effect immediately and persist across bot restarts.

+ +
+
Loading available models...
+ + + +
+ + +
+
+ + -
@@ -1375,15 +1415,15 @@
- - - - - - - - - - + + + + + + + + + + diff --git a/bot/static/js/core.js b/bot/static/js/core.js index aadb863..271c6ca 100644 --- a/bot/static/js/core.js +++ b/bot/static/js/core.js @@ -168,6 +168,13 @@ function switchTab(tabId) { showTabLoading('tab6'); loadAutonomousStats().finally(() => hideTabLoading('tab6')); } + if (tabId === 'tab4') { + if (typeof loadAvailableModels === 'function') { + loadAvailableModels(); + } else { + console.warn('loadAvailableModels not available yet (servers.js may not be loaded)'); + } + } if (tabId === 'tab9') { console.log('🧠 Refreshing memory stats for Memories tab'); showTabLoading('tab9'); diff --git a/bot/static/js/servers.js b/bot/static/js/servers.js index ad01bcc..1c89472 100644 --- a/bot/static/js/servers.js +++ b/bot/static/js/servers.js @@ -745,3 +745,213 @@ async function toggleLanguageMode() { showNotification('Failed to toggle language mode', 'error'); } } + +// ===== Model Selection Functions ===== + +let availableModelsData = null; + +async function loadAvailableModels() { + console.log('📋 loadAvailableModels() called'); + try { + const loadingEl = document.getElementById('model-selection-loading'); + const controlsEl = document.getElementById('model-selection-controls'); + const infoEl = document.getElementById('model-selection-info'); + const infoTextEl = document.getElementById('model-selection-info-text'); + + if (loadingEl) loadingEl.style.display = 'block'; + if (controlsEl) controlsEl.style.display = 'none'; + if (infoEl) infoEl.style.display = 'none'; + + console.log('📋 Fetching /models/available...'); + const result = await apiCall('/models/available'); + console.log('📋 /models/available response:', result); + availableModelsData = result; + + if (loadingEl) loadingEl.style.display = 'none'; + if (controlsEl) controlsEl.style.display = 'block'; + + if (!result.success) { + showNotification('Failed to load models: ' + (result.error || 'Unknown error'), 'error'); + return; + } + + // Populate dropdowns + const allModels = result.all || []; + const gpuMap = result.gpu_map || {}; + console.log('📋 Populating dropdowns with models:', allModels); + + const personas = [ + { id: 'regular', selectId: 'model-regular', badgeId: 'model-regular-badge' }, + { id: 'evil', selectId: 'model-evil', badgeId: 'model-evil-badge' }, + { id: 'japanese', selectId: 'model-japanese', badgeId: 'model-japanese-badge' }, + ]; + + for (const p of personas) { + const select = document.getElementById(p.selectId); + if (!select) continue; + + // Save current selection + const currentVal = select.value; + + select.innerHTML = ''; + for (const model of allModels) { + const option = document.createElement('option'); + option.value = model; + option.textContent = model; + select.appendChild(option); + } + + // Restore selection if still valid + if (currentVal && allModels.includes(currentVal)) { + select.value = currentVal; + } + } + + // Update badges for current selections + updateModelBadges(allModels, gpuMap); + + // Show info about source + if (infoEl && infoTextEl) { + if (result.source === 'fallback') { + infoTextEl.textContent = '⚠️ Could not reach llama-swap containers. Showing known models from config.'; + infoEl.style.display = 'block'; + } else { + infoEl.style.display = 'none'; + } + } + + // Load current status to sync dropdowns + await refreshModelStatus(); + + console.log('Available models loaded:', result); + } catch (error) { + console.error('Failed to load available models:', error); + const loadingEl = document.getElementById('model-selection-loading'); + if (loadingEl) loadingEl.textContent = '❌ Failed to load models. Click "Refresh Models" to retry.'; + showNotification('Failed to load available models', 'error'); + } +} + +function updateModelBadges(allModels, gpuMap) { + const personas = [ + { selectId: 'model-regular', badgeId: 'model-regular-badge' }, + { selectId: 'model-evil', badgeId: 'model-evil-badge' }, + { selectId: 'model-japanese', badgeId: 'model-japanese-badge' }, + ]; + + for (const p of personas) { + const select = document.getElementById(p.selectId); + const badge = document.getElementById(p.badgeId); + if (!select || !badge) continue; + + const model = select.value; + const gpus = gpuMap[model]; + + if (!gpus) { + badge.textContent = ''; + badge.style.display = 'none'; + } else if (gpus.length === 2 || (gpus.includes('nvidia') && gpus.includes('amd'))) { + badge.textContent = '✅ Both GPUs'; + badge.style.background = '#1b5e20'; + badge.style.color = '#a5d6a7'; + badge.style.display = 'inline'; + } else if (gpus.includes('nvidia')) { + badge.textContent = '⚠️ NVIDIA Only'; + badge.style.background = '#e65100'; + badge.style.color = '#ffcc80'; + badge.style.display = 'inline'; + } else if (gpus.includes('amd')) { + badge.textContent = '⚠️ AMD Only'; + badge.style.background = '#e65100'; + badge.style.color = '#ffcc80'; + badge.style.display = 'inline'; + } + } +} + +async function selectModel(persona) { + console.log(`📋 selectModel('${persona}') called`); + try { + const selectId = 'model-' + persona; + const select = document.getElementById(selectId); + if (!select) { + console.warn(`📋 select element #${selectId} not found`); + return; + } + + const model = select.value; + if (!model) { + showNotification('Please select a model first', 'error'); + return; + } + + console.log(`📋 Setting ${persona} model to '${model}'...`); + const result = await apiCall('/models/select', 'POST', { + persona: persona, + model: model, + }); + + console.log(`📋 /models/select response:`, result); + + if (result.success) { + showNotification(`${persona.charAt(0).toUpperCase() + persona.slice(1)} model set to '${model}'`, 'success'); + // Update badges + if (availableModelsData) { + updateModelBadges(availableModelsData.all || [], availableModelsData.gpu_map || {}); + } + // Refresh language status display (active model may have changed) + refreshLanguageStatus(); + } else { + showNotification('Failed to set model: ' + (result.error || 'Unknown error'), 'error'); + } + } catch (error) { + console.error('Failed to select model:', error); + showNotification('Failed to set model: ' + error.message, 'error'); + } +} + +async function refreshModelStatus() { + console.log('📋 refreshModelStatus() called'); + try { + const result = await apiCall('/models/status'); + console.log('📋 /models/status response:', result); + if (!result.success) return; + + // Sync dropdowns with current globals + const personas = [ + { id: 'regular', selectId: 'model-regular' }, + { id: 'evil', selectId: 'model-evil' }, + { id: 'japanese', selectId: 'model-japanese' }, + ]; + + for (const p of personas) { + const select = document.getElementById(p.selectId); + if (!select) continue; + const currentModel = result[p.id]; + // Check if this value exists in the dropdown + let found = false; + for (const option of select.options) { + if (option.value === currentModel) { + select.value = currentModel; + found = true; + break; + } + } + if (!found && currentModel) { + // Add it if it doesn't exist (e.g., a model that wasn't in the API response) + const option = document.createElement('option'); + option.value = currentModel; + option.textContent = currentModel; + select.appendChild(option); + select.value = currentModel; + } + } + + // Update badges + if (availableModelsData) { + updateModelBadges(availableModelsData.all || [], availableModelsData.gpu_map || {}); + } + } catch (error) { + console.error('Failed to refresh model status:', error); + } +}