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
This commit is contained in:
2026-05-20 13:55:35 +03:00
parent b017a0ec04
commit 201f2e3df5
3 changed files with 275 additions and 18 deletions

View File

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