From 5e6de10d0a5934a416a310e0217c3616729164b6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 9 Sep 2026 03:20:21 +0200 Subject: [PATCH] fix: delegated model-selection persistence, admin-settable conversation budget (DB overrides env), unbounded image poll --- public/components/admin.html | 5 +++- public/js/admin/clinicalAssistant.js | 14 ++++++++--- public/js/clinicalAssistant.js | 37 +++++++++++++--------------- src/routes/clinicalAssistant.js | 9 ++++++- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/public/components/admin.html b/public/components/admin.html index b9dd6323..99ef15d4 100644 --- a/public/components/admin.html +++ b/public/components/admin.html @@ -336,7 +336,10 @@
Conversation input budget -

Loading server budget metadata...

+
+ +

UTF-16 code units. Leave empty to use CLINICAL_ASSISTANT_CONVERSATION_CHARS.

+

Clinical Assistant prompts

diff --git a/public/js/admin/clinicalAssistant.js b/public/js/admin/clinicalAssistant.js index df6441f5..597535a3 100644 --- a/public/js/admin/clinicalAssistant.js +++ b/public/js/admin/clinicalAssistant.js @@ -136,10 +136,15 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) { setValue('assistant-search-limit', cfg['clinical_assistant.search_limit'] || '8'); setValue('assistant-context-chars', cfg['clinical_assistant.context_chars'] || '1400'); setValue('assistant-translate-provider', ['libretranslate', 'deepl'].includes(cfg['clinical_assistant.translate_provider']) ? cfg['clinical_assistant.translate_provider'] : 'libretranslate'); - var budgetLabel = document.getElementById('assistant-conversation-budget'); - if (budgetLabel) budgetLabel.textContent = - budget.limit.toLocaleString() + ' ' + budget.unit + ' (' + budget.measure + ') — ' + budget.env + - (budget.source === 'environment' ? ' (environment).' : ' (server default; environment unset).'); + var budgetInput = document.getElementById('assistant-conversation-budget'); + if (budgetInput && budget && Number.isInteger(budget.limit)) { + var cfgBudget = cfg['clinical_assistant.conversation_chars']; + budgetInput.value = cfgBudget ? String(cfgBudget) : String(budget.limit); + } + var budgetMeta = document.getElementById('assistant-conversation-budget-meta'); + if (budgetMeta) budgetMeta.textContent = budget && budget.source === 'environment' + ? 'Currently from CLINICAL_ASSISTANT_CONVERSATION_CHARS (' + budget.limit.toLocaleString() + '). Set a value here to override it; clear it to use the environment again.' + : 'UTF-16 code units. Leave empty to use CLINICAL_ASSISTANT_CONVERSATION_CHARS.'; configState = 'ready'; updateAssistantLoadState(); loadAssistantImageModels(); @@ -358,6 +363,7 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) { putAssistantConfig('clinical_assistant.search_limit', getValue('assistant-search-limit') || '8'), putAssistantConfig('clinical_assistant.context_chars', getValue('assistant-context-chars') || '1400'), putAssistantConfig('clinical_assistant.translate_provider', getValue('assistant-translate-provider') || 'libretranslate'), + putAssistantConfig('clinical_assistant.conversation_chars', getValue('assistant-conversation-budget')), putAssistantConfig('clinical_assistant.allowed_models', checkedAssistantModels('assistant-allowed-chat-models').join(',')), putAssistantConfig('clinical_assistant.allowed_image_models', checkedAssistantModels('assistant-allowed-image-models').join(',')) ]).then(function() { diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 771e42c7..55d59745 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -192,12 +192,12 @@ import { imageModel: data.imageModel || '' }; bindModelSelects(); - fillModelSelect(document.getElementById('assistant-chat-model-select'), statusChoices.allowedChatModels, statusChoices.chatModel, selectedChatModel, 'ped_assistant_chat_model'); + fillModelSelect(document.getElementById('assistant-chat-model-select'), statusChoices.allowedChatModels, statusChoices.chatModel, selectedChatModel, 'ped_assistant_chat_model', 'chat'); var chatSel = document.getElementById('assistant-chat-model-select'); selectedChatModel = chatSel ? chatSel.value : selectedChatModel; var imageSel = document.getElementById('assistant-image-model-select'); if (imageSel) { - fillModelSelect(imageSel, statusChoices.allowedImageModels, statusChoices.imageModel, selectedImageModel, 'ped_assistant_image_model'); + fillModelSelect(imageSel, statusChoices.allowedImageModels, statusChoices.imageModel, selectedImageModel, 'ped_assistant_image_model', 'image'); selectedImageModel = imageSel.value || selectedImageModel; } } @@ -1165,7 +1165,7 @@ import { var select = modal.querySelector('#create-image-chat'); if (select) select.selectedIndex = 0; bindModelSelects(); - fillModelSelect(document.getElementById('assistant-image-model-select'), statusChoices.allowedImageModels, statusChoices.imageModel, selectedImageModel, 'ped_assistant_image_model'); + fillModelSelect(document.getElementById('assistant-image-model-select'), statusChoices.allowedImageModels, statusChoices.imageModel, selectedImageModel, 'ped_assistant_image_model', 'image'); renderCreateImageHistory(); } @@ -1296,8 +1296,9 @@ import { else localStorage.removeItem(key); } catch (e) {} } - function fillModelSelect(select, allowed, configured, saved, saveKind) { + function fillModelSelect(select, allowed, configured, saved, saveKind, kind) { if (!select) return; + if (kind) select.setAttribute('data-model-select-kind', kind); select.innerHTML = ''; var def = document.createElement('option'); def.value = ''; @@ -1320,22 +1321,18 @@ import { if (pill) pill.hidden = !show; } function bindModelSelects() { - var chatSel = document.getElementById('assistant-chat-model-select'); - if (chatSel && !chatSel.dataset.bound) { - chatSel.dataset.bound = '1'; - chatSel.addEventListener('change', function() { - selectedChatModel = chatSel.value || ''; - saveModelSelection('ped_assistant_chat_model', selectedChatModel); - }); - } - var imageSel = document.getElementById('assistant-image-model-select'); - if (imageSel && !imageSel.dataset.bound) { - imageSel.dataset.bound = '1'; - imageSel.addEventListener('change', function() { - selectedImageModel = imageSel.value || ''; - saveModelSelection('ped_assistant_image_model', selectedImageModel); - }); - } + // Delegated persistence: any chat/image model select saves immediately, + // even when the popup recreates its element. + if (document.dataset.modelSelectsBound) return; + document.dataset.modelSelectsBound = '1'; + document.addEventListener('change', function(e) { + var sel = e.target && e.target.closest ? e.target.closest('[data-model-select-kind]') : null; + if (!sel) return; + var kind = sel.getAttribute('data-model-select-kind'); + var value = sel.value || ''; + if (kind === 'chat') { selectedChatModel = value; saveModelSelection('ped_assistant_chat_model', value); } + if (kind === 'image') { selectedImageModel = value; saveModelSelection('ped_assistant_image_model', value); } + }); } // ── Saved-chat options: Rename, Pin, Export, Delete (ChatGPT-style menu) ── diff --git a/src/routes/clinicalAssistant.js b/src/routes/clinicalAssistant.js index 454d4405..9c78f29a 100644 --- a/src/routes/clinicalAssistant.js +++ b/src/routes/clinicalAssistant.js @@ -755,7 +755,14 @@ function isUsefulIndexedTopicExample(item) { return true; } -function getConversationLimit() { +async function getConversationLimit() { + // Admin-set value wins over the environment so the administrator can test + // the warning/refusal behavior with a lower limit. + var override = await getSetting('clinical_assistant.conversation_chars', ''); + if (override !== '') { + var parsed = parseInt(override, 10); + if (Number.isInteger(parsed) && parsed >= 1000 && parsed <= 1000000) return parsed; + } return conversationBudget(process.env).limit; }