2f05b84b0f
Add an owner-gated dashboard to remap any tool/tier's model live without a code push. Overrides live in dbn_tool_engine_config (dobetternorge_maindb) and are consulted by dbnToolsResolveToolRun() + dbnToolsReviewerModel(); no row = unchanged code/.env behaviour (fully back-compatible). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
5.4 KiB
JavaScript
144 lines
5.4 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const d = window.DBN_DASHBOARD || {};
|
|
const apiBase = (d.apiBase || '/api/dashboard') + '/engines.php';
|
|
|
|
const $context = document.getElementById('engContext');
|
|
const $tier = document.getElementById('engTierPanel');
|
|
const $persona = document.getElementById('engPersonaPanel');
|
|
const $refresh = document.getElementById('engRefresh');
|
|
|
|
const safe = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
|
|
|
let tierEngines = [];
|
|
let reviewerModels = [];
|
|
|
|
function optionsHtml(list, selected) {
|
|
return list.map(o =>
|
|
'<option value="' + safe(o.key) + '"' + (o.key === selected ? ' selected' : '') + '>' + safe(o.label) + '</option>'
|
|
).join('');
|
|
}
|
|
|
|
function toolLabel(row) {
|
|
if (row.tool_slug === '*') {
|
|
return row.scope.indexOf('persona') === 0 ? 'All personas' : 'All tier tools';
|
|
}
|
|
return row.tool_slug;
|
|
}
|
|
|
|
function rowHtml(row, list) {
|
|
const list2 = list;
|
|
const sel = row.override || row.code_default;
|
|
const badge = row.override
|
|
? '<span class="eng-badge">override</span>'
|
|
: '<span class="eng-badge eng-badge--default">default</span>';
|
|
const meta = row.override && row.updated_by
|
|
? '<div class="eng-meta">by ' + safe(row.updated_by) + (row.updated_at ? ' · ' + safe(row.updated_at) : '') + '</div>'
|
|
: '';
|
|
return '<div class="eng-row" data-tool="' + safe(row.tool_slug) + '" data-scope="' + safe(row.scope) + '">' +
|
|
'<div><span class="eng-row__tool">' + safe(toolLabel(row)) + '</span>' +
|
|
'<div class="eng-row__def">default: ' + safe(row.code_default || '—') + '</div>' + meta + '</div>' +
|
|
'<div class="eng-row__scope">' + safe(row.scope) + '</div>' +
|
|
'<div><select class="eng-select">' + optionsHtml(list2, sel) + '</select></div>' +
|
|
'<div class="eng-row__actions">' + badge +
|
|
(row.override ? '<button class="dash-btn eng-clear" type="button">Clear</button>' : '') +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function headHtml() {
|
|
return '<div class="eng-head"><div>Tool</div><div>Scope</div><div>Engine</div><div></div></div>';
|
|
}
|
|
|
|
function render(data) {
|
|
tierEngines = data.tier_engines || [];
|
|
reviewerModels = data.reviewer_models || [];
|
|
|
|
const ctx = data.context || {};
|
|
$context.innerHTML = '<strong>Bedrock: ' + (ctx.bedrock_enabled ? 'ON' : 'OFF') + '</strong> — ' + safe(ctx.note || '');
|
|
|
|
const rows = data.rows || [];
|
|
const tierRows = rows.filter(r => r.scope.indexOf('persona') !== 0);
|
|
const personaRows = rows.filter(r => r.scope.indexOf('persona') === 0);
|
|
|
|
$tier.innerHTML = headHtml() + tierRows.map(r => rowHtml(r, tierEngines)).join('');
|
|
$persona.innerHTML = headHtml() + personaRows.map(r => rowHtml(r, reviewerModels)).join('');
|
|
|
|
bind($tier);
|
|
bind($persona);
|
|
}
|
|
|
|
function bind(panel) {
|
|
panel.querySelectorAll('.eng-row').forEach(rowEl => {
|
|
const tool = rowEl.getAttribute('data-tool');
|
|
const scope = rowEl.getAttribute('data-scope');
|
|
const select = rowEl.querySelector('.eng-select');
|
|
const clearBtn = rowEl.querySelector('.eng-clear');
|
|
|
|
if (select) {
|
|
select.addEventListener('change', () => save(tool, scope, select.value, rowEl));
|
|
}
|
|
if (clearBtn) {
|
|
clearBtn.addEventListener('click', () => clear(tool, scope, rowEl));
|
|
}
|
|
});
|
|
}
|
|
|
|
function setBusy(rowEl, busy) {
|
|
rowEl.querySelectorAll('select, button').forEach(el => { el.disabled = busy; });
|
|
}
|
|
|
|
async function post(action, body) {
|
|
const r = await fetch(apiBase + '?action=' + action, {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
const data = await r.json();
|
|
if (!data.ok) throw new Error(data.message || 'Request failed');
|
|
return data;
|
|
}
|
|
|
|
async function save(tool, scope, engine, rowEl) {
|
|
setBusy(rowEl, true);
|
|
try {
|
|
await post('set', { tool_slug: tool, scope: scope, engine: engine });
|
|
await load();
|
|
} catch (e) {
|
|
alert('Could not save: ' + e.message);
|
|
setBusy(rowEl, false);
|
|
}
|
|
}
|
|
|
|
async function clear(tool, scope, rowEl) {
|
|
setBusy(rowEl, true);
|
|
try {
|
|
await post('clear', { tool_slug: tool, scope: scope });
|
|
await load();
|
|
} catch (e) {
|
|
alert('Could not clear: ' + e.message);
|
|
setBusy(rowEl, false);
|
|
}
|
|
}
|
|
|
|
async function load() {
|
|
$tier.innerHTML = '<div class="dms-loading"></div>';
|
|
$persona.innerHTML = '<div class="dms-loading"></div>';
|
|
try {
|
|
const r = await fetch(apiBase, { credentials: 'same-origin' });
|
|
const data = await r.json();
|
|
if (!data.ok) throw new Error(data.message || 'Could not load engine map');
|
|
render(data);
|
|
} catch (e) {
|
|
$context.textContent = 'Could not load: ' + e.message;
|
|
$tier.innerHTML = '';
|
|
$persona.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
if ($refresh) $refresh.addEventListener('click', load);
|
|
load();
|
|
})();
|