fix: keep the sidebar generated image session-only, never stored in saved chats
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 26s
Forgejo Android APK / Build signed APK (push) Successful in 4m4s

This commit is contained in:
Daniel 2026-09-07 16:23:55 +02:00
parent a7fb45515f
commit 3a8dbbcce3
3 changed files with 9 additions and 13 deletions

View file

@ -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 '';

View file

@ -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');
}

View file

@ -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);