(() => {
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 = `
`;
// 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 = `
`;
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();
})();