From 99a72734fbd2bc0fd59e337e73731bf305e4edb5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 14:39:49 +0200 Subject: [PATCH] feat: the Learning screen can ask for grounding, and says what it got MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The route accepted useCorpus and reported grounding, but nothing in the admin screen sent the flag or showed the result — so the feature existed and was unreachable. Opt-out in the UI rather than opt-in. For clinical teaching the library is nearly always the right source, so someone who never notices the checkbox should get the grounded version. The help text explains when to turn it OFF, which is the non-obvious case: a topic the library does not cover is better written without grounding than padded with the nearest unrelated excerpts. Afterwards it says what happened — "Written from 12 library excerpts", or "Not grounded — nothing indexed matched. Written from the model alone." Ungrounded material presented as grounded is the failure worth preventing here, so the wording never implies the library was used when it was not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU --- public/components/cms.html | 18 +++++++++++++++++- public/js/learningHub.js | 18 ++++++++++++++++++ test/learning-retrieval.test.js | 21 +++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/public/components/cms.html b/public/components/cms.html index b0577585..59b300b4 100644 --- a/public/components/cms.html +++ b/public/components/cms.html @@ -242,7 +242,23 @@ - + +
+ +

+ Searches the indexed clinical corpus for this topic and writes from those + excerpts, preferring them over the model's own recall. The resource ends with + a References section listing what it drew on. Turn this off for a topic the + library does not cover — grounding on the nearest unrelated material is + worse than not grounding at all. +

+

+
+ +
diff --git a/public/js/learningHub.js b/public/js/learningHub.js index ca15ccc5..c9bd28b8 100644 --- a/public/js/learningHub.js +++ b/public/js/learningHub.js @@ -495,6 +495,14 @@ import { createWebdavController } from './learningHub/webdavController.js'; if (webdavCtx && webdavCtx.value.trim()) formData.append('topic', webdavCtx.value.trim()); } + // Grounding is opt-out rather than opt-in in the UI: for clinical teaching + // the library is nearly always the right source, and someone who forgets + // the checkbox should get the grounded version. + var corpusBox = document.getElementById('lh-ai-use-corpus'); + formData.append('useCorpus', corpusBox && corpusBox.checked === false ? 'false' : 'true'); + var groundingLine = document.getElementById('lh-ai-grounding-result'); + if (groundingLine) groundingLine.textContent = ''; + var btn = document.getElementById('btn-lh-ai-generate'); if (btn) { btn.disabled = true; btn.innerHTML = ' Generating...'; } showBusy('AI is generating content...'); @@ -512,6 +520,16 @@ import { createWebdavController } from './learningHub/webdavController.js'; hideBusy(); if (btn) { btn.disabled = false; btn.innerHTML = ' Generate Content'; } if (!data.success) { showToast(data.error || 'Generation failed', 'error'); return; } + // Say what it was written from. Ungrounded material presented as grounded + // is the failure worth preventing, so this never implies the library was + // used when it was not. + if (groundingLine) { + var g = data.grounding || {}; + groundingLine.textContent = g.used + ? 'Written from ' + g.count + ' library excerpt' + (g.count === 1 ? '' : 's') + '.' + : 'Not grounded' + (g.reason ? ' \u2014 ' + g.reason : '') + '. Written from the model alone.'; + groundingLine.style.color = g.used ? 'var(--green)' : 'var(--g600)'; + } // Presentation returns marpMarkdown directly; others return content{} var payload = data.contentType === 'presentation' ? data : data.content; return applyAiContent(payload, contentType).then(function() { diff --git a/test/learning-retrieval.test.js b/test/learning-retrieval.test.js index 451493a5..16a5daae 100644 --- a/test/learning-retrieval.test.js +++ b/test/learning-retrieval.test.js @@ -127,3 +127,24 @@ test('the corpus block tells the model to prefer it over recall', () => { // And every success response says what it was grounded on. assert.equal((route.match(/grounding: \{ used: Boolean\(corpus\.context\)/g) || []).length, 3); }); + +test('the Learning screen can ask for grounding and says what it got', () => { + const html = read('public/components/cms.html'); + const js = read('public/js/learningHub.js'); + + // Opt-out in the UI rather than opt-in: for clinical teaching the library is + // nearly always the right source, so forgetting the checkbox should give the + // grounded version, not the ungrounded one. + assert.match(html, //); + assert.match(js, /formData\.append\('useCorpus', corpusBox && corpusBox\.checked === false \? 'false' : 'true'\)/); + + // The checkbox explains when to turn it OFF, which is the non-obvious case. + assert.match(html, /Turn this off for a topic the\s*\n?\s*library does not cover/); + + // And the result is reported. Ungrounded material presented as grounded is + // the failure worth preventing, so the wording must never imply the library + // was used when it was not. + assert.match(js, /'Written from ' \+ g\.count \+ ' library excerpt'/); + assert.match(js, /'Not grounded' \+ \(g\.reason \? ' \\u2014 ' \+ g\.reason : ''\) \+ '\. Written from the model alone\.'/); + assert.match(html, /id="lh-ai-grounding-result"/); +});