fix: keep the sidebar generated image session-only, never stored in saved chats
This commit is contained in:
parent
a7fb45515f
commit
3a8dbbcce3
3 changed files with 9 additions and 13 deletions
|
|
@ -689,12 +689,12 @@ import {
|
||||||
var titleEl = document.getElementById('assistant-save-title');
|
var titleEl = document.getElementById('assistant-save-title');
|
||||||
var title = String(titleEl && titleEl.value || deriveChatTitle()).trim() || deriveChatTitle();
|
var title = String(titleEl && titleEl.value || deriveChatTitle()).trim() || deriveChatTitle();
|
||||||
setBusy(true, 'Saving chat...');
|
setBusy(true, 'Saving chat...');
|
||||||
|
// Sidebar image is session-only and is deliberately not saved with the chat.
|
||||||
return saveAssistantChat({
|
return saveAssistantChat({
|
||||||
title: title,
|
title: title,
|
||||||
messages: messages,
|
messages: messages,
|
||||||
sources: lastSources,
|
sources: lastSources,
|
||||||
lastAnswer: lastAnswer,
|
lastAnswer: lastAnswer
|
||||||
generatedImage: imageForSavedChatPayload(lastGeneratedImageSrc)
|
|
||||||
})
|
})
|
||||||
.then(function (data) {
|
.then(function (data) {
|
||||||
setBusy(false, 'Ready');
|
setBusy(false, 'Ready');
|
||||||
|
|
@ -882,7 +882,7 @@ import {
|
||||||
|
|
||||||
function downloadTranscript() {
|
function downloadTranscript() {
|
||||||
var payload = { version: 2, title: deriveChatTitle(), messages: messages, sources: lastSources,
|
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 url = URL.createObjectURL(new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' }));
|
||||||
var link = document.createElement('a');
|
var link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
|
|
@ -925,11 +925,6 @@ import {
|
||||||
.finally(function() { setBusy(false, 'Ready'); });
|
.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) {
|
function lastAssistantMessage(items) {
|
||||||
for (var i = items.length - 1; i >= 0; i--) if (items[i].role === 'assistant') return items[i].content;
|
for (var i = items.length - 1; i >= 0; i--) if (items[i].role === 'assistant') return items[i].content;
|
||||||
return '';
|
return '';
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,10 @@ function savedChatPayload(body) {
|
||||||
messages,
|
messages,
|
||||||
sources: savedSources(body.sources),
|
sources: savedSources(body.sources),
|
||||||
lastAnswer: body.lastAnswer || '',
|
lastAnswer: body.lastAnswer || '',
|
||||||
generatedImage: savedImage(body.generatedImage),
|
|
||||||
savedAt: new Date().toISOString()
|
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) {
|
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');
|
throw failure('Saved chat exceeds the 8 MiB storage limit. Nothing was saved or truncated; download the complete transcript instead.', 413, 'SAVED_CHAT_LIMIT');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
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 app = server();
|
||||||
const content = ' Preserve indent\n\n| Item | Unit |\n|---|---|\n| 0.25 | mg/kg |\n' + '保留'.repeat(6500);
|
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 }));
|
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', {});
|
const reopened = await app.request('get', '/clinical-assistant/chats/:id', {});
|
||||||
assert.deepEqual(JSON.parse(JSON.stringify(reopened.body.chat.payload.messages)), messages);
|
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.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(reopened.body.chat.payload.sources.length, 31);
|
||||||
assert.equal((await app.request('get', '/clinical-assistant/chats/:id', {}, 8)).statusCode, 404);
|
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) }] });
|
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();
|
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 ui = browserUI();
|
||||||
const messages = Array.from({ length: 20 }, (_, i) => ({ role: i % 2 ? 'assistant' : 'user', content: 'Turn ' + i + (i === 0 ? 'x'.repeat(1500) : ''), sources: [] }));
|
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 });
|
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.context.messages[20].content, 'New question');
|
||||||
assert.equal(ui.document.getElementById('assistant-input').value, '');
|
assert.equal(ui.document.getElementById('assistant-input').value, '');
|
||||||
await ui.context.saveCurrentChat();
|
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);
|
assert.equal(ui.calls.save[0].messages.length, 22);
|
||||||
await ui.context.requestHandoff();
|
await ui.context.requestHandoff();
|
||||||
assert.equal(ui.calls.handoff.length, 1);
|
assert.equal(ui.calls.handoff.length, 1);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue