Changeset 0.4.3 (#32)
This commit is contained in:
214
src/js/admin.js
214
src/js/admin.js
@@ -40,6 +40,21 @@ function initAdminListeners() {
|
||||
document.getElementById('adminResetCancelBtn').addEventListener('click', closeResetDialog);
|
||||
document.getElementById('adminResetConfirmBtn').addEventListener('click', handleResetPassword);
|
||||
|
||||
// Admin providers
|
||||
document.getElementById('adminProviderShowAddBtn').addEventListener('click', () => {
|
||||
document.getElementById('adminProviderAddForm').style.display = '';
|
||||
document.getElementById('adminProviderShowAddBtn').style.display = 'none';
|
||||
});
|
||||
document.getElementById('adminProviderCancelBtn').addEventListener('click', () => {
|
||||
document.getElementById('adminProviderAddForm').style.display = 'none';
|
||||
document.getElementById('adminProviderShowAddBtn').style.display = '';
|
||||
clearAdminProviderForm();
|
||||
});
|
||||
document.getElementById('adminProviderCreateBtn').addEventListener('click', handleCreateAdminProvider);
|
||||
|
||||
// Admin models
|
||||
document.getElementById('adminFetchModelsBtn').addEventListener('click', handleAdminFetchModels);
|
||||
|
||||
document.getElementById('adminResetNewPw').addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') handleResetPassword();
|
||||
});
|
||||
@@ -50,10 +65,14 @@ function switchAdminTab(tab) {
|
||||
t.classList.toggle('active', t.dataset.tab === tab);
|
||||
});
|
||||
document.getElementById('adminUsersTab').style.display = tab === 'users' ? '' : 'none';
|
||||
document.getElementById('adminModelsTab').style.display = tab === 'models' ? '' : 'none';
|
||||
document.getElementById('adminProvidersTab').style.display = tab === 'providers' ? '' : 'none';
|
||||
document.getElementById('adminSettingsTab').style.display = tab === 'settings' ? '' : 'none';
|
||||
document.getElementById('adminStatsTab').style.display = tab === 'stats' ? '' : 'none';
|
||||
document.getElementById('adminResetDialog').style.display = 'none';
|
||||
|
||||
if (tab === 'models') loadAdminModels();
|
||||
if (tab === 'providers') loadAdminProviders();
|
||||
if (tab === 'settings') loadAdminSettings();
|
||||
if (tab === 'stats') loadAdminStats();
|
||||
}
|
||||
@@ -271,3 +290,198 @@ function escapeHtml(str) {
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// ── Admin Models ────────────────────────────
|
||||
|
||||
const CAPS = ['tool', 'thinking', 'vision', 'code'];
|
||||
|
||||
async function loadAdminModels() {
|
||||
const container = document.getElementById('adminModelList');
|
||||
container.innerHTML = '<div class="admin-loading">Loading models...</div>';
|
||||
|
||||
try {
|
||||
const data = await Backend.adminListModels();
|
||||
const models = data.models || [];
|
||||
|
||||
if (models.length === 0) {
|
||||
container.innerHTML = '<div class="admin-empty">No models configured. Add a provider first, then click "Fetch Models".</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by provider
|
||||
const grouped = {};
|
||||
for (const m of models) {
|
||||
const key = m.provider_name || 'Unknown';
|
||||
if (!grouped[key]) grouped[key] = [];
|
||||
grouped[key].push(m);
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const [provider, provModels] of Object.entries(grouped)) {
|
||||
html += '<div class="admin-model-group">';
|
||||
html += '<div class="admin-model-group-header">' + escapeHtml(provider) + '</div>';
|
||||
for (const m of provModels) {
|
||||
const caps = m.capabilities || {};
|
||||
const name = m.display_name || m.model_id;
|
||||
html += '<div class="admin-model-row">';
|
||||
html += ' <div class="admin-model-toggle">';
|
||||
html += ' <label class="toggle-label-compact">';
|
||||
html += ' <input type="checkbox" ' + (m.is_enabled ? 'checked' : '') +
|
||||
' onchange="toggleModelEnabled(\'' + m.id + '\', this.checked)">';
|
||||
html += ' </label>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="admin-model-info">';
|
||||
html += ' <span class="admin-model-name">' + escapeHtml(name) + '</span>';
|
||||
html += ' <span class="admin-model-id">' + escapeHtml(m.model_id) + '</span>';
|
||||
html += ' </div>';
|
||||
html += ' <div class="admin-model-caps">';
|
||||
for (const cap of CAPS) {
|
||||
const active = caps[cap] === true;
|
||||
html += ' <button class="cap-badge ' + (active ? 'cap-active' : 'cap-inactive') + '"' +
|
||||
' onclick="toggleModelCap(\'' + m.id + '\', \'' + cap + '\', ' + !active + ', \'' + escapeHtml(JSON.stringify(caps)) + '\')"' +
|
||||
' title="' + cap + '">' + cap + '</button>';
|
||||
}
|
||||
html += ' </div>';
|
||||
html += ' <div class="admin-model-actions">';
|
||||
html += ' <button class="btn btn-small btn-danger" onclick="deleteAdminModel(\'' + m.id + '\', \'' + escapeHtml(m.model_id) + '\')">✕</button>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="admin-error">Failed to load models: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdminFetchModels() {
|
||||
const btn = document.getElementById('adminFetchModelsBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Fetching...';
|
||||
|
||||
try {
|
||||
const data = await Backend.adminFetchModels();
|
||||
const added = data.total_added || 0;
|
||||
const results = data.results || [];
|
||||
const errors = results.filter(r => r.error);
|
||||
|
||||
if (errors.length > 0) {
|
||||
showToast('⚠️ Fetched with ' + errors.length + ' error(s). ' + added + ' new models.', 'warning');
|
||||
} else {
|
||||
showToast('✅ Fetched ' + added + ' new models', 'success');
|
||||
}
|
||||
loadAdminModels();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '🔄 Fetch Models';
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleModelEnabled(modelId, enabled) {
|
||||
try {
|
||||
await Backend.adminUpdateModel(modelId, { is_enabled: enabled });
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
loadAdminModels();
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleModelCap(modelId, cap, value, capsJson) {
|
||||
try {
|
||||
const caps = JSON.parse(capsJson);
|
||||
caps[cap] = value;
|
||||
await Backend.adminUpdateModel(modelId, { capabilities: caps });
|
||||
loadAdminModels();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
loadAdminModels();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdminModel(modelId, modelName) {
|
||||
if (!confirm('Remove model "' + modelName + '"? It can be re-fetched later.')) return;
|
||||
try {
|
||||
await Backend.adminDeleteModel(modelId);
|
||||
showToast('✅ Model removed', 'success');
|
||||
loadAdminModels();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAdminProviders() {
|
||||
const container = document.getElementById('adminProviderList');
|
||||
container.innerHTML = '<div class="admin-loading">Loading...</div>';
|
||||
|
||||
try {
|
||||
const data = await Backend.adminListGlobalConfigs();
|
||||
const configs = data.configs || [];
|
||||
|
||||
if (configs.length === 0) {
|
||||
container.innerHTML = '<div class="provider-empty">No global providers. Users must add their own.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = configs.map(c => `
|
||||
<div class="provider-row">
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">${escapeHtml(c.name)}</span>
|
||||
<span class="provider-meta">${c.provider} · ${c.model_default || 'no default'}</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<span class="admin-badge admin-badge-active">${c.has_key ? '🔑' : '⚠️'}</span>
|
||||
<button class="btn btn-small btn-danger" onclick="deleteAdminProvider('${c.id}', '${escapeHtml(c.name)}')">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (e) {
|
||||
container.innerHTML = '<div class="admin-error">Failed to load providers: ' + e.message + '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function clearAdminProviderForm() {
|
||||
document.getElementById('adminProviderName').value = '';
|
||||
document.getElementById('adminProviderType').value = 'openai';
|
||||
document.getElementById('adminProviderEndpoint').value = '';
|
||||
document.getElementById('adminProviderApiKey').value = '';
|
||||
document.getElementById('adminProviderDefaultModel').value = '';
|
||||
}
|
||||
|
||||
async function handleCreateAdminProvider() {
|
||||
const name = document.getElementById('adminProviderName').value.trim();
|
||||
const provider = document.getElementById('adminProviderType').value;
|
||||
const endpoint = document.getElementById('adminProviderEndpoint').value.trim();
|
||||
const apiKey = document.getElementById('adminProviderApiKey').value.trim();
|
||||
const modelDefault = document.getElementById('adminProviderDefaultModel').value.trim();
|
||||
|
||||
if (!name || !endpoint || !apiKey) {
|
||||
showToast('⚠️ Name, endpoint, and API key are required', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Backend.adminCreateGlobalConfig(name, provider, endpoint, apiKey, modelDefault || null);
|
||||
showToast('✅ Global provider added', 'success');
|
||||
clearAdminProviderForm();
|
||||
document.getElementById('adminProviderAddForm').style.display = 'none';
|
||||
document.getElementById('adminProviderShowAddBtn').style.display = '';
|
||||
loadAdminProviders();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAdminProvider(configId, name) {
|
||||
if (!confirm('Remove global provider "' + name + '"?')) return;
|
||||
try {
|
||||
await Backend.adminDeleteGlobalConfig(configId);
|
||||
showToast('✅ Global provider removed', 'success');
|
||||
loadAdminProviders();
|
||||
} catch (e) {
|
||||
showToast('❌ ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user