Files
shopify-ai-backup/chat/public/admin.js
southseact-3d 828a9dad41 Remove separate chain concept - fallback is now determined by OpenCode models order
- Removed opencodeChain variable entirely
- Removed chain form/list from admin UI
- Fallback now uses the order of models in the OpenCode models list
- Updated buildOpencodeAttemptChain to iterate through opencodeModels
- Removed chain-related API endpoints
- Simplified to just two lists: opencodeModels and publicModels
2026-02-18 16:18:31 +00:00

403 lines
14 KiB
JavaScript

(() => {
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 = '<option value="">No icon</option>';
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 = '<div class="muted">No icons uploaded yet. Add files to /chat/public/assets</div>';
return;
}
state.icons.forEach((iconPath) => {
const row = document.createElement('div');
row.className = 'admin-row';
row.innerHTML = `
<div class="model-chip">
<img src="${iconPath}" alt="" style="width: 24px; height: 24px; object-fit: contain;">
<span>${iconPath.replace('/assets/', '')}</span>
</div>
`;
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 = '<div class="muted">No OpenCode models added yet.</div>';
return;
}
state.opencodeModels.forEach((m, idx) => {
const row = document.createElement('div');
row.className = 'provider-row slim';
row.innerHTML = `
<div class="provider-row-header">
<div class="model-chip">
<span class="pill" style="background: ${idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'}; font-weight: 700;">#${idx + 1}</span>
${m.icon ? `<img src="${m.icon}" alt="" style="width: 20px; height: 20px;">` : ''}
<span>${m.label || m.name}</span>
<span class="pill">${m.name}</span>
<span class="pill">${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x)</span>
${m.supportsMedia ? '<span class="pill" style="background: var(--shopify-green);">Media</span>' : ''}
</div>
<div class="provider-row-actions">
<button class="ghost move-up" ${idx === 0 ? 'disabled' : ''}>↑</button>
<button class="ghost move-down" ${idx === state.opencodeModels.length - 1 ? 'disabled' : ''}>↓</button>
<button class="ghost delete-btn">Delete</button>
</div>
</div>
`;
// 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 = '<div class="muted">No public models added yet.</div>';
return;
}
state.publicModels.forEach((m, idx) => {
const row = document.createElement('div');
row.className = 'provider-row slim';
row.innerHTML = `
<div class="provider-row-header">
<div class="model-chip">
<span class="pill" style="background: ${idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'}; font-weight: 700;">#${idx + 1}</span>
${m.icon ? `<img src="${m.icon}" alt="" style="width: 20px; height: 20px;">` : ''}
<span>${m.label || m.name}</span>
<span class="pill">${m.name}</span>
<span class="pill">${(m.tier || 'free').toUpperCase()} (${m.multiplier || 1}x)</span>
${m.supportsMedia ? '<span class="pill" style="background: var(--shopify-green);">Media</span>' : ''}
</div>
<div class="provider-row-actions">
<button class="ghost move-up" ${idx === 0 ? 'disabled' : ''}>↑</button>
<button class="ghost move-down" ${idx === state.publicModels.length - 1 ? 'disabled' : ''}>↓</button>
<button class="ghost delete-btn">Delete</button>
</div>
</div>
`;
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();
})();