fix: reviewer findings — mhchem macro wrapping, regenerate history dedupe, strict safe image allowlist, stale docs
This commit is contained in:
parent
8b072496e2
commit
79de7ccddb
5 changed files with 12 additions and 12 deletions
|
|
@ -3,14 +3,12 @@
|
||||||
`CLINICAL_ASSISTANT_CONVERSATION_CHARS` is the sole conversation budget source.
|
`CLINICAL_ASSISTANT_CONVERSATION_CHARS` is the sole conversation budget source.
|
||||||
Missing/empty uses 120000; nonempty values must validate as an integer from 1000
|
Missing/empty uses 120000; nonempty values must validate as an integer from 1000
|
||||||
through 1000000. Invalid configuration returns 503 before query rewrite,
|
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,
|
JavaScript string length (UTF-16 code units), including all history plus draft,
|
||||||
not model tokens. The old `clinical_assistant.conversation_chars` database value
|
not model tokens. The old `clinical_assistant.conversation_chars` database value
|
||||||
is ignored and its generic config PUT is rejected. Status retains
|
is ignored and its generic config PUT is rejected. Status retains
|
||||||
`conversationChars`/`conversationUnit` and adds `conversationEnv`,
|
`conversationChars`/`conversationUnit` and adds `conversationEnv`,
|
||||||
`conversationSource`, `conversationMeasure`. Admin config exposes the same
|
`conversationSource`, `conversationMeasure`. Admin config exposes the same
|
||||||
metadata as top-level `conversationBudget: {limit,unit,measure,env,source}`.
|
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
|
## Catalogue and runtime use
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export function safeImageUrl(src) {
|
||||||
var base = (typeof window !== 'undefined' && window.location && window.location.href) || 'https://synthetic.invalid/';
|
var base = (typeof window !== 'undefined' && window.location && window.location.href) || 'https://synthetic.invalid/';
|
||||||
var url = new URL(s, base);
|
var url = new URL(s, base);
|
||||||
if (url.protocol === 'http:' || url.protocol === 'https:') return url.origin === new URL(base).origin ? s : '';
|
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) {
|
} catch (e) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
@ -398,8 +399,8 @@ export function renderLatexText(text, katex, hold) {
|
||||||
if (dollars || brackets) return render(raw, dollars || brackets, display);
|
if (dollars || brackets) return render(raw, dollars || brackets, display);
|
||||||
if (inline !== undefined) return render(raw, inline, false);
|
if (inline !== undefined) return render(raw, inline, false);
|
||||||
if (parens !== undefined) return render(raw, parens, false);
|
if (parens !== undefined) return render(raw, parens, false);
|
||||||
if (ce !== undefined) return render(raw, ce, false);
|
if (ce !== undefined) return render(raw, '\\ce{' + ce + '}', false);
|
||||||
if (pu !== undefined) return render(raw, pu, false);
|
if (pu !== undefined) return render(raw, '\\pu{' + pu + '}', false);
|
||||||
return raw;
|
return raw;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -939,7 +939,7 @@ import {
|
||||||
if (typeof showToast === 'function') showToast('No question precedes this answer to regenerate.', 'error');
|
if (typeof showToast === 'function') showToast('No question precedes this answer to regenerate.', 'error');
|
||||||
return;
|
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();
|
if (row && row.parentNode) row.remove();
|
||||||
var input = document.getElementById('assistant-input');
|
var input = document.getElementById('assistant-input');
|
||||||
if (input) input.value = question.content;
|
if (input) input.value = question.content;
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
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.deepEqual(expressions, [{ expr: '\\ce{HCO3-}', display: false }, { expr: '\\pu{37 C}', display: false }]);
|
||||||
assert.match(html, /<span class="katex">HCO3-<\/span>/);
|
assert.match(html, /<span class="katex">\\ce{HCO3-}<\/span>/);
|
||||||
assert.match(html, /<span class="katex">37 C<\/span>/);
|
assert.match(html, /<span class="katex">\\pu{37 C}<\/span>/);
|
||||||
assert.match(html, /without braces stays text/);
|
assert.match(html, /without braces stays text/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,11 +77,12 @@ test('regenerate replays the preceding question for the latest answer and never
|
||||||
secondRow.querySelector('[data-assistant-msg-regenerate]').click();
|
secondRow.querySelector('[data-assistant-msg-regenerate]').click();
|
||||||
assert.equal(app.document.getElementById('assistant-input').value, 'Second question', 'input refilled with the question');
|
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));
|
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.length, 3, 'the replayed question is sent as the new message, not duplicated in history');
|
||||||
assert.equal(c.messages[3].content, 'Regenerated answer.');
|
assert.equal(c.messages[2].content, 'Regenerated answer.');
|
||||||
assert.equal(c.messages[0].content, 'First question', 'earlier turns untouched');
|
assert.equal(c.messages[0].content, 'First question', 'earlier turns untouched');
|
||||||
assert.equal(c.messages[1].content, 'First answer.');
|
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 => {
|
test('regenerate on a non-latest answer refuses honestly without touching the chat', t => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue