Mobile body.assistant-workspace .assistant-layout is both later in the file and more specific than the .assistant-layout rule inside @media (max-width:640px), so phones were inheriting height: calc(100vh - 64px) — an offset for the app header that workspace mode already hides — plus grid rules on a flex container. The topbar then overlapped the content. Added a matching-specificity mobile override restoring 100dvh and the flex layout. Workspace menu in the rail The saved-chats rail gains a collapsible Workspace section listing the app's own tabs, so the assistant sidebar carries both its chats and the workspace navigation. It is built from the real .tab-btn elements rather than a second copy of the menu, so tabs added, renamed or hidden in index.html follow automatically and admin-only tabs stay hidden. Prompt Rewritten as principles instead of an enumerated rulebook: "respond to the user's latest message, not to an earlier one", "if it carries no question, ask what they would like you to look up", "never repeat a previous answer", and for the tool "use it when the latest message asks for a picture, or for a change to one you just made, and not otherwise". No example words in any language remain — the model reads the message as written. 1032 -> 788 characters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
58 lines
3.2 KiB
JavaScript
58 lines
3.2 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
|
|
// replayed it verbatim for "Окей" and "Nice". The fix is not a word list in the
|
|
// route — the model already has the tool, the prompt and the conversation, so the
|
|
// policy lives where the model reads it.
|
|
test('the prompt states principles, not an enumerated list of cases', () => {
|
|
const { DEFAULT_BEHAVIOR } = require('../src/utils/clinicalPrompts');
|
|
assert.match(DEFAULT_BEHAVIOR, /Respond to the user's latest message, not to an earlier one/);
|
|
assert.match(DEFAULT_BEHAVIOR, /If it carries no question/, 'covers acknowledgements without naming any');
|
|
assert.match(DEFAULT_BEHAVIOR, /Never repeat a previous answer/);
|
|
assert.match(DEFAULT_BEHAVIOR, /do not answer from your own knowledge/, 'grounded in retrieved context');
|
|
// No word list anywhere: the model reads the message in whatever language it
|
|
// arrives in, so no vocabulary needs maintaining here.
|
|
for (const word of ['ok', 'nice', 'thanks', 'okay']) {
|
|
assert.doesNotMatch(DEFAULT_BEHAVIOR, new RegExp('"' + word + '"', 'i'), 'no example word list');
|
|
}
|
|
});
|
|
|
|
test('the tool description says when to use it and when not', () => {
|
|
const { tools } = require('../src/utils/imageTool');
|
|
const description = tools[0].function.description;
|
|
assert.match(description, /asks for a picture, or for a change to one you just made/);
|
|
assert.match(description, /and not otherwise/);
|
|
assert.match(description, /grounded in the current clinical content/);
|
|
});
|
|
|
|
test('no hand-maintained acknowledgement or language list is left in the route', () => {
|
|
const fs = require('node:fs');
|
|
const src = fs.readFileSync(path.join(__dirname, '..', 'src/routes/clinicalAssistant.js'), 'utf8');
|
|
assert.doesNotMatch(src, /GREETING_RE/, 'greeting word list removed — the model decides');
|
|
assert.doesNotMatch(src, /IMAGE_NOUN_NON_LATIN|IMAGE_REPEAT_NON_LATIN/, 'no multilingual vocabulary lists');
|
|
assert.doesNotMatch(src, /suppressRepeatedImage/, 'no answer-inspection backstop');
|
|
// The one surviving pattern is the compatibility path for models that write
|
|
// the prompt as text; it reads the USER's message only.
|
|
const fallback = src.slice(src.indexOf('async function dispatchImageRequestFallback'));
|
|
assert.match(fallback, /isExplicitImageRequest\(prepared\.message\)/, 'gated on the user, never on the answer');
|
|
});
|