feat(sidebar+assistant): filtre Recents/Sauvegardes (#99), pastille Deep Research (#100) et icone du bouton + (BUG-049)
This commit is contained in:
+17
-14
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user