feat: the Learning screen can ask for grounding, and says what it got
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Docker Build / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m20s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 2s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
Daniel 2026-09-11 14:39:49 +02:00
parent 94f1290aae
commit 99a72734fb
3 changed files with 56 additions and 1 deletions

View file

@ -242,7 +242,23 @@
</div>
</div>
<!-- Row 4: instructions -->
<!-- Row 4: grounding -->
<div class="lh-ai-opt-field" style="width:100%;">
<label style="display:flex;align-items:center;gap:8px;font-weight:600;">
<input type="checkbox" id="lh-ai-use-corpus" checked>
Use the clinical library
</label>
<p style="margin:4px 0 0;font-size:12px;color:var(--g500);">
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 &mdash; grounding on the nearest unrelated material is
worse than not grounding at all.
</p>
<p id="lh-ai-grounding-result" role="status" style="margin:6px 0 0;font-size:12px;color:var(--g600);"></p>
</div>
<!-- Row 5: instructions -->
<div class="lh-ai-opt-field" style="width:100%;">
<label>Special instructions <span style="color:var(--g400);font-weight:400;">(optional)</span></label>
<textarea id="lh-ai-refinement" class="cms-input-sm" style="width:100%;min-height:90px;resize:vertical;font-family:inherit;line-height:1.5;" placeholder="e.g., Focus on ER management, suitable for residents, case-based format"></textarea>

View file

@ -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 = '<i class="fas fa-spinner fa-spin"></i> Generating...'; }
showBusy('AI is generating content...');
@ -512,6 +520,16 @@ import { createWebdavController } from './learningHub/webdavController.js';
hideBusy();
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-wand-magic-sparkles"></i> 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() {

View file

@ -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, /<input type="checkbox" id="lh-ai-use-corpus" checked>/);
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"/);
});