fix: reviewer findings — mhchem macro wrapping, regenerate history dedupe, strict safe image allowlist, stale docs

This commit is contained in:
Daniel 2026-09-08 19:25:31 +02:00
parent 8b072496e2
commit 79de7ccddb
5 changed files with 12 additions and 12 deletions

View file

@ -3,14 +3,12 @@
`CLINICAL_ASSISTANT_CONVERSATION_CHARS` is the sole conversation budget source.
Missing/empty uses 120000; nonempty values must validate as an integer from 1000
through 1000000. Invalid configuration returns 503 before query rewrite,
retrieval, handoff generation or chat provider calls. Counting is exactly
JavaScript string length (UTF-16 code units), including all history plus draft,
not model tokens. The old `clinical_assistant.conversation_chars` database value
is ignored and its generic config PUT is rejected. Status retains
`conversationChars`/`conversationUnit` and adds `conversationEnv`,
`conversationSource`, `conversationMeasure`. Admin config exposes the same
metadata as top-level `conversationBudget: {limit,unit,measure,env,source}`.
Full-history storage, 8 MiB saves and explicit successful-only handoffs are unchanged.
## Catalogue and runtime use

View file

@ -13,7 +13,8 @@ export function safeImageUrl(src) {
var base = (typeof window !== 'undefined' && window.location && window.location.href) || 'https://synthetic.invalid/';
var url = new URL(s, base);
if (url.protocol === 'http:' || url.protocol === 'https:') return url.origin === new URL(base).origin ? s : '';
return s; // relative paths resolve to our own origin by construction
if (url.protocol === 'file:' || url.protocol === 'javascript:' || url.protocol === 'data:') return '';
return url.origin === new URL(base).origin ? s : ''; // relative paths resolve to our own origin only
} catch (e) {
return '';
}
@ -398,8 +399,8 @@ export function renderLatexText(text, katex, hold) {
if (dollars || brackets) return render(raw, dollars || brackets, display);
if (inline !== undefined) return render(raw, inline, false);
if (parens !== undefined) return render(raw, parens, false);
if (ce !== undefined) return render(raw, ce, false);
if (pu !== undefined) return render(raw, pu, false);
if (ce !== undefined) return render(raw, '\\ce{' + ce + '}', false);
if (pu !== undefined) return render(raw, '\\pu{' + pu + '}', false);
return raw;
});
}

View file

@ -939,7 +939,7 @@ import {
if (typeof showToast === 'function') showToast('No question precedes this answer to regenerate.', 'error');
return;
}
messages = messages.slice(0, index);
messages = messages.slice(0, index - 1); // drop the answer AND its question; the question is replayed as the new message
if (row && row.parentNode) row.remove();
var input = document.getElementById('assistant-input');
if (input) input.value = question.content;

View file

@ -23,9 +23,9 @@ test('mhchem inline delimiters render as KaTeX without eating ordinary backslash
}
};
const html = renderAssistantMarkdown('Base deficit \\ce{HCO3-} and \\pu{37 C}; a plain \\ce without braces stays text.', [], { katex });
assert.deepEqual(expressions, [{ expr: 'HCO3-', display: false }, { expr: '37 C', display: false }]);
assert.match(html, /<span class="katex">HCO3-<\/span>/);
assert.match(html, /<span class="katex">37 C<\/span>/);
assert.deepEqual(expressions, [{ expr: '\\ce{HCO3-}', display: false }, { expr: '\\pu{37 C}', display: false }]);
assert.match(html, /<span class="katex">\\ce{HCO3-}<\/span>/);
assert.match(html, /<span class="katex">\\pu{37 C}<\/span>/);
assert.match(html, /without braces stays text/);
});

View file

@ -77,11 +77,12 @@ test('regenerate replays the preceding question for the latest answer and never
secondRow.querySelector('[data-assistant-msg-regenerate]').click();
assert.equal(app.document.getElementById('assistant-input').value, 'Second question', 'input refilled with the question');
await new Promise(r => setImmediate(r)); await new Promise(r => setImmediate(r));
assert.equal(c.messages.length, 4, 'old answer replaced by the regenerated one');
assert.equal(c.messages[3].content, 'Regenerated answer.');
assert.equal(c.messages.length, 3, 'the replayed question is sent as the new message, not duplicated in history');
assert.equal(c.messages[2].content, 'Regenerated answer.');
assert.equal(c.messages[0].content, 'First question', 'earlier turns untouched');
assert.equal(c.messages[1].content, 'First answer.');
assert.match(rows(app)[3].textContent, /Regenerated answer/);
const all = rows(app);
assert.match(all[all.length - 1].textContent, /Regenerated answer/);
});
test('regenerate on a non-latest answer refuses honestly without touching the chat', t => {