From bb822951734dbcccb4f719c78a075abe383a9957 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 7 Sep 2026 17:58:28 +0200 Subject: [PATCH] fix: single image-model dropdown applied to both workflows; concise budget labels --- public/js/admin/imageSettings.js | 67 +++++++++++++++++++------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/public/js/admin/imageSettings.js b/public/js/admin/imageSettings.js index 5e058d8a..a13a757f 100644 --- a/public/js/admin/imageSettings.js +++ b/public/js/admin/imageSettings.js @@ -1,36 +1,49 @@ -// Separate from the existing clinical model form; loading never replaces a draft. +// One image-model setting for the whole app: choose from discovered models, applied to both workflows. import { imageJson } from '../generatedImages.js'; let loading = false; +let loaded = false; +let select; let budgetClinical; let budgetLearning; let save; let status; + async function load() { const root = document.getElementById('workflow-image-settings'); - if (!root || root.children.length || loading) return; + if (!root || loaded || loading) return; loading = true; try { - const data = await imageJson('/api/admin/image-settings'); - for (const workflow of ['clinical_assistant', 'learning_hub']) { - const form = document.createElement('form'); - const heading = document.createElement('h4'); heading.textContent = workflow === 'learning_hub' ? 'Learning Hub image model and budget' : 'Clinical Assistant image budget'; - const help = document.createElement('p'); help.textContent = 'Budget: full original request + tool description + workflow/output/layout instructions + selected preceding context, in UTF-16 code units. Default 32,000; allowed 1,000–32,000. Conservative allowance, not an average or computed model token limit. Mandatory overflow is rejected; only whole recent context turns are included. Original chats and drafts are never truncated.'; - const label = document.createElement('label'); label.textContent = 'Image input budget (UTF-16 code units) '; - const budget = document.createElement('input'); budget.type = 'number'; budget.min = '1000'; budget.max = '32000'; budget.step = '1'; budget.required = true; budget.value = data.workflows[workflow].budget; label.append(budget); - form.append(heading, help, label); - let model; - if (workflow === 'learning_hub') { - const ml = document.createElement('label'); ml.textContent = 'Learning image model ID (configured LiteLLM gateway only) '; - model = document.createElement('input'); model.required = true; model.value = data.workflows[workflow].model; model.maxLength = 200; ml.append(model); form.append(ml); - } - const save = document.createElement('button'); save.type = 'submit'; save.textContent = 'Save image settings'; - const status = document.createElement('p'); status.setAttribute('role', 'status'); form.append(save, status); - form.onsubmit = async e => { - e.preventDefault(); if (save.disabled) return; save.disabled = true; - try { - await imageJson('/api/admin/image-settings/' + workflow, { method: 'PUT', body: JSON.stringify({ budget: Number(budget.value), ...(model ? { model: model.value } : {}) }) }); - status.textContent = 'Saved. New jobs snapshot these settings; existing jobs are unchanged.'; - } catch (error) { status.textContent = error.message + ' Draft preserved.'; } finally { save.disabled = false; } - }; - root.append(form); - } - } catch (_) { /* Next tab entry retries, without touching prompt drafts. */ } + const [settings, models] = await Promise.all([ + imageJson('/api/admin/image-settings'), + imageJson('/api/admin/config/image-models/discover').catch(() => ({ success: false })) + ]); + root.textContent = ''; + const form = document.createElement('form'); + const heading = document.createElement('h4'); heading.textContent = 'Image model'; + select = document.createElement('select'); select.className = 'prompt-select'; + select.style.cssText = 'display:block;max-width:100%;font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;'; + const known = new Set([settings.workflows.clinical_assistant.model, settings.workflows.learning_hub.model]); + const options = []; + (Array.isArray(models.models) ? models.models : []).forEach(m => { if (m && typeof m.id === 'string' && m.id) { options.push(m.id); known.add(m.id); } }); + const current = settings.workflows.clinical_assistant.model || settings.workflows.learning_hub.model; + [...new Set([...known])].forEach(id => { const o = document.createElement('option'); o.value = id; o.textContent = id; select.appendChild(o); }); + if (current) select.value = current; + const label1 = document.createElement('label'); label1.textContent = 'Clinical image input budget (UTF-16 code units) '; + budgetClinical = document.createElement('input'); budgetClinical.type = 'number'; budgetClinical.min = '1000'; budgetClinical.max = '32000'; budgetClinical.required = true; budgetClinical.value = settings.workflows.clinical_assistant.budget; label1.append(budgetClinical); + const label2 = document.createElement('label'); label2.textContent = 'Learning image input budget (UTF-16 code units) '; + budgetLearning = document.createElement('input'); budgetLearning.type = 'number'; budgetLearning.min = '1000'; budgetLearning.max = '32000'; budgetLearning.required = true; budgetLearning.value = settings.workflows.learning_hub.budget; label2.append(budgetLearning); + save = document.createElement('button'); save.type = 'submit'; save.className = 'btn-sm btn-primary'; save.textContent = 'Save image settings'; + status = document.createElement('p'); status.setAttribute('role', 'status'); + form.append(heading, select, label1, label2, save, status); + form.onsubmit = async e => { + e.preventDefault(); if (save.disabled) return; save.disabled = true; + try { + // One model chosen once, applied to both workflows. + for (const workflow of ['clinical_assistant', 'learning_hub']) { + await imageJson('/api/admin/image-settings/' + workflow, { method: 'PUT', body: JSON.stringify({ model: select.value, budget: Number(workflow === 'clinical_assistant' ? budgetClinical.value : budgetLearning.value) }) }); + } + status.textContent = 'Saved. New jobs use these settings; existing jobs are unchanged.'; + } catch (error) { status.textContent = error.message + ' Nothing was saved.'; } finally { save.disabled = false; } + }; + root.append(form); + loaded = true; + } catch (_) { /* Next tab entry retries; no drafts are touched. */ } finally { loading = false; } } document.addEventListener('tabChanged', e => { if (e.detail?.tab === 'admin') load(); });