Saved chats Grouped the way Open WebUI groups them: Pinned, Today, Yesterday, Previous 3 days, Previous 7 days, Previous 30 days, then calendar months (with the year once it is not the current one). Chats with no usable timestamp land in Undated rather than disappearing. Sidebar toggle The collapse button used <i class="fas fa-sidebar">, which is a Font Awesome PRO icon; on the Free 6.5.0 build this app loads it rendered nothing, so the toggle has been an invisible button since it shipped. It now uses fa-table-columns, keeps aria-expanded/aria-controls in sync, flips its label between "Hide saved chats" and "Show saved chats" so the collapsed state is escapable, and animates. Acknowledgements "Окей", "Nice", "Perfect", "gracias" and friends now get the existing "What clinical question would you like me to look up?" reply instead of a retrieval and a paid generation. "yes", "sure", "no" and "more" are deliberately excluded: answers end by offering more detail, so those must still be answered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
103 lines
5.8 KiB
JavaScript
103 lines
5.8 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
async function imagesModule() {
|
|
return import(pathToFileURL(path.join(__dirname, '..', 'public/js/assistant/images.js')).href);
|
|
}
|
|
|
|
test('assistant image intent does not intercept table retrieval requests', async () => {
|
|
const { isImageRequest } = await imagesModule();
|
|
assert.equal(isImageRequest('can you show me the table?'), false);
|
|
assert.equal(isImageRequest('show me Table 13.1'), false);
|
|
});
|
|
|
|
test('assistant image intent still handles explicit visual requests', async () => {
|
|
const { isImageRequest } = await imagesModule();
|
|
assert.equal(isImageRequest('show me the diagram'), true);
|
|
assert.equal(isImageRequest('create an infographic for asthma'), true);
|
|
});
|
|
|
|
// A run of image turns used to make every acknowledgement produce another image:
|
|
// the model saw its own "I'll generate an educational image…" in the history and
|
|
// repeated it for "Окей" and "Nice". The tool is withheld for short follow-ups
|
|
// that say nothing about a picture, which does not depend on recognising an
|
|
// acknowledgement in any particular language.
|
|
function imageToolGuard() {
|
|
const fs = require('node:fs');
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
const start = src.indexOf('const IMAGE_REQUEST_PATTERN');
|
|
const end = src.indexOf('async function dispatchImageRequestFallback');
|
|
assert.ok(start > 0 && end > start, 'image guard block located');
|
|
return new Function(src.slice(start, end) + '; return { withholdImageTool, mentionsImage };')();
|
|
}
|
|
|
|
const afterImage = [
|
|
{ role: 'user', content: 'Can you illustrate with an image' },
|
|
{ role: 'assistant', content: "I'll generate an educational image illustrating the imaging findings of periventricular leukomalacia." }
|
|
];
|
|
const afterText = [
|
|
{ role: 'user', content: 'periventricular leukomalacia' },
|
|
{ role: 'assistant', content: 'PVL is a disorder of the periventricular white matter.' }
|
|
];
|
|
|
|
test('acknowledgements after an image turn do not get the image tool, in any language', () => {
|
|
const { withholdImageTool } = imageToolGuard();
|
|
for (const ack of ['ok', 'okay', 'Nice', 'Perfect', 'thanks!', 'Окей', 'bien', 'ç', '👍', 'got it']) {
|
|
assert.equal(withholdImageTool(ack, afterImage), true, JSON.stringify(ack) + ' must not trigger another image');
|
|
}
|
|
});
|
|
|
|
test('real image follow-ups keep the tool, including terse repeat requests', () => {
|
|
const { withholdImageTool } = imageToolGuard();
|
|
for (const ask of ['again', 'another one', 'ещё', 'redo', 'make it bigger with labels',
|
|
'now show the MRI diagram', 'can you add a figure for the cystic phase']) {
|
|
assert.equal(withholdImageTool(ask, afterImage), false, JSON.stringify(ask) + ' is a real image request');
|
|
}
|
|
});
|
|
|
|
test('the guard only applies after an image turn and never blocks a first request', () => {
|
|
const { withholdImageTool } = imageToolGuard();
|
|
assert.equal(withholdImageTool('Nice', afterText), false, 'no preceding image turn, nothing to repeat');
|
|
assert.equal(withholdImageTool('Окей', []), false, 'an empty conversation cannot be repeating an image');
|
|
assert.equal(withholdImageTool('draw me a diagram of the airway', afterText), false);
|
|
assert.equal(withholdImageTool('Explain the pathophysiology of PVL in preterm infants', afterImage), false,
|
|
'a substantive question is not a short acknowledgement');
|
|
});
|
|
|
|
test('the in-chat image is a clickable thumbnail, not a full-width picture', async () => {
|
|
const fs = require('node:fs');
|
|
const root = path.join(__dirname, '..');
|
|
const { createAssistantImageStore } = await import(pathToFileURL(path.join(root, 'public/js/assistant/images.js')).href);
|
|
const store = createAssistantImageStore({});
|
|
const html = store.renderGeneratedImage('/api/generated-images/synthetic.png', 'Generated teaching visual');
|
|
assert.match(html, /<img[^>]*data-assistant-open-image="img-\d+"/, 'the image itself opens the full-resolution preview');
|
|
assert.match(html, /<img[^>]*role="button"[^>]*>/, 'and announces itself as activatable');
|
|
assert.match(html, /data-assistant-open-image="img-\d+"><i class="fas fa-expand"><\/i> Preview/, 'the Preview button still works');
|
|
|
|
const css = fs.readFileSync(path.join(root, 'public/css/assistant.css'), 'utf8');
|
|
const rule = css.split('\n').find(line => line.startsWith('.assistant-generated-image img {'));
|
|
assert.ok(rule, 'the generated-image rule exists');
|
|
assert.doesNotMatch(rule, /width:100%/, 'no longer stretched to the bubble width');
|
|
assert.match(rule, /max-width:min\(100%,320px\)/, 'capped to a thumbnail');
|
|
assert.match(rule, /max-height:240px/);
|
|
assert.match(rule, /cursor:zoom-in/, 'the thumbnail invites the click');
|
|
});
|
|
|
|
test('acknowledgements ask for a clinical question instead of costing a generation', () => {
|
|
const fs = require('node:fs');
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
const line = src.split('\n').find(l => l.startsWith('var GREETING_RE'));
|
|
assert.ok(line, 'GREETING_RE located');
|
|
const RE = new Function(line + '; return GREETING_RE;')();
|
|
for (const ack of ['ok', 'Okay', 'Окей', 'ок', 'Nice', 'Perfect', 'cool', 'great',
|
|
'thanks!', 'Thank you', 'got it', 'noted', 'bien', 'gracias', 'merci', 'danke', 'спасибо']) {
|
|
assert.equal(RE.test(ack), true, JSON.stringify(ack) + ' should get the "ask me something" reply');
|
|
}
|
|
// Answers end with "would you like more detail?", so these must reach the model.
|
|
for (const real of ['yes', 'sure', 'no', 'more', 'yes please', 'tell me more',
|
|
'amoxicillin dose', 'PVL', 'what is IVH', 'okay but what about grade 3']) {
|
|
assert.equal(RE.test(real), false, JSON.stringify(real) + ' must still be answered');
|
|
}
|
|
});
|