feat(ai): commandes @/ & skills, analyse d'images et capacites des modeles (#81)
This commit is contained in:
@@ -607,6 +607,187 @@ async function main() {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 29. Resizable panel bounds and persistence (#81) ──
|
||||
await test("panel width is clamped and persisted", async () => {
|
||||
localStorage.clear();
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
assert.equal(b._clampPanelWidth(100), 320, "min width enforced");
|
||||
assert.equal(b._clampPanelWidth(5000), 1000, "max width enforced");
|
||||
localStorage.setItem("obsigate-bookslm-width", "640");
|
||||
b._applyPanelWidth();
|
||||
assert.equal(b._panel.style.width, "640px", "persisted width applied");
|
||||
assert.ok(b._panel.querySelector(".bookslm-resize-handle"), "resize handle present");
|
||||
b._panel.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 30. Ad-hoc context chips (#81) ──
|
||||
await test("ad-hoc files and directories render as chips", async () => {
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
b._addAdhocFile("notes/a.md");
|
||||
b._addAdhocDir("projets/");
|
||||
const chips = b._panel.querySelectorAll(".bookslm-attachments .bookslm-chip");
|
||||
assert.equal(chips.length, 2, "one chip per ad-hoc entry");
|
||||
assert.ok(b._adhocDirs[0].path === "projets", "trailing slash stripped");
|
||||
b._removeAdhocFile("notes/a.md");
|
||||
assert.equal(b._panel.querySelectorAll(".bookslm-attachments .bookslm-chip").length, 1);
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
// ── 31. Chat payload carries extras, images and skill (#81) ──
|
||||
await test("send payload includes extra context, images and skill", async () => {
|
||||
localStorage.clear();
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
b._mode = MODE.GENERAL;
|
||||
b._vault = "V";
|
||||
b._messages = [];
|
||||
b._adhocFiles = [{ path: "a.md" }];
|
||||
b._adhocDirs = [{ path: "sub" }];
|
||||
b._images = [{ path: "pics/x.png", name: "x.png" }];
|
||||
b._activeSkill = "research";
|
||||
b._panel.querySelector("textarea").value = "Analyse";
|
||||
|
||||
let posted = null;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
if (String(url).includes("/api/ai/bookslm/chat")) posted = JSON.parse(opts.body);
|
||||
return { ok: true, status: 200, body: { getReader: () => ({ read: async () => ({ done: true }) }) } };
|
||||
};
|
||||
await b._sendMessage();
|
||||
|
||||
assert.ok(posted, "chat request posted");
|
||||
assert.deepEqual(posted.extra_files, ["a.md"]);
|
||||
assert.deepEqual(posted.extra_directories, ["sub"]);
|
||||
assert.deepEqual(posted.images, [{ path: "pics/x.png" }]);
|
||||
assert.equal(posted.skill, "research");
|
||||
b._panel.remove();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 32. Images force the plain chat endpoint (not the agent) (#81) ──
|
||||
await test("images route to /chat even in agent mode", async () => {
|
||||
const b = new BooksLM();
|
||||
b._agentMode = true;
|
||||
let url = "";
|
||||
globalThis.fetch = async (u) => {
|
||||
url = String(u);
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
};
|
||||
await b._postChat({ images: [{ data: "AAA", mime_type: "image/png" }] });
|
||||
assert.ok(url.includes("/api/ai/bookslm/chat"), "image request uses /chat");
|
||||
await b._postChat({ images: [] });
|
||||
assert.ok(url.includes("/api/ai/bookslm/agent"), "text request uses /agent in agent mode");
|
||||
});
|
||||
|
||||
// ── 33. Vision gate (#81) ──
|
||||
await test("_modelSupportsVision reflects selected model capabilities", async () => {
|
||||
localStorage.setItem("obsigate_ai_picker", JSON.stringify({
|
||||
provider: "deepseek", model: "deepseek-chat", capabilities: { chat: true, vision: false },
|
||||
}));
|
||||
const b = new BooksLM();
|
||||
assert.equal(await b._modelSupportsVision(), false);
|
||||
localStorage.setItem("obsigate_ai_picker", JSON.stringify({
|
||||
provider: "qwencloud", model: "qwen-vl-max", capabilities: { chat: true, vision: true },
|
||||
}));
|
||||
assert.equal(await b._modelSupportsVision(), true);
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
// ── 34. `/` command menu and selection (#81) ──
|
||||
await test("slash command menu lists skills and selects one", async () => {
|
||||
localStorage.clear();
|
||||
globalThis.fetch = async (url) => {
|
||||
if (String(url).includes("/api/ai/skills")) {
|
||||
return {
|
||||
ok: true, status: 200,
|
||||
json: async () => ({
|
||||
skills: [{ id: "research", label: "Recherche", icon: "🔎", type: "skill", description: "d" }],
|
||||
commands: [{ id: "help", label: "Aide", icon: "❓", type: "admin", description: "h" }],
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
};
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
const ta = b._panel.querySelector("textarea");
|
||||
ta.value = "/res";
|
||||
ta.selectionStart = ta.selectionEnd = 4;
|
||||
await b._showCommandMenu("res");
|
||||
const menu = b._panel.querySelector(".bookslm-command-menu");
|
||||
assert.ok(!menu.classList.contains("hidden"), "command menu shown");
|
||||
assert.equal(menu.querySelectorAll(".bookslm-menu-item").length, 1);
|
||||
b._selectCommand({ id: "research", label: "Recherche", type: "skill" });
|
||||
assert.equal(b._activeSkill, "research");
|
||||
assert.ok(b._panel.querySelector(".bookslm-chip-skill"), "skill chip rendered");
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
// ── 35. `@` mention menu adds ad-hoc context / images (#81) ──
|
||||
await test("mention menu adds files, directories and images", async () => {
|
||||
localStorage.clear();
|
||||
globalThis.fetch = async (url) => {
|
||||
if (String(url).includes("/api/tree-search")) {
|
||||
return {
|
||||
ok: true, status: 200,
|
||||
json: async () => ({ results: [
|
||||
{ path: "notes/a.md", type: "file" },
|
||||
{ path: "notes", type: "dir" },
|
||||
] }),
|
||||
};
|
||||
}
|
||||
return { ok: true, status: 200, json: async () => ({}) };
|
||||
};
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
b._vault = "V";
|
||||
await b._showMentionMenu("notes");
|
||||
const menu = b._panel.querySelector(".bookslm-mention-menu");
|
||||
assert.equal(menu.querySelectorAll(".bookslm-menu-item").length, 2);
|
||||
b._selectMention({ id: "notes/a.md", type: "file" });
|
||||
assert.ok(b._adhocFiles.some((f) => f.path === "notes/a.md"));
|
||||
b._selectMention({ id: "notes", type: "dir" });
|
||||
assert.ok(b._adhocDirs.some((d) => d.path === "notes"));
|
||||
b._selectMention({ id: "pics/x.png", type: "file" });
|
||||
assert.ok(b._images.some((img) => img.path === "pics/x.png"));
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
// ── 36. Capability checklist rendering (#81) ──
|
||||
await test("capability list renders checked and unchecked flags", () => {
|
||||
const caps = {
|
||||
chat: true, embeddings: false, rerank: false, images: false,
|
||||
video: false, audio_speech: false, audio_transcription: false, vision: true,
|
||||
};
|
||||
const box = ai.renderCapabilityList(caps);
|
||||
assert.equal(box.querySelectorAll(".ai-cap-item").length, 8);
|
||||
assert.equal(box.querySelectorAll(".ai-cap-item.on").length, 2);
|
||||
assert.ok(box.textContent.includes("☑"));
|
||||
assert.ok(box.textContent.includes("□"));
|
||||
});
|
||||
|
||||
// ── 37. Admin commands handled locally (#81) ──
|
||||
await test("admin commands are parsed without an LLM call", async () => {
|
||||
const b = new BooksLM();
|
||||
b._panel = b._render();
|
||||
document.body.appendChild(b._panel);
|
||||
b._messages = [];
|
||||
let fetched = false;
|
||||
globalThis.fetch = async () => { fetched = true; return { ok: true, status: 200, json: async () => ({}) }; };
|
||||
const handled = b._maybeRunAdminWithArg("/model deepseek-chat");
|
||||
assert.equal(handled, true);
|
||||
assert.equal(fetched, false, "no LLM request for admin command");
|
||||
assert.ok(b._messages.length === 1, "assistant note pushed");
|
||||
b._panel.remove();
|
||||
});
|
||||
|
||||
// ── Summary ──
|
||||
console.log(`\n${passCount}/${testCount} tests passed`);
|
||||
if (passCount !== testCount) {
|
||||
|
||||
Reference in New Issue
Block a user