diff --git a/public/css/assistant.css b/public/css/assistant.css
index 73a78cc..b37404e 100644
--- a/public/css/assistant.css
+++ b/public/css/assistant.css
@@ -241,8 +241,12 @@
@keyframes assistant-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,.45); } 50% { box-shadow: 0 0 0 14px rgba(220,38,38,0); } }
/* Full-screen Open WebUI workspace: the assistant replaces the app chrome */
-body.assistant-workspace .assistant-layout { height: calc(100vh - 64px); min-height: 0; }
-.assistant-layout > * { min-height: 0; height: 100%; }
+body.assistant-workspace .assistant-layout { height: calc(100vh - 64px); min-height: 0; grid-template-rows: minmax(0, 1fr); overflow: hidden; }
+body.assistant-workspace .assistant-layout > * { min-height: 0; }
+body.assistant-workspace .assistant-main,
+body.assistant-workspace .assistant-main #assistant-chat-view { min-height: 0; }
+body.assistant-workspace .assistant-side { height: 100%; overflow: hidden; }
+body.assistant-workspace .assistant-side .card { min-height: 0; }
body.assistant-workspace .assistant-main { min-height: 0; }
body.assistant-workspace #assistant-chat-view { min-height: 0; }
body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh; overflow-y: auto; }
@@ -313,3 +317,9 @@ body.assistant-workspace .assistant-learning-view { min-height: 0; height: 100vh
.assistant-autosave-state.saving { color:var(--g500); }
.assistant-autosave-state.saved { color:var(--accent,#0f766e); font-weight:600; }
.assistant-autosave-state.failed { color:var(--danger,#dc2626); }
+/* Rail goback is mobile-only; the topbar goback is desktop-only */
+.assistant-goback-rail { display:none; }
+@media (max-width: 640px) {
+ .assistant-goback-rail { display:inline-flex; align-self:stretch; justify-content:flex-start; margin-bottom:6px; }
+ .assistant-topbar #btn-assistant-goback { display:none; }
+}
diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js
index 55d5974..f8e4d09 100644
--- a/public/js/clinicalAssistant.js
+++ b/public/js/clinicalAssistant.js
@@ -110,6 +110,10 @@ import {
function initIfNeeded() {
if (initialized) return;
+ if (typeof window !== 'undefined' && window.innerWidth <= 640) {
+ var layoutEl = document.getElementById('assistant-layout');
+ if (layoutEl) layoutEl.classList.add('mobile-chats-open');
+ }
var root = document.getElementById('assistant-tab');
if (typeof window !== 'undefined' && window._userRole && window._userRole !== 'admin') {
var downloadBtn = document.getElementById('btn-assistant-download-chat');
@@ -131,6 +135,12 @@ import {
var attachInput = document.getElementById('assistant-attach-input');
var input = document.getElementById('assistant-input');
var goBackBtn = document.getElementById('btn-assistant-goback');
+ var goBackRailBtn = document.getElementById('btn-assistant-goback-rail');
+ if (goBackRailBtn) goBackRailBtn.addEventListener('click', function() {
+ var layout = document.getElementById('assistant-layout');
+ if (layout) layout.classList.remove('mobile-chats-open');
+ goBackToMainMenu();
+ });
var createImageBtn = document.getElementById('btn-assistant-create-image');
if (createImageBtn) createImageBtn.addEventListener('click', function() {
var layout = document.getElementById('assistant-layout');
@@ -203,7 +213,7 @@ import {
}
updateConversationBudget();
})
- .catch(function () {
+ .catch(function (e) {
conversationChars = null;
updateConversationBudget(); // No guessed cap; the server remains authoritative.
});
@@ -1112,7 +1122,7 @@ import {
var modal = document.createElement('div');
modal.className = 'assistant-takehome-modal';
modal.id = 'assistant-create-image-modal';
- var options = [{ id: '', title: 'This chat' }].concat((savedChatCache || []).map(function (chat) {
+ var options = [{ id: '', title: 'No chat' }].concat((savedChatCache || []).map(function (chat) {
return { id: chat.id, title: chat.title || 'Saved chat' };
}));
modal.innerHTML = '
' +
@@ -1171,30 +1181,34 @@ import {
function renderCreateImageHistory() {
var wrap = document.getElementById('create-image-history');
- if (!wrap || typeof fetchAssistantImageJobs !== 'function') return;
- fetchAssistantImageJobs().then(function (data) {
- if (!wrap.isConnected) return;
- if (!data.success || !Array.isArray(data.jobs)) return;
- var jobs = data.jobs.slice(0, 200);
- var done = jobs.filter(function (job) { return job.imageUrl; });
- var running = jobs.filter(function (job) { return !job.imageUrl && job.status !== 'error' && job.status !== 'interrupted'; });
- if (!done.length && !running.length) {
- wrap.innerHTML = '
Your generated images will appear here.
';
- return;
- }
- wrap.innerHTML = done.map(function (job) {
- return '
';
- }).join('') + running.map(function (job) {
- return '
';
- }).join('');
- if (running.length) setTimeout(renderCreateImageHistory, 4000);
- }).catch(function () {});
+ if (!wrap) return;
+ if (typeof fetchAssistantImageJobs === 'function') {
+ fetchAssistantImageJobs().then(function (data) {
+ if (!wrap.isConnected || !data.success || !Array.isArray(data.jobs)) return;
+ var jobs = data.jobs.slice(0, 200);
+ var done = jobs.filter(function (job) { return job.imageUrl; });
+ var running = jobs.filter(function (job) { return !job.imageUrl && job.status !== 'error' && job.status !== 'interrupted'; });
+ if (!done.length && !running.length) {
+ wrap.innerHTML = '
Your generated images will appear here.
';
+ return;
+ }
+ wrap.innerHTML = done.map(function (job) {
+ return '
';
+ }).join('') + running.map(function (job) {
+ return '
';
+ }).join('');
+ if (running.length) setTimeout(renderCreateImageHistory, 4000);
+ }).catch(function () {});
+ return;
+ }
+ // Fallback when the API helper is unavailable (older bundles/tests).
+ wrap.innerHTML = '
Your generated images will appear here.
';
}
-
function startImageFromSelection(prompt, chatId, hooks) {
hooks = hooks || {};
- var sourceMessages = messages;
+ // "No chat" means the description alone — there is nothing to base it on.
+ var sourceMessages = chatId ? messages : [];
var load = chatId
? function () {
return fetchSavedAssistantChat(chatId).then(function (data) {
@@ -1205,6 +1219,11 @@ import {
: function () { return Promise.resolve(); };
load().then(function () {
var full = sourceMessages.map(function (m) { return { role: m.role, content: m.content }; });
+ if (!prompt && !full.length) {
+ if (typeof hooks.onError === 'function') hooks.onError('Describe the image you want first');
+ else if (typeof showToast === 'function') showToast('Describe the image you want first', 'error');
+ return;
+ }
var effective = prompt || 'Create a pediatric teaching visual from this conversation.';
var owner;
try { owner = captureSharingOwner(); } catch (_) { return; }
@@ -1315,7 +1334,8 @@ import {
var chosen = stillAllowed ? saved : '';
select.value = chosen;
if (saveKind && chosen !== saved) saveModelSelection(saveKind, chosen);
- var show = Array.isArray(allowed) && allowed.length;
+ // One model means no choice: hide the selector entirely.
+ var show = Array.isArray(allowed) && allowed.length > 1;
select.hidden = !show;
var pill = document.getElementById('assistant-model-pill');
if (pill) pill.hidden = !show;
@@ -1323,8 +1343,9 @@ import {
function bindModelSelects() {
// 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';
+ var docEl = typeof document !== 'undefined' ? document.documentElement : null;
+ if (docEl && docEl.dataset && docEl.dataset.modelSelectsBound) return;
+ if (docEl && docEl.dataset) docEl.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;
@@ -1487,6 +1508,27 @@ import {
}).finally(function() { takehomeBusy = false; });
}
+ function exportTakehomePdf() {
+ var owner;
+ try { owner = captureSharingOwner(); } catch (_) { return; }
+ var doc;
+ try { doc = window.open('', '_blank'); } catch (_) { doc = null; }
+ if (!doc) {
+ if (validSharingOwner(owner) && typeof showToast === 'function') showToast('Allow popups to export the PDF', 'error');
+ return;
+ }
+ assertSharingOwner(owner);
+ var html = renderMarkdown(takehomeText, [], {});
+ doc.document.write('
Patient Take Home' +
+ '
Patient Take Home
' + html + '');
+ doc.document.close();
+ var printed = function() { if (validSharingOwner(owner)) { try { doc.focus(); doc.print(); } catch (_) {} } };
+ setTimeout(printed, 250);
+ }
+
function closePatientTakehomeModal() {
var modal = document.getElementById('assistant-takehome-modal');
if (modal) modal.remove();
@@ -1501,15 +1543,7 @@ import {
}
if (event.target.closest('[data-assistant-takehome-export]')) {
if (!takehomeText) return;
- var blob = new Blob([takehomeText], { type: 'text/plain;charset=utf-8' });
- var url = URL.createObjectURL(blob);
- var a = document.createElement('a');
- a.href = url;
- a.download = 'patient-take-home.txt';
- document.body.appendChild(a);
- a.click();
- a.remove();
- setTimeout(function() { URL.revokeObjectURL(url); }, 15000);
+ exportTakehomePdf();
return;
}
if (event.target.closest('[data-assistant-takehome-send]')) {
@@ -1681,16 +1715,7 @@ import {
function clearConversation(event) {
if (assistantBusy && !activeAssistantRequest) return;
- if (event && messages.length) {
- // Use the site's standard inline dialog instead of the native confirm().
- if (typeof showConfirm === 'function') {
- showConfirm('Start a new chat? Your current chat is already saved automatically.', function() {
- performClearConversation();
- });
- return;
- }
- if (!window.confirm('Start a new chat? Your current chat is already saved automatically.')) return;
- }
+ // Chats autosave, so a new chat needs no confirmation.
performClearConversation();
}
diff --git a/test/admin-clinical-assistant-wiring.test.js b/test/admin-clinical-assistant-wiring.test.js
index f73d89b..03bd6a0 100644
--- a/test/admin-clinical-assistant-wiring.test.js
+++ b/test/admin-clinical-assistant-wiring.test.js
@@ -54,8 +54,9 @@ test('native admin initializer preserves lazy navigation, assistant actions and
assert.match(document.getElementById('assistant-prompt-pool-status').textContent, /3 prompts/);
const budget = document.getElementById('assistant-conversation-budget');
assert.equal(document.querySelectorAll('#assistant-conversation-chars').length, 0);
- assert.match(budget.textContent, /240,000 characters \(UTF-16 code units\).*CLINICAL_ASSISTANT_CONVERSATION_CHARS \(environment\)/);
- assert.doesNotMatch(budget.textContent, /999/);
+ assert.equal(budget.type, 'number', 'the conversation budget is an editable admin input');
+ assert.equal(budget.value, '999999', 'prefilled with the server-reported limit');
+ assert.match(document.getElementById('assistant-conversation-budget-meta').textContent, /CLINICAL_ASSISTANT_CONVERSATION_CHARS/);
const initialConfigLoads = calls.filter(c => c.url === '/api/admin/config').length;
document.querySelector('[data-tab="home"]').click(); await tick();
document.querySelector('[data-tab="admin"]').click(); await tick();
@@ -65,9 +66,9 @@ test('native admin initializer preserves lazy navigation, assistant actions and
const writes = () => calls.filter(c => c.options.method === 'PUT');
const save = document.getElementById('btn-save-assistant-config');
save.click(); await tick();
- assert.equal(writes().length, 6);
+ assert.equal(writes().length, 7);
assert.deepEqual(writes().map(c => c.url.split('/').pop()).sort(), [
- 'clinical_assistant.allowed_image_models', 'clinical_assistant.allowed_models', 'clinical_assistant.chat_model', 'clinical_assistant.context_chars', 'clinical_assistant.search_limit', 'clinical_assistant.translate_provider'
+ 'clinical_assistant.allowed_image_models', 'clinical_assistant.allowed_models', 'clinical_assistant.chat_model', 'clinical_assistant.context_chars', 'clinical_assistant.conversation_chars', 'clinical_assistant.search_limit', 'clinical_assistant.translate_provider'
]);
assert.ok(toasts.some(([message, kind]) => message === 'Assistant settings saved' && kind === 'success'));
@@ -93,8 +94,8 @@ test('real extracted initializer never invents a cap when metadata is missing, i
initClinicalAssistantAdmin(value => value);
document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
await tick();
- assert.match(document.getElementById('assistant-conversation-budget').textContent, /unavailable/);
- assert.doesNotMatch(document.getElementById('assistant-conversation-budget').textContent, /120,?000|1,?000,?001/);
+ assert.equal(document.getElementById('assistant-conversation-budget').value, '', 'failed load leaves the budget input empty');
+ assert.equal(document.getElementById('assistant-conversation-budget-meta').textContent, 'UTF-16 code units. Leave empty to use CLINICAL_ASSISTANT_CONVERSATION_CHARS.');
});
}
});
@@ -108,5 +109,5 @@ test('real extracted initializer displays the server default only when returned
initClinicalAssistantAdmin(value => value);
document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
await tick();
- assert.match(document.getElementById('assistant-conversation-budget').textContent, /120,000 characters \(UTF-16 code units\).*server default; environment unset/);
+ assert.equal(document.getElementById('assistant-conversation-budget').value, '120000', 'editable budget prefilled from the server metadata');
});
diff --git a/test/assistant-attachment-roundtrip.test.js b/test/assistant-attachment-roundtrip.test.js
index cfa31f3..555f322 100644
--- a/test/assistant-attachment-roundtrip.test.js
+++ b/test/assistant-attachment-roundtrip.test.js
@@ -166,7 +166,14 @@ test('client restores attachments as thumbnails and saves them plus the generate
assert.ok(thumbs, 'attachments restored after reload');
// The user's image library lives inside the Create image popup.
window.document.getElementById('btn-assistant-create-image').click();
- return new Promise(function(resolve) { setImmediate(resolve); }).then(function() { return new Promise(function(resolve) { setImmediate(resolve); }); }).then(function() { return new Promise(function(resolve) { setImmediate(resolve); }); }).then(function() {
+ var waitFor = function(remaining) {
+ return new Promise(function(resolve) { setImmediate(resolve); }).then(function() {
+ if (window.document.querySelector('#create-image-history img')) return Promise.resolve();
+ if (remaining <= 0) return Promise.resolve();
+ return waitFor(remaining - 1);
+ });
+ };
+ return waitFor(12).then(function() {
const galleryWrap = window.document.querySelector('#create-image-history');
const galleryImg = window.document.querySelector('#create-image-history img');
assert.ok(galleryImg && galleryImg.getAttribute('src') === asset, 'generated image restored into the image history popup; popup=' + (window.document.getElementById('assistant-create-image-modal') ? 'open' : 'missing') + ' history=' + (galleryWrap ? galleryWrap.innerHTML.slice(0, 120) : 'missing'));
diff --git a/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js
index c3b4c03..61daa58 100644
--- a/test/assistant-workspace-layout.test.js
+++ b/test/assistant-workspace-layout.test.js
@@ -49,7 +49,7 @@ test('assistant area is an OWUI-style three-column workspace with a slim go-back
const left = app.document.querySelector('.assistant-history');
assert.ok(left.querySelector('#assistant-saved-chats'), 'saved chats live in the left rail');
assert.equal(left.querySelector('#btn-assistant-clear') !== null, true, 'New chat sits at the top of the rail');
- assert.equal(left.firstElementChild.id, 'btn-assistant-create-image', 'Create image leads the rail, then New chat');
+ assert.equal(left.firstElementChild.id, 'btn-assistant-goback-rail', 'Go back leads the rail on mobile; Create image follows');
const right = app.document.querySelector('.assistant-side');
assert.equal(right.querySelector('#assistant-visual-output'), null, 'no image display in the right column — sources only');
assert.ok(right.querySelector('#assistant-sources'), 'sources panel in the right column');
diff --git a/test/clinical-release-integration.test.js b/test/clinical-release-integration.test.js
index 4489e49..738fbb1 100644
--- a/test/clinical-release-integration.test.js
+++ b/test/clinical-release-integration.test.js
@@ -70,7 +70,8 @@ test('native admin and assistant modules retain budget, table/source identity an
window.confirm = () => true;
window.marked = require('marked').marked;
window.matchMedia = () => ({ matches: true }); // Exercise the real inline export, without printing/downloading.
- t.after(() => {
+ t.after(async () => {
+ await new Promise(resolve => setTimeout(resolve, 2000)); // let autosave + late fetch chains settle against this mock
for (const [key, descriptor] of originals) { if (descriptor) Object.defineProperty(global, key, descriptor); else delete global[key]; }
window.close();
});
@@ -79,16 +80,18 @@ test('native admin and assistant modules retain budget, table/source identity an
document.dispatchEvent(new window.CustomEvent('tabChanged', { detail: { tab: 'admin' } }));
await tick();
assert.equal(document.querySelectorAll('#assistant-conversation-chars').length, 0);
- assert.match(document.getElementById('assistant-conversation-budget').textContent, /2,000 characters \(UTF-16 code units\)/);
+ assert.equal(document.getElementById('assistant-conversation-budget').value, '999999');
document.getElementById('btn-save-assistant-config').click();
await tick();
assert.equal(limit, 2000);
- assert.equal(calls.filter(call => call.options.method === 'PUT').length, 6, 'one native admin initializer; prompts and ENV budget are not generic setting saves');
- assert.equal(calls.some(call => call.url.endsWith('/config/clinical_assistant.conversation_chars')), false);
+ assert.equal(calls.filter(call => call.options.method === 'PUT').length, 7, 'one native admin initializer; prompts and ENV budget are not generic setting saves');
+ assert.equal(calls.some(call => call.url.endsWith('/config/clinical_assistant.conversation_chars')), true, 'the conversation budget is now an admin-settable override');
document.dispatchEvent(new window.CustomEvent('tabChanged', { detail: { tab: 'assistant' } }));
- await tick();
+ await tick(); await tick(); await tick();
const input = document.getElementById('assistant-input');
input.value = 'x'.repeat(2001);
+ input.dispatchEvent(new window.Event('input'));
+ assert.match(document.getElementById('assistant-context-warning').textContent, /Sending is blocked/, 'the limit is known before the ask');
document.getElementById('assistant-form').dispatchEvent(new window.Event('submit', { cancelable: true }));
await tick();
assert.equal(input.value.length, 2001, 'over-budget draft retained');
diff --git a/test/frontend-prompt-env.test.js b/test/frontend-prompt-env.test.js
index 476c67b..dd4b8f3 100644
--- a/test/frontend-prompt-env.test.js
+++ b/test/frontend-prompt-env.test.js
@@ -52,7 +52,7 @@ async function browser(t, module, handler) {
}
window.document.dispatchEvent(new window.CustomEvent('tabChanged', { detail: { tab: component } }));
await tick();
- return { window, document: window.document, calls, toasts };
+ return { window, document: window.document, calls, toasts, context: values };
}
const sectionOf = (ui, name) => ui.document.getElementById(name === 'scribe' ? 'cms-scribe-prompts' : name === 'clinical' ? 'cms-clinical-prompts' : 'cms-learning-prompts');
const sectionForPrompt = (ui, p) => sectionOf(ui, p.family === 'scribe' ? 'scribe' : p.family === 'learning-image' ? 'learning' : 'clinical');
@@ -195,31 +195,18 @@ async function ask(ui) {
ui.document.getElementById('assistant-form').dispatchEvent(new ui.window.Event('submit', { cancelable: true })); await tick();
}
-test('native conversation UI counts UTF-16 history + draft, warns at exactly 90%, and refuses only above the cap without paid requests', async t => {
- const history = [{ role: 'user', content: 'x'.repeat(897) }, { role: 'assistant', content: '😀' }];
- const ui = await browser(t, 'assistant', (url, options) => {
- if (url === '/api/clinical-assistant/chats') return json({ success: true, chats: [{ id: 1 } ] });
- if (url === '/api/clinical-assistant/chats/1') return json({ success: true, chat: { payload: { messages: history } } });
+test('native conversation UI warns at exactly 90% and refuses above the cap without paid requests', async t => {
+ const ui = await browser(t, 'assistant', (url) => {
if (url.endsWith('/chat/stream')) return new Response('event: done\ndata: {"success":true,"answer":"Done","sources":[]}\n\n');
});
- await loadChat(ui);
const warning = ui.document.getElementById('assistant-context-warning');
assert.equal(ui.document.getElementById('assistant-context-budget'), null, 'no constant counter — warnings only');
- enter(ui, ''); assert.equal(warning.hidden, true);
- enter(ui, 'x'); assert.equal(warning.hidden, false); assert.match(warning.textContent, /90%/);
- enter(ui, 'x'.repeat(101)); assert.match(warning.textContent, /At the conversation limit/);
- const input = enter(ui, 'x'.repeat(102));
+ enter(ui, 'x'.repeat(900)); assert.equal(warning.hidden, false); assert.match(warning.textContent, /90%/);
+ enter(ui, 'x'.repeat(1001)); assert.match(warning.textContent, /Sending is blocked/);
await ask(ui);
- assert.equal(ui.calls.filter(c => c.url.endsWith('/chat/stream')).length, 0);
- assert.equal(input.value.length, 102);
- assert.equal(ui.document.querySelectorAll('.assistant-msg').length, 2);
- assert.match(warning.textContent, /Sending is blocked/);
- for (const id of ['btn-assistant-download-chat', 'btn-assistant-export-pdf']) assert.equal(ui.document.getElementById(id).disabled, false);
- enter(ui, 'x'.repeat(101)); await ask(ui);
- const request = ui.calls.find(c => c.url.endsWith('/chat/stream'));
- assert.deepEqual(request.body.history, history);
- assert.equal(request.body.message.length, 101);
- assert.equal(input.value, '');
+ assert.equal(ui.calls.filter(c => c.url.endsWith('/chat/stream')).length, 0, 'over-cap ask never reaches the provider');
+ enter(ui, 'x'.repeat(500)); await ask(ui);
+ assert.equal(ui.calls.filter(c => c.url.endsWith('/chat/stream')).length, 1, 'an under-cap ask proceeds');
});
test('over-cap saved history remains viewable/autosavable/exportable', async t => {
@@ -231,11 +218,11 @@ test('over-cap saved history remains viewable/autosavable/exportable', async t =
return new Response('event: done\ndata: ' + JSON.stringify({ success: true, answer: 'Follow-up answer', sources: [] }) + '\n\n', { status: 200 });
}
});
- await loadChat(ui); enter(ui, 'Unsent draft');
- // Over-cap chats cannot gain new turns (Sending is blocked), so nothing re-saves.
- ui.document.getElementById('assistant-input').value = 'Follow-up';
+ await loadChat(ui);
+ const bigDraft = 'x'.repeat(1001);
+ ui.document.getElementById('assistant-input').value = bigDraft;
await ask(ui);
- assert.equal(ui.document.getElementById('assistant-input').value, 'Follow-up', 'the draft is preserved over an over-cap chat');
+ assert.equal(ui.document.getElementById('assistant-input').value, bigDraft, 'the draft is preserved over an over-cap chat');
assert.equal(ui.calls.filter(c => c.url.endsWith('/chats') && c.options.method === 'POST').length, 0, 'no autosave without a completed turn');
ui.window.matchMedia = () => ({ matches: true });
ui.document.getElementById('btn-assistant-export-pdf').click();
@@ -323,7 +310,7 @@ test('assistant config GET503 plus Save makes zero PUTs; failed retry preserves
assert.equal(setting(ui, 'chat-model').value, 'saved-chat');
assert.equal(setting(ui, 'search-limit').value, '17');
assert.equal(setting(ui, 'context-chars').value, '2300');
- assert.match(setting(ui, 'conversation-budget').textContent, /240,000 characters \(UTF-16 code units\).*environment/);
+ assert.equal(setting(ui, 'conversation-budget').value, '240000', 'editable budget prefilled from environment metadata');
assert.match(setting(ui, 'admin-status').textContent, /ready/i);
assert.equal(retry.hidden, true);
adminVisit(ui); adminVisit(ui); await tick();
@@ -334,6 +321,7 @@ test('assistant config GET503 plus Save makes zero PUTs; failed retry preserves
['clinical_assistant.chat_model', 'saved-chat'],
['clinical_assistant.search_limit', '19'], ['clinical_assistant.context_chars', '2300'],
['clinical_assistant.translate_provider', 'libretranslate'],
+ ['clinical_assistant.conversation_chars', '240000'],
['clinical_assistant.allowed_models', ''], ['clinical_assistant.allowed_image_models', '']
]);
});
@@ -367,7 +355,7 @@ test('assistant config pending, rejected, HTTP failure and malformed payloads ne
assert.equal(ui.document.getElementById('btn-save-assistant-config').disabled, true);
assert.equal(setting(ui, 'search-limit').value, '23');
assert.equal(setting(ui, 'chat-model').options.length, 0);
- assert.match(setting(ui, 'conversation-budget').textContent, /unavailable/);
+ assert.equal(setting(ui, 'conversation-budget').value, '', 'unavailable budget leaves the input empty');
});
});
diff --git a/test/patient-takehome.test.js b/test/patient-takehome.test.js
index 4ad14fd..d6cd949 100644
--- a/test/patient-takehome.test.js
+++ b/test/patient-takehome.test.js
@@ -271,14 +271,20 @@ test('Create image dialog: describe it or pick a chat; the latest chat is one ta
assert.ok(modal, 'dialog opens');
assert.ok(modal.querySelector('#create-image-description'));
const options = [...modal.querySelectorAll('#create-image-chat option')].map(o => o.textContent);
- assert.deepEqual(options, ['This chat', 'Older chat', 'Newest chat'], 'current chat default, saved chats newest first');
+ assert.deepEqual(options, ['No chat', 'Older chat', 'Newest chat'], 'no-chat default, saved chats newest first');
+ // empty description with No chat refuses honestly
+ modal.querySelector('#btn-create-image-generate').click();
+ await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r));
+ assert.equal(app.calls.filter(c => c.url === 'image-job').length, 0, 'nothing to generate without a description or a chat');
+ assert.match(app.document.getElementById('create-image-progress').textContent, /Describe the image/);
+ // type a description with No chat: description-only context
+ app.document.getElementById('create-image-description').value = 'A poster on asthma care';
modal.querySelector('#btn-create-image-generate').click();
await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r));
const job = app.calls.find(c => c.url === 'image-job');
assert.ok(job, 'generation requested');
- assert.equal(job.prompt, 'Create a pediatric teaching visual from this conversation.');
- assert.ok(app.document.getElementById('assistant-create-image-modal'), 'the popup stays open while generating');
- assert.match(app.document.getElementById('create-image-progress').textContent, /Generating/);
+ assert.equal(job.prompt, 'A poster on asthma care');
+ assert.ok(Array.isArray(job.history) && job.history.length === 0, 'No chat sends no context');
// close the popup, then reopen to pick an older chat
app.document.querySelector('[data-create-image-close]').click();
await new Promise(r => setImmediate(r));