From 3a8dbbcce3dc64be0ffc26ee85ba7f58caa1c937 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 7 Sep 2026 16:23:55 +0200 Subject: [PATCH] fix: keep the sidebar generated image session-only, never stored in saved chats --- public/js/clinicalAssistant.js | 11 +++-------- src/utils/clinicalConversation.js | 3 ++- test/clinical-conversation.test.js | 8 ++++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index c451a1c..e323fc9 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -689,12 +689,12 @@ import { var titleEl = document.getElementById('assistant-save-title'); var title = String(titleEl && titleEl.value || deriveChatTitle()).trim() || deriveChatTitle(); setBusy(true, 'Saving chat...'); + // Sidebar image is session-only and is deliberately not saved with the chat. return saveAssistantChat({ title: title, messages: messages, sources: lastSources, - lastAnswer: lastAnswer, - generatedImage: imageForSavedChatPayload(lastGeneratedImageSrc) + lastAnswer: lastAnswer }) .then(function (data) { setBusy(false, 'Ready'); @@ -882,7 +882,7 @@ import { function downloadTranscript() { var payload = { version: 2, title: deriveChatTitle(), messages: messages, sources: lastSources, - lastAnswer: lastAnswer, generatedImage: lastGeneratedImageSrc, savedAt: new Date().toISOString() }; + lastAnswer: lastAnswer, savedAt: new Date().toISOString() }; var url = URL.createObjectURL(new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })); var link = document.createElement('a'); link.href = url; @@ -925,11 +925,6 @@ import { .finally(function() { setBusy(false, 'Ready'); }); } - function imageForSavedChatPayload(src) { - src = String(src || ''); - return src; // Preserve inline generated images; the server validates supported image formats. - } - function lastAssistantMessage(items) { for (var i = items.length - 1; i >= 0; i--) if (items[i].role === 'assistant') return items[i].content; return ''; diff --git a/src/utils/clinicalConversation.js b/src/utils/clinicalConversation.js index 2511d79..e3182b3 100644 --- a/src/utils/clinicalConversation.js +++ b/src/utils/clinicalConversation.js @@ -107,9 +107,10 @@ function savedChatPayload(body) { messages, sources: savedSources(body.sources), lastAnswer: body.lastAnswer || '', - generatedImage: savedImage(body.generatedImage), savedAt: new Date().toISOString() }; + // Sidebar image is session-only: validated when supplied but never stored in saved chats. + if (body.generatedImage !== undefined && body.generatedImage !== null && body.generatedImage !== '') savedImage(body.generatedImage); if (Buffer.byteLength(JSON.stringify(payload), 'utf8') > MAX_SAVED_CHAT_BYTES) { throw failure('Saved chat exceeds the 8 MiB storage limit. Nothing was saved or truncated; download the complete transcript instead.', 413, 'SAVED_CHAT_LIMIT'); } diff --git a/test/clinical-conversation.test.js b/test/clinical-conversation.test.js index 3a683ac..d77a50b 100644 --- a/test/clinical-conversation.test.js +++ b/test/clinical-conversation.test.js @@ -120,7 +120,7 @@ test('real accepted route retains early turns and late corrections in rewrite an assert.equal(long.statusCode, 200, 'The old independent 4,000-character clipping boundary is gone'); }); -test('real save/reopen keeps 101 turns, Markdown, Unicode, full source metadata and generated image', async () => { +test('real save/reopen keeps 101 turns, Markdown, Unicode and full source metadata; the sidebar image is not stored', async () => { const app = server(); const content = ' Preserve indent\n\n| Item | Unit |\n|---|---|\n| 0.25 | mg/kg |\n' + '保留'.repeat(6500); const sources = Array.from({ length: 31 }, (_, index) => ({ number: index + 1, title: 'Title ' + index, excerpt: content, resource: 'source-' + index, page: index + 1 })); @@ -131,7 +131,7 @@ test('real save/reopen keeps 101 turns, Markdown, Unicode, full source metadata const reopened = await app.request('get', '/clinical-assistant/chats/:id', {}); assert.deepEqual(JSON.parse(JSON.stringify(reopened.body.chat.payload.messages)), messages); assert.equal(reopened.body.chat.payload.lastAnswer, content); - assert.equal(reopened.body.chat.payload.generatedImage, png); + assert.equal(reopened.body.chat.payload.generatedImage, undefined, 'The sidebar image is session-only'); assert.equal(reopened.body.chat.payload.sources.length, 31); assert.equal((await app.request('get', '/clinical-assistant/chats/:id', {}, 8)).statusCode, 404); const oversized = await app.request('post', '/clinical-assistant/chats', { messages: [{ role: 'user', content: 'x'.repeat(policy.MAX_SAVED_CHAT_BYTES) }] }); @@ -205,7 +205,7 @@ test('actual UI blocks over-budget input without clearing draft/history or autom ui.dom.window.close(); }); -test('actual UI sends complete prior history exactly once, preserves images on save, and offers explicit handoff', async () => { +test('actual UI sends complete prior history exactly once, does not save the sidebar image, and offers explicit handoff', async () => { const ui = browserUI(); const messages = Array.from({ length: 20 }, (_, i) => ({ role: i % 2 ? 'assistant' : 'user', content: 'Turn ' + i + (i === 0 ? 'x'.repeat(1500) : ''), sources: [] })); ui.context.restoreSavedChat({ messages, lastAnswer: 'Previous answer.', generatedImage: png }); @@ -217,7 +217,7 @@ test('actual UI sends complete prior history exactly once, preserves images on s assert.equal(ui.context.messages[20].content, 'New question'); assert.equal(ui.document.getElementById('assistant-input').value, ''); await ui.context.saveCurrentChat(); - assert.equal(ui.calls.save[0].generatedImage, png); + assert.equal(ui.calls.save[0].generatedImage, undefined); assert.equal(ui.calls.save[0].messages.length, 22); await ui.context.requestHandoff(); assert.equal(ui.calls.handoff.length, 1);