(() => { const pageType = document?.body?.dataset?.page || 'build'; console.log('Admin JS loaded, pageType:', pageType); // State structure const state = { opencodeModels: [], // Models from OpenCode (order determines fallback) publicModels: [], // User-facing models (completely separate) icons: [], availableOpencodeModels: [], // Loaded from OpenCode planSettings: { provider: 'openrouter', freePlanModel: '', planningChain: [] }, providerLimits: {}, providerUsage: [], }; // Element references const el = { // OpenCode Models opencodeModelForm: document.getElementById('opencode-model-form'), opencodeModelSelect: document.getElementById('opencode-model-select'), opencodeModelLabel: document.getElementById('opencode-model-label'), opencodeModelTier: document.getElementById('opencode-model-tier'), opencodeModelIcon: document.getElementById('opencode-model-icon'), opencodeModelMedia: document.getElementById('opencode-model-media'), opencodeModelStatus: document.getElementById('opencode-model-status'), reloadOpencodeModels: document.getElementById('reload-opencode-models'), opencodeModelsList: document.getElementById('opencode-models-list'), opencodeModelsCount: document.getElementById('opencode-models-count'), // Public Models publicModelForm: document.getElementById('public-model-form'), publicModelName: document.getElementById('public-model-name'), publicModelLabel: document.getElementById('public-model-label'), publicModelTier: document.getElementById('public-model-tier'), publicModelIcon: document.getElementById('public-model-icon'), publicModelMedia: document.getElementById('public-model-media'), publicModelStatus: document.getElementById('public-model-status'), publicModelsList: document.getElementById('public-models-list'), publicModelsCount: document.getElementById('public-models-count'), // Auto Model Form autoModelForm: document.getElementById('auto-model-form'), autoModelSelect: document.getElementById('auto-model-select'), autoModelStatus: document.getElementById('auto-model-status'), // Provider Limits Form providerLimitForm: document.getElementById('provider-limit-form'), limitProvider: document.getElementById('limit-provider'), limitScope: document.getElementById('limit-scope'), limitModel: document.getElementById('limit-model'), limitTpm: document.getElementById('limit-tpm'), limitTph: document.getElementById('limit-tph'), limitTpd: document.getElementById('limit-tpd'), limitRpm: document.getElementById('limit-rpm'), limitRph: document.getElementById('limit-rph'), limitRpd: document.getElementById('limit-rpd'), providerLimitStatus: document.getElementById('provider-limit-status'), providerUsage: document.getElementById('provider-usage'), // Other iconList: document.getElementById('icon-list'), adminRefresh: document.getElementById('admin-refresh'), adminLogout: document.getElementById('admin-logout'), cancelAllMessages: document.getElementById('cancel-all-messages'), cancelMessagesStatus: document.getElementById('cancel-messages-status'), }; // Helper functions function setStatus(el, msg, isError = false) { if (!el) return; el.textContent = msg || ''; el.style.color = isError ? 'var(--danger)' : 'inherit'; } async function api(url, options = {}) { const res = await fetch(url, { headers: { 'Content-Type': 'application/json' }, ...options, }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Request failed'); return data; } // Load available models from OpenCode async function loadAvailableOpencodeModels() { try { const data = await api('/api/admin/available-models'); state.availableOpencodeModels = data.models || []; renderOpencodeModelSelect(); } catch (err) { console.error('Failed to load OpenCode models:', err); } } // Render OpenCode model dropdown function renderOpencodeModelSelect() { if (!el.opencodeModelSelect) return; el.opencodeModelSelect.innerHTML = ''; if (!state.availableOpencodeModels.length) { const opt = document.createElement('option'); opt.value = ''; opt.textContent = 'No models available'; el.opencodeModelSelect.appendChild(opt); return; } state.availableOpencodeModels.forEach((m) => { const opt = document.createElement('option'); opt.value = m.name || m.id || m; opt.textContent = m.label || m.name || m.id || m; el.opencodeModelSelect.appendChild(opt); }); } // Render icons in selects function renderIconOptions(selectEl) { if (!selectEl) return; const currentValue = selectEl.value; selectEl.innerHTML = ''; state.icons.forEach((iconPath) => { const opt = document.createElement('option'); opt.value = iconPath; opt.textContent = iconPath.replace('/assets/', ''); selectEl.appendChild(opt); }); selectEl.value = currentValue; } // Load icons async function loadIcons() { try { const data = await api('/api/admin/icons'); state.icons = data.icons || []; renderIconOptions(el.opencodeModelIcon); renderIconOptions(el.publicModelIcon); renderIconLibrary(); } catch (err) { console.error('Failed to load icons:', err); } } // Render icon library function renderIconLibrary() { if (!el.iconList) return; el.iconList.innerHTML = ''; if (!state.icons.length) { el.iconList.innerHTML = '
No icons uploaded yet. Add files to /chat/public/assets
'; return; } state.icons.forEach((iconPath) => { const row = document.createElement('div'); row.className = 'admin-row'; row.innerHTML = `
${iconPath.replace('/assets/', '')}
`; el.iconList.appendChild(row); }); } // Load all model data async function loadModels() { try { const data = await api('/api/admin/models'); state.opencodeModels = data.opencodeModels || []; state.publicModels = data.publicModels || []; renderOpencodeModels(); renderPublicModels(); } catch (err) { console.error('Failed to load models:', err); } } // Render OpenCode Models list function renderOpencodeModels() { if (!el.opencodeModelsList) return; el.opencodeModelsList.innerHTML = ''; if (el.opencodeModelsCount) { el.opencodeModelsCount.textContent = state.opencodeModels.length.toString(); } if (!state.opencodeModels.length) { el.opencodeModelsList.innerHTML = '
No OpenCode models added yet.
'; return; } state.opencodeModels.forEach((m, idx) => { const row = document.createElement('div'); row.className = 'provider-row slim'; row.innerHTML = `
#${idx + 1} ${m.icon ? `` : ''} ${m.label || m.name} ${m.name} ${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x) ${m.supportsMedia ? 'Media' : ''}
`; // Add event listeners row.querySelector('.move-up')?.addEventListener('click', () => moveOpencodeModel(idx, -1)); row.querySelector('.move-down')?.addEventListener('click', () => moveOpencodeModel(idx, 1)); row.querySelector('.delete-btn')?.addEventListener('click', () => deleteModel(m.id, 'opencode')); el.opencodeModelsList.appendChild(row); }); } // Render Public Models function renderPublicModels() { if (!el.publicModelsList) return; el.publicModelsList.innerHTML = ''; if (el.publicModelsCount) { el.publicModelsCount.textContent = state.publicModels.length.toString(); } if (!state.publicModels.length) { el.publicModelsList.innerHTML = '
No public models added yet.
'; return; } state.publicModels.forEach((m, idx) => { const row = document.createElement('div'); row.className = 'provider-row slim'; row.innerHTML = `
#${idx + 1} ${m.icon ? `` : ''} ${m.label || m.name} ${m.name} ${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x) ${m.supportsMedia ? 'Media' : ''}
`; row.querySelector('.move-up')?.addEventListener('click', () => movePublicModel(idx, -1)); row.querySelector('.move-down')?.addEventListener('click', () => movePublicModel(idx, 1)); row.querySelector('.delete-btn')?.addEventListener('click', () => deleteModel(m.id, 'public')); el.publicModelsList.appendChild(row); }); } // Move OpenCode Model async function moveOpencodeModel(fromIdx, direction) { const toIdx = fromIdx + direction; if (toIdx < 0 || toIdx >= state.opencodeModels.length) return; const newOrder = [...state.opencodeModels]; const [item] = newOrder.splice(fromIdx, 1); newOrder.splice(toIdx, 0, item); try { await api('/api/admin/models/reorder', { method: 'POST', body: JSON.stringify({ type: 'opencode', models: newOrder }), }); state.opencodeModels = newOrder; renderOpencodeModels(); } catch (err) { console.error('Failed to reorder:', err); } } // Move Public Model async function movePublicModel(fromIdx, direction) { const toIdx = fromIdx + direction; if (toIdx < 0 || toIdx >= state.publicModels.length) return; const newOrder = [...state.publicModels]; const [item] = newOrder.splice(fromIdx, 1); newOrder.splice(toIdx, 0, item); try { await api('/api/admin/models/reorder', { method: 'POST', body: JSON.stringify({ type: 'public', models: newOrder }), }); state.publicModels = newOrder; renderPublicModels(); } catch (err) { console.error('Failed to reorder:', err); } } // Delete Model async function deleteModel(id, type) { try { await api(`/api/admin/models/${id}?type=${type}`, { method: 'DELETE' }); await loadModels(); } catch (err) { console.error('Failed to delete:', err); } } // Form Handlers if (el.opencodeModelForm) { el.opencodeModelForm.addEventListener('submit', async (e) => { e.preventDefault(); const model = el.opencodeModelSelect.value; const label = el.opencodeModelLabel.value.trim(); if (!model || !label) { setStatus(el.opencodeModelStatus, 'Model and display name are required', true); return; } try { await api('/api/admin/models', { method: 'POST', body: JSON.stringify({ type: 'opencode', name: model, label, tier: el.opencodeModelTier?.value || 'free', icon: el.opencodeModelIcon?.value || '', supportsMedia: el.opencodeModelMedia?.checked || false, }), }); setStatus(el.opencodeModelStatus, 'Added'); el.opencodeModelLabel.value = ''; await loadModels(); } catch (err) { setStatus(el.opencodeModelStatus, err.message, true); } }); } if (el.reloadOpencodeModels) { el.reloadOpencodeModels.addEventListener('click', async () => { setStatus(el.opencodeModelStatus, 'Loading...'); await loadAvailableOpencodeModels(); setStatus(el.opencodeModelStatus, 'Loaded'); setTimeout(() => setStatus(el.opencodeModelStatus, ''), 1500); }); } if (el.publicModelForm) { el.publicModelForm.addEventListener('submit', async (e) => { e.preventDefault(); const name = el.publicModelName.value.trim(); const label = el.publicModelLabel.value.trim(); if (!name || !label) { setStatus(el.publicModelStatus, 'Model ID and display name are required', true); return; } try { await api('/api/admin/models', { method: 'POST', body: JSON.stringify({ type: 'public', name, label, tier: el.publicModelTier?.value || 'free', icon: el.publicModelIcon?.value || '', supportsMedia: el.publicModelMedia?.checked || false, }), }); setStatus(el.publicModelStatus, 'Added'); el.publicModelName.value = ''; el.publicModelLabel.value = ''; await loadModels(); } catch (err) { setStatus(el.publicModelStatus, err.message, true); } }); } // Initialize // Load plan settings async function loadPlanSettings() { try { const data = await api('/api/admin/plan-settings'); state.planSettings = data || { provider: 'openrouter', freePlanModel: '', planningChain: [] }; populateAutoModelSelect(); if (el.autoModelSelect && state.planSettings.freePlanModel) { el.autoModelSelect.value = state.planSettings.freePlanModel; } } catch (err) { console.error('Failed to load plan settings:', err); } } // Populate auto model select dropdown function populateAutoModelSelect() { if (!el.autoModelSelect) return; const currentValue = el.autoModelSelect.value; el.autoModelSelect.innerHTML = ''; state.opencodeModels.forEach((m) => { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = `${m.label || m.name} (${m.name})`; el.autoModelSelect.appendChild(opt); }); el.autoModelSelect.value = currentValue; } // Load provider limits async function loadProviderLimits() { try { const data = await api('/api/admin/provider-limits'); state.providerLimits = data.limits || {}; state.providerUsage = data.usage || []; renderProviderUsage(); } catch (err) { console.error('Failed to load provider limits:', err); } } // Render provider usage function renderProviderUsage() { if (!el.providerUsage) return; el.providerUsage.innerHTML = ''; if (!state.providerUsage.length) { el.providerUsage.innerHTML = '
No usage data available.
'; return; } state.providerUsage.forEach((usage) => { const row = document.createElement('div'); row.className = 'admin-row'; row.innerHTML = `
${usage.provider} ${usage.tokens || 0} tokens / ${usage.requests || 0} requests
`; el.providerUsage.appendChild(row); }); } // Update limit model options based on provider selection function updateLimitModelOptions() { if (!el.limitModel || !el.limitProvider) return; const provider = el.limitProvider.value; const currentValue = el.limitModel.value; el.limitModel.innerHTML = ''; // Add models from opencodeModels that match this provider state.opencodeModels.forEach((m) => { if (m.name && m.name.includes('/')) { const modelProvider = m.name.split('/')[0]; if (modelProvider === provider) { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.label || m.name; el.limitModel.appendChild(opt); } } }); el.limitModel.value = currentValue; } async function init() { await loadIcons(); await loadAvailableOpencodeModels(); await loadModels(); await loadPlanSettings(); await loadProviderLimits(); } if (el.adminRefresh) { el.adminRefresh.addEventListener('click', init); } if (el.adminLogout) { el.adminLogout.addEventListener('click', async () => { await api('/api/admin/logout', { method: 'POST' }); window.location.href = '/admin/login'; }); } if (el.cancelAllMessages) { el.cancelAllMessages.addEventListener('click', async () => { if (!confirm('Are you sure you want to cancel all running and queued messages?')) return; try { await api('/api/admin/cancel-all-messages', { method: 'POST' }); setStatus(el.cancelMessagesStatus, 'All messages cancelled'); } catch (err) { setStatus(el.cancelMessagesStatus, err.message, true); } }); } // Auto Model Form Handler if (el.autoModelForm) { el.autoModelForm.addEventListener('submit', async (e) => { e.preventDefault(); const selectedModel = el.autoModelSelect.value; try { await api('/api/admin/plan-settings', { method: 'POST', body: JSON.stringify({ ...state.planSettings, freePlanModel: selectedModel, }), }); setStatus(el.autoModelStatus, 'Saved'); setTimeout(() => setStatus(el.autoModelStatus, ''), 1500); } catch (err) { setStatus(el.autoModelStatus, err.message, true); } }); } // Provider Limit Form Handler if (el.providerLimitForm) { // Update model options when provider changes el.limitProvider?.addEventListener('change', () => { updateLimitModelOptions(); // Load existing limits for this provider if any const provider = el.limitProvider.value; const scope = el.limitScope.value; const model = el.limitModel.value; const limits = state.providerLimits[provider]; if (limits) { const target = scope === 'model' && model ? (limits.perModel?.[model] || {}) : limits; if (el.limitTpm) el.limitTpm.value = target.tokensPerMinute || ''; if (el.limitTph) el.limitTph.value = target.tokensPerHour || ''; if (el.limitTpd) el.limitTpd.value = target.tokensPerDay || ''; if (el.limitRpm) el.limitRpm.value = target.requestsPerMinute || ''; if (el.limitRph) el.limitRph.value = target.requestsPerHour || ''; if (el.limitRpd) el.limitRpd.value = target.requestsPerDay || ''; } }); // Update form when scope changes el.limitScope?.addEventListener('change', () => { if (el.limitModel) { el.limitModel.disabled = el.limitScope.value !== 'model'; if (el.limitScope.value !== 'model') el.limitModel.value = ''; } }); el.providerLimitForm.addEventListener('submit', async (e) => { e.preventDefault(); const provider = el.limitProvider?.value; const scope = el.limitScope?.value; const model = el.limitModel?.value; const limits = { tokensPerMinute: parseInt(el.limitTpm?.value) || 0, tokensPerHour: parseInt(el.limitTph?.value) || 0, tokensPerDay: parseInt(el.limitTpd?.value) || 0, requestsPerMinute: parseInt(el.limitRpm?.value) || 0, requestsPerHour: parseInt(el.limitRph?.value) || 0, requestsPerDay: parseInt(el.limitRpd?.value) || 0, }; try { const payload = { provider, scope, model: scope === 'model' ? model : null, limits, }; await api('/api/admin/provider-limits', { method: 'POST', body: JSON.stringify(payload), }); setStatus(el.providerLimitStatus, 'Saved'); await loadProviderLimits(); setTimeout(() => setStatus(el.providerLimitStatus, ''), 1500); } catch (err) { setStatus(el.providerLimitStatus, err.message, true); } }); } init(); })();