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:
@@ -663,15 +663,55 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language Mode Status Section -->
|
||||
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px;">
|
||||
<h4 style="margin-top: 0;">📊 Current Status</h4>
|
||||
<div id="language-status-display" style="background: #1a1a1a; padding: 1rem; border-radius: 4px; font-family: monospace; font-size: 0.9rem;">
|
||||
<p style="margin: 0.5rem 0;"><strong>Language Mode:</strong> <span id="status-language">English</span></p>
|
||||
<p style="margin: 0.5rem 0;"><strong>Active Model:</strong> <span id="status-model">llama3.1</span></p>
|
||||
<p style="margin: 0.5rem 0;"><strong>Available Languages:</strong> English, 日本語 (Japanese)</p>
|
||||
<!-- Model Selection Section -->
|
||||
<div style="margin-bottom: 1.5rem; padding: 1rem; background: #2a2a2a; border-radius: 4px; border: 2px solid #7c4dff;">
|
||||
<h4 style="margin-top: 0; color: #b388ff;">🎛️ Model Selection</h4>
|
||||
<p style="margin: 0.5rem 0; color: #aaa;">Choose which model each persona uses. Changes take effect immediately and persist across bot restarts.</p>
|
||||
|
||||
<div style="margin: 1rem 0;">
|
||||
<div id="model-selection-loading" style="color: #aaa;">Loading available models...</div>
|
||||
|
||||
<div id="model-selection-controls" style="display: none;">
|
||||
<!-- Regular Miku -->
|
||||
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #69f0ae;">
|
||||
<label for="model-regular" style="font-weight: bold; color: #69f0ae;">🎤 Regular Miku</label>
|
||||
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<select id="model-regular" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||
<span id="model-regular-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||
<button onclick="selectModel('regular')" style="background: #69f0ae; color: #000; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Evil Miku -->
|
||||
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #ff5252;">
|
||||
<label for="model-evil" style="font-weight: bold; color: #ff5252;">😈 Evil Miku</label>
|
||||
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<select id="model-evil" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||
<span id="model-evil-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||
<button onclick="selectModel('evil')" style="background: #ff5252; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Japanese Mode -->
|
||||
<div style="margin-bottom: 1rem; padding: 0.8rem; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #40c4ff;">
|
||||
<label for="model-japanese" style="font-weight: bold; color: #40c4ff;">🗾 Japanese Mode</label>
|
||||
<div style="margin-top: 0.5rem; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<select id="model-japanese" style="flex: 1; min-width: 200px; padding: 0.5rem; background: #333; color: #fff; border: 1px solid #555; border-radius: 4px;"></select>
|
||||
<span id="model-japanese-badge" style="font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px;"></span>
|
||||
<button onclick="selectModel('japanese')" style="background: #40c4ff; color: #000; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||
<button onclick="loadAvailableModels()" style="background: #7c4dff; color: #fff; padding: 0.5rem 1rem; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">🔄 Refresh Models</button>
|
||||
<button onclick="refreshModelStatus()" style="background: #333; color: #fff; padding: 0.5rem 1rem; border: 1px solid #555; border-radius: 4px; cursor: pointer;">📊 Refresh Status</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="model-selection-info" style="margin-top: 0.5rem; padding: 0.5rem; background: #1a1a1a; border-radius: 4px; font-size: 0.8rem; color: #888; display: none;">
|
||||
<span id="model-selection-info-text"></span>
|
||||
</div>
|
||||
<button onclick="refreshLanguageStatus()" style="margin-top: 1rem;">🔄 Refresh Status</button>
|
||||
</div>
|
||||
|
||||
<!-- Information Section -->
|
||||
@@ -1375,15 +1415,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/core.js?v=20260502"></script>
|
||||
<script src="/static/js/servers.js?v=20260502"></script>
|
||||
<script src="/static/js/modes.js?v=20260502"></script>
|
||||
<script src="/static/js/actions.js?v=20260502"></script>
|
||||
<script src="/static/js/image-gen.js?v=20260502"></script>
|
||||
<script src="/static/js/status.js?v=20260502"></script>
|
||||
<script src="/static/js/dm.js?v=20260502"></script>
|
||||
<script src="/static/js/chat.js?v=20260502"></script>
|
||||
<script src="/static/js/memories.js?v=20260502"></script>
|
||||
<script src="/static/js/profile.js?v=20260502"></script>
|
||||
<script src="/static/js/core.js?v=20260520"></script>
|
||||
<script src="/static/js/servers.js?v=20260520"></script>
|
||||
<script src="/static/js/modes.js?v=20260520"></script>
|
||||
<script src="/static/js/actions.js?v=20260520"></script>
|
||||
<script src="/static/js/image-gen.js?v=20260520"></script>
|
||||
<script src="/static/js/status.js?v=20260520"></script>
|
||||
<script src="/static/js/dm.js?v=20260520"></script>
|
||||
<script src="/static/js/chat.js?v=20260520"></script>
|
||||
<script src="/static/js/memories.js?v=20260520"></script>
|
||||
<script src="/static/js/profile.js?v=20260520"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user