(() => { const pageType = document?.body?.dataset?.page || 'build'; console.log('Admin JS loaded, pageType:', pageType); // Clean state structure - just two things const state = { opencodeModels: [], // Models from OpenCode (order determines fallback) publicModels: [], // User-facing models (completely separate) icons: [], availableOpencodeModels: [], // Loaded from OpenCode }; // 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'), // 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 async function init() { await loadIcons(); await loadAvailableOpencodeModels(); await loadModels(); } 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); } }); } init(); })();