diff --git a/public/components/admin.html b/public/components/admin.html index 385d984d..dc8923eb 100644 --- a/public/components/admin.html +++ b/public/components/admin.html @@ -296,6 +296,7 @@ Model availability
Models users may select in the Clinical Assistant. Leave a list empty to keep only the configured model.
+This list is what the gateway already offers. To make a new model appear here, add it in AI Model Management above — search the gateway, press +, and it shows up in this list.
Click Search to list the image models your gateway advertises. Unlike a voice, an image model has no single default: pick which workflow uses it under Clinical Assistant / Learning, and tick it there to offer it to users in chat.
+Press Test on a model above, or type an id the gateway does not advertise.
+Querying provider API...
'; if (hint) hint.style.display = 'none'; + // The Clinical Assistant lists models but cannot add them; adding happens in + // AI Model Management, one section up. Without this the two sections look + // unrelated and there is no visible way to get a new model into the list. + document.addEventListener('click', function(event) { + var jump = event.target.closest && event.target.closest('[data-admin-jump]'); + if (!jump) return; + event.preventDefault(); + var target = document.getElementById(jump.getAttribute('data-admin-jump')); + if (!target) return; + target.scrollIntoView({ behavior: 'smooth', block: 'center' }); + target.focus({ preventScroll: true }); + }); + fetch('/api/admin/config/models/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() }) .then(function(r) { return r.json(); }) @@ -1462,6 +1475,100 @@ initImageSettings(); } +// ============================================================ +// ADMIN IMAGE MODEL MANAGEMENT +// ============================================================ +// Unlike TTS and STT there is no single default to Set: an image model is +// chosen per workflow, so discovery here ends in a Test, and the workflow +// pickers in the Clinical Assistant card consume the same discovery call. +// Nothing loads on tab entry: the workflow pickers already make the one +// discovery call opening Admin needs, so this card asks only when searched. +{ + document.addEventListener('click', function(e) { + if (e.target.closest('#btn-discover-image')) discoverImageModels(); + if (e.target.closest('#btn-test-image-model')) testImageModel((document.getElementById('admin-image-test-model') || {}).value || ''); + var pick = e.target.closest('.admin-image-test-btn'); + if (pick) { + var field = document.getElementById('admin-image-test-model'); + if (field) field.value = pick.dataset.id; + testImageModel(pick.dataset.id, pick); + } + }); + document.addEventListener('keydown', function(e) { + if (e.target.id === 'admin-image-search' && e.key === 'Enter') { e.preventDefault(); discoverImageModels(); } + if (e.target.id === 'admin-image-test-model' && e.key === 'Enter') { e.preventDefault(); testImageModel(e.target.value || ''); } + }); + + const esc = adminEscapeHtml; + + function discoverImageModels() { + var search = (document.getElementById('admin-image-search') || {}).value || ''; + var container = document.getElementById('admin-image-discovered'); + var hint = document.getElementById('admin-image-discover-hint'); + if (!container) return; + container.innerHTML = 'Querying provider...
'; + if (hint) hint.style.display = 'none'; + + fetch('/api/admin/config/image-models/discover?q=' + encodeURIComponent(search), { headers: getAuthHeaders() }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (!data.success) { + container.innerHTML = 'Error: ' + esc(data.error || 'Unknown') + '
'; + return; + } + var items = data.models || []; + var badge = document.getElementById('admin-image-provider-badge'); + if (badge && !search) badge.textContent = data.count + ' available'; + if (items.length === 0) { + container.innerHTML = 'No image models found' + (search ? ' matching "' + esc(search) + '"' : '') + '
'; + return; + } + container.innerHTML = 'Found ' + data.count + ' image model' + (data.count === 1 ? '' : 's') + '
' + + items.map(function(m) { + return 'Request failed: ' + esc(err.message) + '
'; + }); + } + + function testImageModel(modelId, btn) { + var id = String(modelId || '').trim(); + var result = document.getElementById('admin-image-test-result'); + if (!id) { + if (result) result.innerHTML = 'Enter or pick a model id first.'; + return; + } + if (btn) adminSetButtonText(btn, '...', true); + if (result) result.innerHTML = ' Generating with ' + esc(id) + '... this can take a minute.'; + fetch('/api/admin/config/image-models/test', { + method: 'POST', headers: getAuthHeaders(), body: JSON.stringify({ modelId: id }) + }) + .then(function(r) { return r.json(); }) + .then(function(data) { + if (btn) adminSetButtonText(btn, 'Test', false); + if (!data.success) { + if (result) result.innerHTML = '' + esc(data.error || 'Image test failed') + ''; + return; + } + var src = data.imageUrl || (data.base64 ? ('data:image/png;base64,' + data.base64) : ''); + if (result) { + result.innerHTML = esc(id) + ' works (' + data.duration + ' ms).' + + (src ? '