From 7e5dc8b62d5b638acbf4c1749b41ffd38c2115d7 Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Wed, 18 Feb 2026 14:08:21 +0000 Subject: [PATCH] Add up/down reorder buttons and order numbers to public models - Add order number badge (#1, #2, #3) to each public model - Add up/down arrow buttons to reorder models in the list - Add persistPublicModelsOrder function to save reordered list - Add server-side /api/admin/models/reorder endpoint - Remove automatic alphabetical sorting to preserve custom order - First model (#1) gets green background highlighting --- chat/public/admin.js | 58 +++++++++++++++++++++++++++++++++++++++++++- chat/server.js | 44 +++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/chat/public/admin.js b/chat/public/admin.js index dba3fab..3f61953 100644 --- a/chat/public/admin.js +++ b/chat/public/admin.js @@ -605,7 +605,7 @@ return; } - modelsToRender.forEach((m) => { + modelsToRender.forEach((m, idx) => { const row = document.createElement('div'); row.className = 'provider-row slim'; @@ -615,6 +615,14 @@ const info = document.createElement('div'); info.className = 'model-chip'; + // Order number badge + const orderBadge = document.createElement('span'); + orderBadge.className = 'pill'; + orderBadge.textContent = `#${idx + 1}`; + orderBadge.style.background = idx === 0 ? 'var(--shopify-green)' : 'var(--primary)'; + orderBadge.style.fontWeight = '700'; + info.appendChild(orderBadge); + if (m.icon) { const img = document.createElement('img'); img.src = m.icon; @@ -658,6 +666,36 @@ const headerActions = document.createElement('div'); headerActions.className = 'provider-row-actions'; + // Up button + const upBtn = document.createElement('button'); + upBtn.className = 'ghost'; + upBtn.textContent = '↑'; + upBtn.title = 'Move up'; + upBtn.disabled = idx === 0; + upBtn.addEventListener('click', async () => { + if (idx === 0) return; + const next = [...modelsToRender]; + const [item] = next.splice(idx, 1); + next.splice(idx - 1, 0, item); + await persistPublicModelsOrder(next); + }); + headerActions.appendChild(upBtn); + + // Down button + const downBtn = document.createElement('button'); + downBtn.className = 'ghost'; + downBtn.textContent = '↓'; + downBtn.title = 'Move down'; + downBtn.disabled = idx === modelsToRender.length - 1; + downBtn.addEventListener('click', async () => { + if (idx === modelsToRender.length - 1) return; + const next = [...modelsToRender]; + const [item] = next.splice(idx, 1); + next.splice(idx + 1, 0, item); + await persistPublicModelsOrder(next); + }); + headerActions.appendChild(downBtn); + const delBtn = document.createElement('button'); delBtn.className = 'ghost'; delBtn.textContent = 'Delete'; @@ -826,6 +864,24 @@ } } + async function persistPublicModelsOrder(orderedModels) { + setStatus('Saving order...'); + try { + // Use the reorder endpoint to save the new order + const res = await api('/api/admin/models/reorder', { + method: 'POST', + body: JSON.stringify({ models: orderedModels }), + }); + // Update local state with new order from server + state.publicModels = res.publicModels || orderedModels; + renderConfigured(); + setStatus('Order saved'); + setTimeout(() => setStatus(''), 1500); + } catch (err) { + setStatus(err.message, true); + } + } + // Render the unified provider chain with up/down controls function renderProviderChain() { if (!el.providerChainList) return; diff --git a/chat/server.js b/chat/server.js index 42aa49a..a5e2aeb 100644 --- a/chat/server.js +++ b/chat/server.js @@ -15857,7 +15857,7 @@ async function handleAdminModelsList(req, res) { if (!session) return; // Return new unified structure sendJson(res, 200, { - publicModels: publicModels.sort((a, b) => (a.label || '').localeCompare(b.label || '')), + publicModels: publicModels, providerChain, // Legacy support models: getConfiguredModels('opencode'), @@ -15954,7 +15954,7 @@ async function handleAdminModelUpsert(req, res) { await persistAdminModels(); sendJson(res, 200, { model: payload, - publicModels: publicModels.sort((a, b) => (a.label || '').localeCompare(b.label || '')), + publicModels: publicModels, providerChain, models: getConfiguredModels('opencode'), }); @@ -15964,6 +15964,45 @@ async function handleAdminModelUpsert(req, res) { } } +async function handleAdminModelsReorder(req, res) { + const session = requireAdminAuth(req, res); + if (!session) return; + try { + const body = await parseJsonBody(req); + if (!Array.isArray(body.models)) { + return sendJson(res, 400, { error: 'models array is required' }); + } + + // Validate that all provided IDs exist + const currentIds = new Set(publicModels.map(m => m.id)); + const newIds = body.models.map(m => m.id); + const allExist = newIds.every(id => currentIds.has(id)); + + if (!allExist) { + return sendJson(res, 400, { error: 'Invalid model IDs in reorder request' }); + } + + // Reorder publicModels based on the provided order + const reordered = []; + body.models.forEach(m => { + const model = publicModels.find(pm => pm.id === m.id); + if (model) reordered.push(model); + }); + + publicModels = reordered; + await persistAdminModels(); + + sendJson(res, 200, { + ok: true, + publicModels: publicModels, + providerChain, + models: getConfiguredModels('opencode'), + }); + } catch (error) { + sendJson(res, 400, { error: error.message || 'Unable to reorder models' }); + } +} + async function handleAdminModelDelete(req, res, id) { const session = requireAdminAuth(req, res); if (!session) return; @@ -18878,6 +18917,7 @@ async function routeInternal(req, res, url, pathname) { if (req.method === 'GET' && pathname === '/api/admin/icons') return handleAdminListIcons(req, res); if (req.method === 'GET' && pathname === '/api/admin/models') return handleAdminModelsList(req, res); if (req.method === 'POST' && pathname === '/api/admin/models') return handleAdminModelUpsert(req, res); + if (req.method === 'POST' && pathname === '/api/admin/models/reorder') return handleAdminModelsReorder(req, res); if (req.method === 'GET' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsGet(req, res); if (req.method === 'POST' && pathname === '/api/admin/openrouter-settings') return handleAdminOpenRouterSettingsPost(req, res); if (req.method === 'GET' && pathname === '/api/admin/mistral-settings') return handleAdminMistralSettingsGet(req, res);