feat(assistant): #91 refonte Notion de la zone de discussion — question ancrée en haut, fil chronologique, étapes repliables, barre Copier/Ajouter
This commit is contained in:
+132
-77
@@ -104,6 +104,7 @@ async function main() {
|
||||
const bookslmMod = await import(pathToFileURL(path.join(JS_DIR, "bookslm.js")).href);
|
||||
const { BooksLM, MODE } = bookslmMod;
|
||||
const { collectOpenDocuments, documentsSignature } = bookslmMod;
|
||||
const { state } = await import(pathToFileURL(path.join(JS_DIR, "state.js")).href);
|
||||
|
||||
// ── 1. Rewrite modal resolves with the typed instruction ──
|
||||
await test("rewrite modal returns the typed instruction on confirm", async () => {
|
||||
@@ -190,87 +191,141 @@ 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();
|
||||
});
|
||||
// ── 4b. Notion-style thread: top-anchored latest post, provider/model tag, action bar ──
|
||||
await test("thread renders chronologically (user post above its answer)", () => {
|
||||
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 roles = [...panel.querySelectorAll(".bookslm-messages > .bookslm-msg")].map((el) =>
|
||||
el.classList.contains("user") ? "user" : "assistant");
|
||||
assert.deepEqual(roles, ["user", "assistant", "user", "assistant"], "chronological order kept");
|
||||
// No background bubble on the assistant answer (Notion-style full-width text).
|
||||
const aBubble = panel.querySelector(".bookslm-msg.assistant .bookslm-bubble");
|
||||
assert.ok(aBubble.classList.contains("assistant"), "assistant bubble class");
|
||||
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("sending anchors the latest user post to the top of the thread", async () => {
|
||||
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: "Dernier sujet" },
|
||||
];
|
||||
b._renderMessages();
|
||||
let anchored = null;
|
||||
const users = panel.querySelectorAll(".bookslm-msg.user");
|
||||
const last = users[users.length - 1];
|
||||
last.scrollIntoView = (opts) => { anchored = opts; };
|
||||
b._anchorLatestUser();
|
||||
await sleep(10); // requestAnimationFrame shim → setTimeout(0)
|
||||
assert.ok(anchored, "scrollIntoView called on the latest user post");
|
||||
assert.equal(anchored.block, "start");
|
||||
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("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 .bookslm-msg-meta");
|
||||
assert.ok(meta, "provider/model meta line rendered");
|
||||
assert.equal(meta.textContent, "deepseek · deepseek-v3");
|
||||
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("action bar: user has copy, assistant has copy + add", () => {
|
||||
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();
|
||||
assert.ok(panel.querySelector(".bookslm-msg.user .bookslm-msg-action-copy"), "user copy action");
|
||||
assert.ok(!panel.querySelector(".bookslm-msg.user .bookslm-msg-action-insert"), "no add on user post");
|
||||
assert.ok(panel.querySelector(".bookslm-msg.assistant .bookslm-msg-action-copy"), "assistant copy action");
|
||||
assert.ok(panel.querySelector(".bookslm-msg.assistant .bookslm-msg-action-insert"), "assistant add action");
|
||||
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();
|
||||
});
|
||||
await test("copy action 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-msg-action-copy");
|
||||
assert.ok(btn, "copy button present");
|
||||
btn.click();
|
||||
assert.deepEqual(copied, ["texte à copier"], "copy handler receives raw content");
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
await test("add action inserts the answer at the editor cursor", () => {
|
||||
const b = new BooksLM();
|
||||
const panel = b._render();
|
||||
b._panel = panel;
|
||||
document.body.appendChild(panel);
|
||||
b._messages = [{ role: "assistant", content: "Réponse à insérer" }];
|
||||
b._renderMessages();
|
||||
const dispatched = [];
|
||||
state.editorView = {
|
||||
state: { selection: { main: { to: 7 } } },
|
||||
dispatch: (spec) => dispatched.push(spec),
|
||||
focus: () => {},
|
||||
};
|
||||
panel.querySelector(".bookslm-msg.assistant .bookslm-msg-action-insert").click();
|
||||
assert.equal(dispatched.length, 1, "editor dispatch called");
|
||||
assert.ok(dispatched[0].changes.insert.includes("Réponse à insérer"));
|
||||
state.editorView = null;
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
await test("empty content renders no action bar", () => {
|
||||
const b = new BooksLM();
|
||||
const panel = b._render();
|
||||
b._panel = panel;
|
||||
b._messages = [{ role: "user", content: " " }];
|
||||
b._renderMessages();
|
||||
assert.equal(panel.querySelectorAll(".bookslm-msg-actions").length, 0);
|
||||
panel.remove();
|
||||
});
|
||||
|
||||
await test("agent tool calls collapse into a discreet steps block", () => {
|
||||
const b = new BooksLM();
|
||||
const trace = b._renderToolActivity([
|
||||
{ name: "read_file", ok: true },
|
||||
{ name: "search", ok: true },
|
||||
]);
|
||||
assert.equal(trace.tagName, "DETAILS", "collapsible details element");
|
||||
const summary = trace.querySelector("summary");
|
||||
assert.ok(summary, "summary present");
|
||||
assert.ok(summary.textContent.trim().length > 0, "summary shows the steps label");
|
||||
assert.equal(trace.querySelectorAll(".bookslm-tool-line").length, 2, "each step listed inside");
|
||||
});
|
||||
|
||||
// ── 5. Source guards: no window.prompt() and authenticated BooksLM ──
|
||||
await test("ai.js no longer uses window.prompt()", async () => {
|
||||
|
||||
Reference in New Issue
Block a user