From fce05a2749cb17d59d092a2e0483f37639e659ce Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 12 Sep 2026 16:27:21 +0200 Subject: [PATCH] fix: the active library view is unmistakable, and a leading question cannot mislead the vision model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents and Images were two buttons of the same weight, which reads as two buttons rather than as a current view and an alternative. The active one now carries the surface, the weight and a hairline — keyed off aria-selected, so the visible highlight and what a screen reader announces cannot disagree. The look_at_image question is written by a model that has not seen the image, from what the user said, so it can presume something that is not there — "describe this rash" about a photograph of a drug chart. Answering the presumption would send it back as fact. The seeing model is now told the question may assume something absent, and to say so first and describe what is actually there. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- public/components/my-resources.html | 8 +++----- public/css/styles.css | 11 +++++++++++ public/js/myResources.js | 3 ++- src/utils/visionTool.js | 13 ++++++++++++- test/vision-tool.test.js | 10 ++++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/public/components/my-resources.html b/public/components/my-resources.html index a2e2a9f8..a163086a 100644 --- a/public/components/my-resources.html +++ b/public/components/my-resources.html @@ -154,11 +154,9 @@ shown; Images is every picture this account has generated, wherever it was made, because a figure is worth keeping after the deck it was drawn for has been replaced. --> -
- - +
+ +
diff --git a/public/css/styles.css b/public/css/styles.css index 9bb44aae..77523032 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -1555,3 +1555,14 @@ button, a, .btn-sm, .btn-generate, .btn-send, .tab-btn, input, textarea, select, border-radius:8px;box-shadow:0 16px 48px rgba(0,0,0,.45);background:var(--g100);} .img-lightbox-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:center;max-width:640px;} .img-lightbox-caption{color:#e5e7eb;font-size:12px;line-height:1.5;text-align:center;} + +/* Library view switch. The active view has to be unmistakable — two buttons of + the same weight read as two buttons, not as a current view and an alternative. */ +.lib-tabs{display:flex;gap:2px;background:var(--g100);border-radius:8px;padding:3px;} +.lib-tab{border:0;border-radius:6px;background:transparent;color:var(--g500); + font:inherit;font-size:12px;font-weight:500;padding:5px 12px;cursor:pointer; + transition:background .15s ease,color .15s ease,box-shadow .15s ease;} +.lib-tab:hover{color:var(--g700);} +.lib-tab[aria-selected="true"]{background:var(--white,#fff);color:var(--g900);font-weight:600; + box-shadow:0 1px 2px rgba(17,24,39,.10),0 0 0 1px rgba(17,24,39,.04);} +.lib-tab:focus-visible{outline:2px solid var(--blue);outline-offset:1px;} diff --git a/public/js/myResources.js b/public/js/myResources.js index 1fe39758..270b24d9 100644 --- a/public/js/myResources.js +++ b/public/js/myResources.js @@ -281,8 +281,9 @@ [['tab-mr-docs', docs], ['tab-mr-images', !docs]].forEach(function (pair) { var tab = document.getElementById(pair[0]); if (!tab) return; + // aria-selected is the whole state: the stylesheet keys off it, so the + // visible highlight and what a screen reader announces cannot disagree. tab.setAttribute('aria-selected', pair[1] ? 'true' : 'false'); - tab.style.background = pair[1] ? 'var(--white)' : 'transparent'; }); var list = document.getElementById('mr-list'); var panel = document.getElementById('mr-images-panel'); diff --git a/src/utils/visionTool.js b/src/utils/visionTool.js index bdbef305..79ab2544 100644 --- a/src/utils/visionTool.js +++ b/src/utils/visionTool.js @@ -58,7 +58,18 @@ function describePrompt(question) { 'cannot see it can reason about it. Report only what is observable — colours, ' + 'distribution, morphology, text, readings, scale. Do not diagnose, do not ' + 'speculate about causes, and say plainly when something is unclear or cut off ' + - 'rather than guessing.\n\nThe question to answer: ' + question; + 'rather than guessing.\n\n' + + // The question is written by a model that has not seen the image, from what + // the user said. So it can presume something that is not there — "describe + // this rash" about a photograph of a drug chart. Answering the presumption + // rather than the picture is the failure that matters, because the answer + // then travels back as fact. + 'The question below was written by someone who cannot see the image, so it ' + + 'may assume something the image does not show. Describe what is actually ' + + 'there. If the question presumes something absent — a finding, a body part, ' + + 'a kind of document — say so first, in plain words, and then describe what ' + + 'the image does contain.\n\n' + + 'The question: ' + question; } /** diff --git a/test/vision-tool.test.js b/test/vision-tool.test.js index 44ee0832..21ae90d8 100644 --- a/test/vision-tool.test.js +++ b/test/vision-tool.test.js @@ -56,6 +56,16 @@ test('a call routes the image to the vision model, never to the chat model', asy assert.equal(out.content, 'That pattern fits...'); }); +test('a question that presumes the wrong thing is corrected, not answered', () => { + // The question is written by a model that has not seen the image, from what + // the user said — so "describe this rash" can arrive about a photograph of a + // drug chart. Answering the presumption would send that back as fact. + const prompt = visionTool.describePrompt('what is the distribution of this rash'); + assert.match(prompt, /may assume something the image does not show/); + assert.match(prompt, /If the question presumes something absent/); + assert.match(prompt, /describe what the image does contain/); +}); + test('the seeing model is told to report, not to diagnose', () => { // It has a picture and no conversation, no sources and no system prompt. An // opinion from it would carry weight it has not earned.