feat: admin allowlists for chat and image models (users see the model pill once configured)

This commit is contained in:
Daniel 2026-09-09 02:58:30 +02:00
parent 73955dd5f9
commit b21080f2a6
2 changed files with 76 additions and 1 deletions

View file

@ -292,6 +292,20 @@
<button id="btn-test-assistant-chat-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
</div>
<div id="assistant-chat-test-result" style="font-size:12px;color:var(--g500);"></div>
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Model availability</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:8px;min-width:0;">
<p style="margin:0;font-size:12px;color:var(--g500);">Models users may select in the Clinical Assistant. Leave a list empty to keep only the configured model.</p>
<div>
<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:2px;">Chat models</div>
<div id="assistant-allowed-chat-models" aria-label="Chat models available to the Clinical Assistant"></div>
</div>
<div>
<div style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:2px;">Image models</div>
<div id="assistant-allowed-image-models" aria-label="Image models available to the Clinical Assistant"></div>
</div>
</div>
</div>
<div style="border:1px solid var(--g100);border-radius:8px;padding:10px;display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;">
<div>
<div style="font-size:13px;font-weight:600;color:var(--g700);">Starter prompt pool</div>

View file

@ -1,5 +1,52 @@
export function initClinicalAssistantAdmin(adminEscapeHtml) {
let configState = 'idle';
const IMAGE_MODEL_FALLBACKS = ['openai-gpt-image-1', 'openai-gpt-image-1-mini', 'openai-gpt-image-1.5', 'openai-dall-e-3'];
let savedChatAllowed = [];
let savedImageAllowed = [];
let chatRoster = [];
let imageRoster = [];
function parseAssistantList(value) {
return String(value || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean);
}
function renderAssistantCheckboxList(containerId, candidates, saved) {
var container = document.getElementById(containerId);
if (!container) return;
var ids = candidates.slice();
saved.forEach(function(id) { if (ids.indexOf(id) === -1) ids.push(id); });
ids = ids.filter(Boolean).filter(function(id, i, all) { return all.indexOf(id) === i; }).sort();
container.innerHTML = '';
if (!ids.length) {
var empty = document.createElement('p');
empty.style.cssText = 'margin:0;font-size:12px;color:var(--g500);';
empty.textContent = 'No models available. Add or enable models first.';
container.appendChild(empty);
return;
}
ids.forEach(function(id) {
var row = document.createElement('label');
row.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:12px;padding:2px 0;';
var box = document.createElement('input');
box.type = 'checkbox';
box.value = id;
box.checked = saved.indexOf(id) !== -1;
var span = document.createElement('span');
span.textContent = id;
row.appendChild(box);
row.appendChild(span);
container.appendChild(row);
});
}
function checkedAssistantModels(containerId) {
var container = document.getElementById(containerId);
if (!container) return [];
return Array.prototype.map.call(container.querySelectorAll('input[type="checkbox"]:checked'), function(box) { return box.value; });
}
function renderAssistantImageModelCheckboxes(models) {
imageRoster = (Array.isArray(models) ? models : []).map(function(m) { return m.id; }).concat(IMAGE_MODEL_FALLBACKS, [window._assistantImageModelValue]);
renderAssistantCheckboxList('assistant-allowed-image-models', imageRoster, savedImageAllowed);
}
let imageModelsLoading = false;
document.addEventListener('tabChanged', function(e) {
@ -80,6 +127,11 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
chatSelect.value = savedChatModel;
}
window._assistantImageModelValue = cfg['clinical_assistant.image_model'] || '';
savedChatAllowed = parseAssistantList(cfg['clinical_assistant.allowed_models']);
savedImageAllowed = parseAssistantList(cfg['clinical_assistant.allowed_image_models']);
chatRoster = chatOptions().map(function(o) { return o.value; }).filter(Boolean).concat([cfg['clinical_assistant.chat_model'] || '']);
renderAssistantCheckboxList('assistant-allowed-chat-models', chatRoster, savedChatAllowed);
renderAssistantImageModelCheckboxes([]);
renderAssistantImageModels([], window._assistantImageModelValue);
setValue('assistant-search-limit', cfg['clinical_assistant.search_limit'] || '8');
setValue('assistant-context-chars', cfg['clinical_assistant.context_chars'] || '1400');
@ -135,6 +187,7 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
.then(function(data) {
if (!data || data.success === false || !Array.isArray(data.models) ||
!data.models.every(function(m) { return m && typeof m.id === 'string' && m.id; })) throw new Error('Invalid image models');
renderAssistantImageModelCheckboxes(data.models);
renderAssistantImageModels(data.models, sel.value);
})
.catch(function() {
@ -304,7 +357,9 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
putAssistantConfig('clinical_assistant.chat_model', getValue('assistant-chat-model')),
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.translate_provider', getValue('assistant-translate-provider') || 'libretranslate'),
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() {
if (status) status.textContent = '';
showToast('Assistant settings saved', 'success');
@ -322,6 +377,12 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) {
return data;
});
}
function chatOptions() {
var chat = document.getElementById('assistant-chat-model');
var out = [];
if (chat) Array.prototype.forEach.call(chat.options, function(o) { out.push({ value: o.value }); });
return out;
}
function getValue(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; }
function setValue(id, value) { var el = document.getElementById(id); if (el) el.value = value; }
const escAssistant = adminEscapeHtml;