feat(sidebar+assistant): filtre Recents/Sauvegardes (#99), pastille Deep Research (#100) et icone du bouton + (BUG-049)
CI / lint (push) Failing after 1m20s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 59s

This commit is contained in:
2026-09-16 20:16:41 -04:00
parent 8f26a418a9
commit 4231f2e929
22 changed files with 491 additions and 80 deletions
+17 -14
View File
@@ -114,6 +114,9 @@ class BooksLM {
this._adhocDirs = [];
this._images = [];
this._activeSkill = null;
// Deep Research: a skill-like chip that enables agent tools and injects a
// research directive into the request (without polluting the composer).
this._activeDeepResearch = false;
this._skills = [];
this._menuItems = [];
this._menuIndex = 0;
@@ -214,6 +217,7 @@ class BooksLM {
this._adhocDirs = [];
this._images = [];
this._activeSkill = null;
this._activeDeepResearch = false;
this._isLoading = true;
await this._loadHistory(preferredSessionId);
@@ -1216,7 +1220,7 @@ class BooksLM {
const host = this._panel.querySelector('.bookslm-attachments');
if (!host) return;
host.innerHTML = '';
const hasAny = this._adhocFiles.length || this._adhocDirs.length || this._images.length || this._activeSkill;
const hasAny = this._adhocFiles.length || this._adhocDirs.length || this._images.length || this._activeSkill || this._activeDeepResearch;
host.classList.toggle('empty', !hasAny);
if (!hasAny) return;
@@ -1228,6 +1232,13 @@ class BooksLM {
() => { this._activeSkill = null; this._renderAttachments(); },
));
}
if (this._activeDeepResearch) {
host.appendChild(this._chip(
`🔎 ${t('bookslm.ext_deep_research')}`,
'deep-research',
() => { this._activeDeepResearch = false; this._renderAttachments(); },
));
}
for (const file of this._adhocFiles) {
host.appendChild(this._chip(`📄 ${file.path}`, 'file', () => this._removeAdhocFile(file.path)));
}
@@ -1449,21 +1460,13 @@ class BooksLM {
if (typeof safeCreateIcons === 'function') safeCreateIcons();
}
/** #97 — Deep Research: enabled agent tools + a pre-filled directive. */
/** #97/#99 — Deep Research: agent tools + a skill-like chip (no composer text). */
_startDeepResearch() {
this._closeExtMenu();
if (!this._agentMode) this._toggleAgentMode();
const textarea = this._panel && this._panel.querySelector('textarea');
if (textarea) {
const directive = textarea.value.trim()
? textarea.value.replace(/\s*$/, '')
: '';
textarea.value = (directive ? directive + '\n\n' : '') + t('bookslm.deep_research_prompt');
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.focus();
} else {
showToast(t('bookslm.deep_research_started'), 'info');
}
this._activeDeepResearch = true;
this._renderAttachments();
showToast(t('bookslm.deep_research_started'), 'info');
}
_onPaste(e) {
@@ -2728,7 +2731,7 @@ class BooksLM {
img.path ? { path: img.path } : { data: img.data, mime_type: img.mime_type }
)),
skill: this._activeSkill,
message: text,
message: this._activeDeepResearch ? `${t('bookslm.deep_research_prompt')}\n\n${text}` : text,
conversation_history: history,
provider,
model,
+48 -11
View File
@@ -1,7 +1,7 @@
// config.js — extracted from app.js (3872-4865)
import { api, AuthManager, initMfaSettings } from './auth.js';
import { state } from './state.js';
import { el, icon, openFile } from './viewer.js';
import { el, icon, openFile, filterSavedSearches } from './viewer.js';
import { syncVaultSelectors, setSelectedVaultContext, refreshSidebarForContext, loadVaults, loadVaultSettings, loadTags, TagFilterService, refreshSidebarTreePreservingState } from './sidebar.js';
import { escapeHtml, safeCreateIcons } from './utils.js';
import { showToast, closeHeaderMenu, closeMobileSidebar } from './ui.js';
@@ -11,6 +11,7 @@ import { getModelCapabilities, renderCapabilityList, refreshAIPickers } from './
let _recentTimestampTimer = null;
let _recentFilesCache = [];
let _recentRefreshTimer = null;
let _recentQuery = "";
let _aiSessionsCache = [];
let _aiQuery = "";
@@ -24,7 +25,7 @@ export async function loadRecentFiles(vaultFilter) {
try {
const data = await api(url);
_recentFilesCache = data.files || [];
renderRecentList(_recentFilesCache);
renderRecentList(_applyRecentFilter(_recentFilesCache));
} catch (err) {
console.error("Failed to load recent files:", err);
listEl.innerHTML = "";
@@ -41,6 +42,9 @@ function renderRecentList(files) {
listEl.innerHTML = "";
if (!files || files.length === 0) {
if (_recentFilesCache.length && _recentQuery) {
listEl.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode(t("sidebar.no_results"))]));
}
if (emptyEl) {
emptyEl.classList.remove("hidden");
safeCreateIcons();
@@ -162,25 +166,45 @@ function _formatAIDate(ts) {
}
}
/** Accent- and case-insensitive normalization for the AI history filter (#98). */
function _aiNorm(value) {
/** Accent- and case-insensitive normalization for the sidebar filters (#98/#99). */
function _sidebarNorm(value) {
return String(value || "")
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase();
}
/** Apply the current sidebar query to the recent-files list (#99). */
function _applyRecentFilter(files) {
const q = _sidebarNorm(_recentQuery);
if (!q) return files;
return (files || []).filter((f) => {
const tags = Array.isArray(f.tags) ? f.tags.join(" ") : "";
return _sidebarNorm(f.title).includes(q)
|| _sidebarNorm(f.path).includes(q)
|| _sidebarNorm(f.vault).includes(q)
|| _sidebarNorm(f.preview).includes(q)
|| _sidebarNorm(tags).includes(q);
});
}
/** Filter the sidebar recent-files list by the sidebar filter input (#99). */
export function filterRecentFiles(query) {
_recentQuery = (query || "").trim();
renderRecentList(_applyRecentFilter(_recentFilesCache));
}
/** Apply the current sidebar query to a session list, resolving the mode label. */
function _applyAIFilter(sessions) {
const q = _aiNorm(_aiQuery);
const q = _sidebarNorm(_aiQuery);
if (!q) return sessions;
return (sessions || []).filter((s) => {
const modeLabel = t("bookslm.mode_" + (s.mode || "general"));
return _aiNorm(s.title).includes(q)
|| _aiNorm(s.preview).includes(q)
|| _aiNorm(s.directory).includes(q)
|| _aiNorm(s.context).includes(q)
|| _aiNorm(modeLabel).includes(q);
return _sidebarNorm(s.title).includes(q)
|| _sidebarNorm(s.preview).includes(q)
|| _sidebarNorm(s.directory).includes(q)
|| _sidebarNorm(s.context).includes(q)
|| _sidebarNorm(modeLabel).includes(q);
});
}
@@ -274,21 +298,34 @@ function switchSidebarTab(tab) {
});
const filterInput = document.getElementById("sidebar-filter-input");
if (filterInput) {
const placeholders = { vaults: "Filtrer fichiers...", tags: "Filtrer tags...", recent: "", ai: t("sidebar.filter_ai") };
const placeholders = {
vaults: "Filtrer fichiers...",
tags: "Filtrer tags...",
recent: t("sidebar.filter_recent"),
saved: t("sidebar.filter_saved"),
ai: t("sidebar.filter_ai"),
};
filterInput.placeholder = placeholders[tab] || "";
}
const query = filterInput ? (state.sidebarFilterCaseSensitive ? filterInput.value.trim() : filterInput.value.trim().toLowerCase()) : "";
if (query) {
if (tab === "vaults") performTreeSearch(query);
else if (tab === "tags") filterTagCloud(query);
else if (tab === "recent") filterRecentFiles(query);
else if (tab === "saved") filterSavedSearches(query);
else if (tab === "ai") filterAIHistory(query);
}
// Auto-load recent files when switching to the recent tab
if (tab === "recent") {
_populateRecentVaultFilter();
if (filterInput) filterRecentFiles(filterInput.value.trim());
const vaultFilter = document.getElementById("recent-vault-filter");
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
}
// #99 — Re-apply the current sidebar query on the saved-searches tab.
if (tab === "saved" && filterInput) {
filterSavedSearches(filterInput.value.trim());
}
// #96/#98 — Auto-load the AI conversation history when entering its tab,
// re-applying the current sidebar filter query.
if (tab === "ai") {
+24 -35
View File
@@ -2,8 +2,8 @@ import { state } from './state.js';
import { safeCreateIcons, getFileIcon, flushIcons } from './utils.js';
import { api } from './auth.js';
import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js';
import { _populateRecentVaultFilter, switchSidebarTab, filterAIHistory } from './config.js';
import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText } from './viewer.js';
import { _populateRecentVaultFilter, switchSidebarTab, filterAIHistory, filterRecentFiles } from './config.js';
import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText, filterSavedSearches } from './viewer.js';
import { performAdvancedSearch } from './search.js';
import { t } from './i18n.js';
@@ -700,29 +700,32 @@ function initSidebarFilter() {
const caseBtn = document.getElementById("sidebar-filter-case-btn");
const clearBtn = document.getElementById("sidebar-filter-clear-btn");
// Route the query to the active tab's own filter (#98/#99).
const routeFilter = async (q) => {
const tab = state.activeSidebarTab;
if (tab === "vaults") await performTreeSearch(q);
else if (tab === "recent") filterRecentFiles(q);
else if (tab === "saved") filterSavedSearches(q);
else if (tab === "ai") filterAIHistory(q);
else filterTagCloud(q);
};
const routeClear = async () => {
const tab = state.activeSidebarTab;
if (tab === "vaults") await restoreSidebarTree();
else if (tab === "recent") filterRecentFiles("");
else if (tab === "saved") filterSavedSearches("");
else if (tab === "ai") filterAIHistory("");
else filterTagCloud("");
};
input.addEventListener("input", () => {
const hasText = input.value.length > 0;
clearBtn.style.display = hasText ? "flex" : "none";
clearTimeout(state.filterDebounce);
state.filterDebounce = setTimeout(async () => {
const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
if (hasText) {
if (state.activeSidebarTab === "vaults") {
await performTreeSearch(q);
} else if (state.activeSidebarTab === "ai") {
filterAIHistory(q);
} else {
filterTagCloud(q);
}
} else {
if (state.activeSidebarTab === "vaults") {
await restoreSidebarTree();
} else if (state.activeSidebarTab === "ai") {
filterAIHistory("");
} else {
filterTagCloud("");
}
}
if (hasText) await routeFilter(q);
else await routeClear();
}, 220);
});
@@ -730,15 +733,7 @@ function initSidebarFilter() {
state.sidebarFilterCaseSensitive = !state.sidebarFilterCaseSensitive;
caseBtn.classList.toggle("active");
const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
if (input.value.trim()) {
if (state.activeSidebarTab === "vaults") {
await performTreeSearch(q);
} else if (state.activeSidebarTab === "ai") {
filterAIHistory(q);
} else {
filterTagCloud(q);
}
}
if (input.value.trim()) await routeFilter(q);
});
clearBtn.addEventListener("click", async () => {
@@ -747,13 +742,7 @@ function initSidebarFilter() {
state.sidebarFilterCaseSensitive = false;
caseBtn.classList.remove("active");
clearTimeout(state.filterDebounce);
if (state.activeSidebarTab === "vaults") {
await restoreSidebarTree();
} else if (state.activeSidebarTab === "ai") {
filterAIHistory("");
} else {
filterTagCloud("");
}
await routeClear();
});
clearBtn.style.display = "none";
+32 -1
View File
@@ -1811,6 +1811,14 @@ function _renderSyncPanel(panel) {
// ── Saved searches filter ──
var _savedFilterType = 'all';
var _savedFilterInitDone = false;
var _savedQuery = '';
function _savedNorm(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase();
}
function _initSavedFilter() {
if (_savedFilterInitDone) return;
@@ -1827,18 +1835,41 @@ function _initSavedFilter() {
_savedFilterInitDone = true;
}
/** #99 — Filter the saved-searches list by the global sidebar filter input. */
export function filterSavedSearches(query) {
_savedQuery = (query || '').trim();
_applySavedFilter();
}
function _applySavedFilter() {
var items = document.querySelectorAll('.saved-search-item');
var list = document.getElementById('saved-searches-list');
var q = _savedNorm(_savedQuery);
var hasVisible = false;
items.forEach(function(item) {
var type = item.dataset.type;
var show = _savedFilterType === 'all' || type === _savedFilterType;
var typeOk = _savedFilterType === 'all' || type === _savedFilterType;
var textOk = !q || _savedNorm(item.textContent).includes(q);
var show = typeOk && textOk;
item.style.display = show ? '' : 'none';
if (show) hasVisible = true;
});
// Remove a previous "no match" hint before deciding whether to add one.
if (list) {
var prev = list.querySelector('.sidebar-filter-empty');
if (prev) prev.remove();
}
// Show/hide empty state
var empty = document.getElementById('saved-searches-empty');
if (empty) empty.style.display = hasVisible ? 'none' : '';
// Explain an empty result caused by the global search rather than by an
// actually empty saved-searches list.
if (!hasVisible && q && list && items.length) {
var hint = document.createElement('div');
hint.className = 'sidebar-filter-empty';
hint.textContent = t('sidebar.no_results');
list.appendChild(hint);
}
}
+3 -1
View File
@@ -1545,6 +1545,8 @@
"sidebar.file_icons": "Icons",
"sidebar.filter_instant": "Instant search",
"sidebar.filter_ai": "Filter AI history...",
"sidebar.filter_recent": "Filter recent files...",
"sidebar.filter_saved": "Filter saved searches...",
"sidebar.filter_path": "Path filters",
"sidebar.filter_placeholder": "Filter files...",
"sidebar.filter_results_grouped": "Grouped results",
@@ -1849,7 +1851,7 @@
"bookslm.soon": "Soon",
"bookslm.ext_more_coming": "More options coming…",
"bookslm.deep_research_prompt": "Perform an in-depth analysis of the request by breaking it into steps, searching for relevant information in the vault, cross-referencing them, and producing a structured summary with sources.",
"bookslm.deep_research_started": "Deep research started — enable Agent mode to use the tools.",
"bookslm.deep_research_started": "Deep Research enabled — add your question and send.",
"bookslm.mode_general": "General",
"bookslm.mode_directory": "Directory",
"bookslm.mode_documents": "Documents",
+3 -1
View File
@@ -1545,6 +1545,8 @@
"sidebar.file_icons": "Icônes",
"sidebar.filter_instant": "Recherche instantanée",
"sidebar.filter_ai": "Filtrer l'historique IA...",
"sidebar.filter_recent": "Filtrer les fichiers récents...",
"sidebar.filter_saved": "Filtrer les recherches sauvegardées...",
"sidebar.filter_path": "Filtres de chemin",
"sidebar.filter_placeholder": "Filtrer fichiers...",
"sidebar.filter_results_grouped": "Résultats groupés",
@@ -1849,7 +1851,7 @@
"bookslm.soon": "Bientôt",
"bookslm.ext_more_coming": "D'autres options à venir…",
"bookslm.deep_research_prompt": "Réalise une analyse approfondie de la demande en décomposant les étapes, en cherchant les informations pertinentes dans le vault, en les croisant, et en produisant une synthèse structurée avec sources.",
"bookslm.deep_research_started": "Deep Research lancé — activez le mode Agent pour utiliser les outils.",
"bookslm.deep_research_started": "Deep Research activé — ajoutez votre question puis envoyez.",
"bookslm.mode_general": "Général",
"bookslm.mode_directory": "Répertoire",
"bookslm.mode_documents": "Documents",
+3 -2
View File
@@ -9676,6 +9676,7 @@ body.bookslm-resizing { cursor: ew-resize; user-select: none; }
.bookslm-chip-remove:hover { color: #f87171; }
.bookslm-chip-thumb { width: 18px; height: 18px; object-fit: cover; border-radius: 4px; }
.bookslm-chip-skill { border-color: var(--accent); }
.bookslm-chip-deep-research { border-color: var(--accent); }
/* Composer menus (`/` commands and `@` mentions). */
.bookslm-menu-layer { position: relative; }
.bookslm-command-menu, .bookslm-mention-menu { position: absolute; left: 12px; right: 12px; bottom: 4px;
@@ -9696,10 +9697,10 @@ body.bookslm-resizing { cursor: ew-resize; user-select: none; }
white-space: nowrap; }
.bookslm-menu-empty { padding: 10px; font-size: 12px; color: var(--text-secondary); text-align: center; }
/* Image attach button. */
.bookslm-btn-plus { display: flex; align-items: center; justify-content: center; flex-shrink: 0;
.bookslm-input-area button.bookslm-btn-plus { display: flex; align-items: center; justify-content: center; flex-shrink: 0;
background: none; border: 1px solid var(--border); color: var(--text-secondary); cursor: pointer;
border-radius: 50%; width: 32px; height: 32px; padding: 0; }
.bookslm-btn-plus:hover { color: var(--accent); border-color: var(--accent); }
.bookslm-input-area button.bookslm-btn-plus:hover { color: var(--accent); border-color: var(--accent); }
.bookslm-file-input { display: none; }
.bookslm-files-input { display: none; }
/* Extensible "+" panel (#97). */