feat(assistant): #91 fenêtre de résultats — post épinglé en haut, fournisseur/modèle discret & bouton copier
CI / lint (push) Successful in 1m20s
CI / security (push) Successful in 57s
CI / test (push) Successful in 2m41s
CI / build (push) Successful in 55s
CI / e2e (push) Successful in 10m40s

This commit is contained in:
2026-09-15 12:04:38 -04:00
parent b69cb9b0f8
commit 256f5a4a03
9 changed files with 275 additions and 37 deletions
+82
View File
@@ -190,6 +190,88 @@ async function main() {
panel.remove();
});
// ── 4b. Latest turn pinned to the top + provider/model tag + copy buttons ──
await test("latest turn (user post + answer) is pinned to the top of messages", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [
{ role: "user", content: "ancien" },
{ role: "assistant", content: "ancienne réponse" },
{ role: "user", content: "Nouveau sujet" },
{ role: "assistant", content: "Nouvelle réponse", provider: "deepseek", model: "deepseek-chat" },
];
b._renderMessages();
const msgs = panel.querySelectorAll(".bookslm-msg");
// Both bubbles of the latest turn carry the pinning class...
assert.ok(msgs[2].classList.contains("latest"), "latest user pinned");
assert.ok(msgs[3].classList.contains("latest"), "latest assistant pinned");
assert.ok(!msgs[0].classList.contains("latest"), "older user not pinned");
assert.ok(!msgs[1].classList.contains("latest"), "older assistant not pinned");
panel.remove();
});
await test("assistant answer shows a discreet provider/model tag", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [
{ role: "user", content: "Bonjour" },
{ role: "assistant", content: "Réponse", provider: "deepseek", model: "deepseek-v3" },
];
b._renderMessages();
const meta = panel.querySelector(".bookslm-msg.assistant.latest .bookslm-msg-meta");
assert.ok(meta, "provider/model meta line rendered");
assert.equal(meta.textContent, "deepseek · deepseek-v3");
panel.remove();
});
await test("both user and assistant blocks expose a copy button", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [
{ role: "user", content: "Question copiable" },
{ role: "assistant", content: "Réponse copiable", provider: "deepseek", model: "m" },
];
b._renderMessages();
const wrapped = panel.querySelectorAll(".bookslm-msg");
assert.equal(wrapped.length, 2);
// Both wraps carry a copy button.
assert.ok(panel.querySelector(".bookslm-msg.user .bookslm-copy-btn"), "user copy button");
assert.ok(panel.querySelector(".bookslm-msg.assistant .bookslm-copy-btn"), "assistant copy button");
panel.remove();
});
await test("copy button copies the raw message text to the clipboard", async () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
document.body.appendChild(panel);
b._messages = [{ role: "user", content: "texte à copier" }];
b._renderMessages();
const copied = [];
b._copyText = (t) => copied.push(t);
const btn = panel.querySelector(".bookslm-msg.user .bookslm-copy-btn");
assert.ok(btn, "copy button present");
btn.click();
assert.deepEqual(copied, ["texte à copier"], "copy handler receives raw content");
panel.remove();
});
await test("empty user content renders no copy button", () => {
const b = new BooksLM();
const panel = b._render();
b._panel = panel;
b._messages = [{ role: "user", content: " " }];
b._renderMessages();
assert.equal(panel.querySelectorAll(".bookslm-copy-btn").length, 0);
panel.remove();
});
// ── 5. Source guards: no window.prompt() and authenticated BooksLM ──
await test("ai.js no longer uses window.prompt()", async () => {
const { readFileSync } = await import("node:fs");