From ee4c273d730a20a01cc5e6404dc9af3c66646c51 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sat, 12 Sep 2026 14:21:33 -0400 Subject: [PATCH] feat(ai): sidebar epuree, suivi visuel des requetes et correctif menu @ (BUG-012, #82) BUG-012: le menu @ est toujours rendu et _mentionVault() retombe sur le vault de contexte puis le premier vault disponible. #82: suppression des intitules Fournisseur & modele / Fournisseur:, description du contexte General en info-bulle, placeholder retire, bouton Envoyer en emoji, indicateur d'activite (envoi/reception/outils/confirmation/succes/echec), SW_VERSION v5. --- CHANGELOG.md | 18 ++++++ docs/ISSUES_TODOLIST.md | 2 + docs/features/ai-provider-picker.md | 13 +++- frontend/js/ai.js | 8 +-- frontend/js/bookslm.js | 97 +++++++++++++++++++++++------ frontend/locales/en.json | 9 ++- frontend/locales/fr.json | 9 ++- frontend/style.css | 14 +++++ frontend/sw.js | 2 +- tests/frontend/ai.test.mjs | 46 ++++++++++++++ 10 files changed, 190 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3315c..23be7ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -221,8 +221,26 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). `localStorage`) : l'envoi bascule vers `/api/ai/bookslm/agent`, affiche la trace des appels d'outils et les cartes de confirmation (Apply + aperçu diff) ; `/chat` reste le défaut. i18n FR/EN. +### Modifié + +- **#82 Assistant IA — barre latérale épurée & suivi visuel des requêtes** — la section + « Fournisseur & modèle » perd ses intitulés (titre de section et libellé « Fournisseur : ») + pour rester discrète ; la description du contexte Général n'est plus affichée (bandeau de + statut masqué, description accessible au **survol** de l'en-tête) ; le placeholder + « Posez une question sur ces documents… » est retiré ; le bouton « Envoyer » devient un **emoji + compact** (✈️). Un **indicateur d'activité** apparaît pendant le traitement et détaille le + workflow : envoi, réception de la réponse, appels d'outils (`Outil : X…`), attente de + confirmation, succès ou échec. i18n FR/EN (`ai.activity_*`, `ai.composer_label`). Tests JSDOM + (`tests/frontend/ai.test.mjs` +1). + ### Corrigé +- **BUG-012 — Assistant IA : la commande `@` n'affichait aucun menu en mode Général** — sans vault + résolu, la détection `@` ne listait rien (menu masqué) et le backend ignorait ensuite le + contexte. Le menu est désormais **toujours rendu** et `_mentionVault()` retombe sur le vault de + contexte de la barre latérale, puis sur le premier vault disponible, avant de basculer sur + `vault=all` pour les recherches. Tests JSDOM (`tests/frontend/ai.test.mjs` +1). + - **BUG-011 — Assistant IA : noms de modèles illisibles dans la liste déroulante** — la liste déroulante des modèles s'ouvrait alignée à gauche (`left: 0`) et pouvait dépasser le bord droit de la barre latérale (ancrée à droite), rendant les noms tronqués/illisibles ; elle est diff --git a/docs/ISSUES_TODOLIST.md b/docs/ISSUES_TODOLIST.md index c73e29f..cda4873 100644 --- a/docs/ISSUES_TODOLIST.md +++ b/docs/ISSUES_TODOLIST.md @@ -121,6 +121,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | *BUG-009* | Assistant IA : liste de modèles corrompue (art ASCII) et clés i18n brutes (`ai.model_search`) sous OpenRouter | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css` | Ouvrir le menu modèle sous « openrouter » | Locale non content-hashée en cache HTTP → `cache:'no-store'` + bump `SW_VERSION` v4 ; styles critiques du picker appliqués **en ligne** (popover absolu, liste en colonne, options `display:block`) ; rendu plafonné à 200 modèles + indicateur « … N autres » | OpenRouter expose plusieurs centaines de modèles ; si `style.css` est en cache, la liste s'affichait en bloc inline (art ASCII) et les clés i18n brutes provenaient d'un `fr.json` obsolète. Tests : `tests/frontend/ai.test.mjs` (+1) | | *BUG-010* | Assistant IA : la commande `@` n'ajoute pas le contexte à la requête en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général, taper `@fichier`, sélectionner, envoyer | Capture du `vault` renvoyé par `/api/tree-search` dans la sélection `@` ; `_contextVault()` propage ce vault aux requêtes `/context` et `/chat` ; recherches `@` suivantes limitées à ce vault | En mode Général, `_vault` est nul : la requête partait sans vault et le backend ignorait `extra_files`. Tests : `tests/frontend/ai.test.mjs` (+1) | | *BUG-011* | Assistant IA : noms de modèles illisibles dans la liste déroulante | 🟢 corrigé | P2 | 📱 frontend | IA | `frontend/js/ai.js`, `frontend/style.css` | Ouvrir le menu modèle dans la barre latérale de l'assistant | Popover aligné à droite du déclencheur (`right:0`), largeur 340 px bornée à `100vw - 24px`, noms sur plusieurs lignes (`overflow-wrap:anywhere`, police 0,78 rem) + `title` | La liste s'ouvrait vers la droite et dépassait le bord de la sidebar ancrée à droite ; noms tronqués. Tests : `tests/frontend/ai.test.mjs` | +| *BUG-012* | Assistant IA : la commande `@` n'affiche aucun menu de sélection en mode Général | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Assistant en mode Général (aucun vault), taper `@` | `_showMentionMenu` rend **toujours** le menu (même vide) et `_mentionVault()` retombe sur `state.selectedContextVault` puis le premier `state.allVaults`, avant `vault=all` pour les requêtes | Sans vault résolu, l'ancien code masquait le menu et le backend ignorait le contexte. Tests : `tests/frontend/ai.test.mjs` (+1) | ### TODOs techniques (améliorations / nouvelles tâches) @@ -147,6 +148,7 @@ Avant de corriger quoi que ce soit, un agent IA doit : | 2026-09-12 | BUG-007, BUG-008, #82 | Correction + refonte UI | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `backend/bookslm_routes.py`, `tests/frontend/ai.test.mjs`, `tests/test_bookslm.py` | BUG-007 : motifs Unicode + recherche NFD insensible aux accents + navigation ↑/↓ fiabilisée (jeton de séquence, scrollIntoView). BUG-008 : contexte ad-hoc `@` pris en compte en mode Général (backend) + repli `vault=all`. #82 : section « Fournisseur & modèle » compacte (recherche modèle + bulle capacités ℹ️ au survol/clic/appui long). Vérifié : tests frontend 41/41, `tests/test_bookslm.py` 47 passed. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-12 | BUG-009 | Correction | `frontend/js/ai.js`, `frontend/js/i18n.js`, `frontend/sw.js`, `frontend/style.css`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-009 : locale non content-hashée servie depuis le cache HTTP → clés i18n brutes ; passage en `cache:'no-store'` + `SW_VERSION` v4. Liste de modèles corrompue (OpenRouter, styles en cache) → styles critiques en ligne, options `display:block`, plafond 200 + indicateur. Vérifié : tests frontend 42/42. | 🟢 corrigé (en attente vérif utilisateur) | | 2026-09-12 | BUG-010, BUG-011 | Correction | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `tests/frontend/ai.test.mjs` | BUG-010 : capture du `vault` des résultats `@` + `_contextVault()` propagé aux requêtes `/context`/`/chat` (mode Général). BUG-011 : popover modèle aligné à droite, largeur 340 px, noms multi-lignes + `title`. Vérifié en navigateur (Playwright) et tests frontend 43/43. | 🟢 corrigé (en attente vérif utilisateur) | +| 2026-09-12 | BUG-012, #82 | Correction + UX | `frontend/js/bookslm.js`, `frontend/js/ai.js`, `frontend/style.css`, `frontend/sw.js`, `frontend/locales/{fr,en}.json`, `tests/frontend/ai.test.mjs` | BUG-012 : le menu `@` est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis le premier vault disponible. #82 : suppression des intitulés « Fournisseur & modèle » / « Fournisseur : », description du contexte Général déplacée en info-bulle, placeholder retiré, bouton Envoyer en emoji ✈️, indicateur d'activité (envoi/réception/outils/confirmation/succès/échec), `SW_VERSION` v5. Vérifié Playwright + tests frontend 45/45. | 🟢 corrigé (en attente vérif utilisateur) | --- diff --git a/docs/features/ai-provider-picker.md b/docs/features/ai-provider-picker.md index 112da50..78ac31b 100644 --- a/docs/features/ai-provider-picker.md +++ b/docs/features/ai-provider-picker.md @@ -52,7 +52,18 @@ propage ce vault aux requêtes `/context` et `/chat` et les recherches suivantes restent dans ce vault (BUG-010, mode Général). -## F. Points d'attention +## F. Barre latérale épurée & suivi visuel (BUG-012, #82) — ✅ livré +- [x] Section « Fournisseur & modèle » sans intitulés (ni titre de section, ni « Fournisseur : »). +- [x] Description du contexte Général retirée de l'affichage (bandeau de statut masqué) et + exposée via `title` au survol de l'en-tête. +- [x] Placeholder « Posez une question sur ces documents… » retiré ; bouton « Envoyer » remplacé + par l'emoji compact ✈️. +- [x] **Indicateur d'activité** (`.bookslm-activity`) affichant le workflow : envoi, réception, + outil en cours, attente de confirmation, terminé, échec (`ai.activity_*`). +- [x] `@` : le menu est toujours rendu et `_mentionVault()` retombe sur le vault de contexte puis + le premier vault disponible (BUG-012). + +## G. Points d'attention - La table de capacités reste **statique** (`backend/model_capabilities.py`) : un modèle inconnu retombe sur le défaut du fournisseur. - Le select masqué est conservé uniquement pour la rétro-compatibilité ; ne pas le supprimer sans diff --git a/frontend/js/ai.js b/frontend/js/ai.js index 25391cf..66c395c 100644 --- a/frontend/js/ai.js +++ b/frontend/js/ai.js @@ -139,13 +139,10 @@ async function _buildPickerUI() { const wrap = document.createElement('div'); wrap.className = 'ai-picker'; - // ── Provider select (compact) ── - const providerLabel = document.createElement('span'); - providerLabel.className = 'ai-picker-label'; - providerLabel.textContent = t('ai.provider') + ':'; - + // ── Provider select (compact, no label to keep the toolbar discreet) ── const providerSelect = document.createElement('select'); providerSelect.className = 'ai-picker-select'; + providerSelect.setAttribute('aria-label', t('ai.provider')); const defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.textContent = t('ai.provider_default'); @@ -445,7 +442,6 @@ async function _buildPickerUI() { _renderList(); } - wrap.appendChild(providerLabel); wrap.appendChild(providerSelect); wrap.appendChild(modelSelect); wrap.appendChild(combo); diff --git a/frontend/js/bookslm.js b/frontend/js/bookslm.js index e587196..c2c98c5 100644 --- a/frontend/js/bookslm.js +++ b/frontend/js/bookslm.js @@ -107,6 +107,8 @@ class BooksLM { this._menuKind = null; // Monotonic token used to drop stale async menu renders (fast typing). this._menuSeq = 0; + // Timer that hides the "done" activity indicator after a short delay. + this._activityHideTimer = null; } _readAgentMode() { @@ -333,6 +335,20 @@ class BooksLM { return this._resolveVault(); } + /** + * Concrete vault used to browse files in the `@` mention menu. Falls back to + * the sidebar's selected context vault (then the first vault) so the menu can + * list files even in General mode where no vault is otherwise resolved. + */ + _mentionVault() { + const resolved = this._contextVault(); + if (resolved) return resolved; + const selected = state.selectedContextVault; + if (selected && selected !== 'all') return selected; + const first = Array.isArray(state.allVaults) ? state.allVaults[0] : null; + return (first && (first.name || first)) || null; + } + /** Open a file mentioned in an assistant answer in the main viewer. */ async _openFileLink(path) { const vault = this._resolveVault(); @@ -433,7 +449,9 @@ class BooksLM { return { icon: 'sparkles', title: t('ai.context_general'), - subtitle: t('ai.context_general_desc'), + // The description is kept for the tooltip only, to keep the header clean. + subtitle: '', + tooltip: t('ai.context_general_desc'), }; } @@ -468,12 +486,15 @@ class BooksLM {
- ${t('ai.model_settings')}
+
@@ -482,8 +503,8 @@ class BooksLM {
- - + +
${t('ai.input_hint')}
`; @@ -820,12 +841,7 @@ class BooksLM { async _showMentionMenu(query) { if (!this._panel) return; const seq = ++this._menuSeq; - const vault = this._contextVault(); - if (!vault && !query) { - // Nothing to list without a vault; a typed query can still search all. - this._hideMenus(); - return; - } + const vault = this._mentionVault(); try { let results = []; if (query) { @@ -834,24 +850,25 @@ class BooksLM { `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(searchVault)}` ); results = (data.results || []).slice(0, 20); - } else { + } else if (vault) { const data = await api( `/api/vault/${encodeURIComponent(vault)}/files?limit=20&recursive=true` ); - results = (data.files || []).slice(0, 20).map((f) => ({ path: f.path, type: 'file' })); + results = (data.files || []).slice(0, 20).map((f) => ({ path: f.path, type: 'file', vault })); } if (seq !== this._menuSeq) return; // superseded while the request ran const items = results.map((r) => { const path = r.path || r; const rawType = r.type || (this._classifyPath(path) === 'dir' ? 'dir' : 'file'); const type = rawType === 'directory' ? 'dir' : rawType; - return { id: path, label: path, type, vault: r.vault || null }; + return { id: path, label: path, type, vault: r.vault || vault || null }; }); + // Always render the menu (even empty) so typing `@` gives visible feedback. this._renderMenu('mention', items, (item) => this._selectMention(item)); } catch (e) { if (seq !== this._menuSeq) return; console.warn('AI assistant: mention search failed', e); - this._hideMenus(); + this._renderMenu('mention', [], () => {}); } } @@ -1185,37 +1202,68 @@ class BooksLM { _updateHeader() { if (!this._panel) return; const meta = this._contextMeta(); + const titles = this._panel.querySelector('.bookslm-header-titles'); const title = this._panel.querySelector('.bookslm-title'); const subtitle = this._panel.querySelector('.bookslm-subtitle'); const icon = this._panel.querySelector('.bookslm-context-icon i'); if (title) title.textContent = meta.title; if (subtitle) subtitle.textContent = meta.subtitle || ''; + if (titles) titles.title = meta.tooltip || meta.subtitle || meta.title || ''; if (icon) icon.setAttribute('data-lucide', meta.icon); if (typeof safeCreateIcons === 'function') safeCreateIcons(); } + /** + * Show the assistant activity state in the panel (visual workflow). + * @param {'idle'|'working'|'done'|'error'} kind + * @param {string} [text] + */ + _setActivity(kind, text) { + if (!this._panel) return; + if (this._activityHideTimer) { + clearTimeout(this._activityHideTimer); + this._activityHideTimer = null; + } + const el = this._panel.querySelector('.bookslm-activity'); + if (!el) return; + const label = el.querySelector('.bookslm-activity-text'); + if (label) label.textContent = text || ''; + el.classList.toggle('hidden', kind === 'idle'); + el.classList.toggle('working', kind === 'working'); + el.classList.toggle('done', kind === 'done'); + el.classList.toggle('error', kind === 'error'); + } + _updateStatus(data) { if (!this._panel) return; const status = this._panel.querySelector('.bookslm-status'); if (!status) return; if (data && data.error) { + status.classList.remove('hidden'); status.innerHTML = `⚠ ${t('bookslm.no_context')}`; return; } if (data && Array.isArray(data.files)) { + if (data.scope === MODE.GENERAL) { + // No document context: keep the header clean. The description is + // available as a tooltip on the header title. + status.classList.add('hidden'); + status.innerHTML = ''; + return; + } + status.classList.remove('hidden'); const count = data.files.length; const chars = data.total_chars || 0; const maxChars = data.max_total_chars || 200000; const pct = Math.min(100, Math.round((chars / maxChars) * 100)); - const label = data.scope === MODE.GENERAL - ? t('ai.context_general_desc') - : `${t('bookslm.files_indexed', { count })}, ${t('bookslm.chars_loaded', { chars: Math.round(chars / 1000) + 'K' })}`; + const label = `${t('bookslm.files_indexed', { count })}, ${t('bookslm.chars_loaded', { chars: Math.round(chars / 1000) + 'K' })}`; status.innerHTML = ` ${label}
`; } else if (this._isLoading) { + status.classList.remove('hidden'); status.textContent = '⏳ …'; } } @@ -1630,6 +1678,7 @@ class BooksLM { if (sendBtn) sendBtn.disabled = true; this._abortCtrl = new AbortController(); + this._setActivity('working', t('ai.activity_sending')); try { let resp = await this._postChat(payload); @@ -1643,6 +1692,7 @@ class BooksLM { throw new Error(detail); } + this._setActivity('working', t('ai.activity_streaming')); await this._streamResponse(resp, assistantMsg, payload); if (!assistantMsg.sources.length && this._contextFiles.length) { @@ -1654,10 +1704,15 @@ class BooksLM { }) .map((f) => f.path || f); } + this._setActivity('done', t('ai.activity_done')); + this._activityHideTimer = setTimeout(() => this._setActivity('idle'), 1500); } catch (e) { if (e.name !== 'AbortError') { assistantMsg.content = `⚠ Error: ${e.message}`; console.warn('AI assistant chat error:', e); + this._setActivity('error', t('ai.activity_error')); + } else { + this._setActivity('idle'); } } @@ -1714,6 +1769,7 @@ class BooksLM { if (currentEvent === 'tool') { assistantMsg.toolCalls.push({ name: data.name, ok: data.ok !== false, arguments: data.arguments || {} }); + this._setActivity('working', t('ai.activity_tool', { name: data.name })); return; } if (currentEvent === 'confirmation') { @@ -1721,15 +1777,20 @@ class BooksLM { pending: data.pending || data, messages: data.messages || [], }; + this._setActivity('working', t('ai.activity_confirmation')); return; } if (currentEvent === 'error' || data.error) { const msg = data.error || t('bookslm.error'); assistantMsg.content += (assistantMsg.content ? '\n\n' : '') + '⚠ ' + msg; + this._setActivity('error', t('ai.activity_error')); return; } const chunk = data.token || data.content || ''; - if (chunk) assistantMsg.content += chunk; + if (chunk) { + if (!assistantMsg.content) this._setActivity('working', t('ai.activity_streaming')); + assistantMsg.content += chunk; + } if (data.sources) assistantMsg.sources = data.sources; }; diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 01ffbd2..370acec 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -142,6 +142,13 @@ "ai.cap_video": "Video", "ai.cap_vision": "Vision capable", "ai.commands": "Commands", + "ai.activity_sending": "Sending request…", + "ai.activity_streaming": "Receiving response…", + "ai.activity_tool": "Tool: {name}…", + "ai.activity_confirmation": "Waiting for confirmation…", + "ai.activity_error": "Request failed", + "ai.activity_done": "Done", + "ai.composer_label": "Message to the assistant", "ai.create_skill": "Create a skill", "ai.image": "image", "ai.keys": "Stored API keys", @@ -1712,7 +1719,7 @@ "bookslm.title": "BooksLM", "bookslm.files_indexed": "{count} files indexed", "bookslm.chars_loaded": "{chars} chars loaded", - "bookslm.placeholder": "Ask a question about these documents...", + "bookslm.placeholder": "", "bookslm.send": "Send", "bookslm.new_conversation": "New conversation", "bookslm.export": "Export conversation", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index d517ca4..aabd5d5 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -142,6 +142,13 @@ "ai.cap_video": "Video", "ai.cap_vision": "Vision capable", "ai.commands": "Commandes", + "ai.activity_sending": "Envoi de la requête…", + "ai.activity_streaming": "Réponse en cours…", + "ai.activity_tool": "Outil : {name}…", + "ai.activity_confirmation": "En attente de confirmation…", + "ai.activity_error": "Échec de la requête", + "ai.activity_done": "Terminé", + "ai.composer_label": "Message à l'assistant", "ai.create_skill": "Créer un skill", "ai.image": "image", "ai.keys": "Clés API enregistrées", @@ -1712,7 +1719,7 @@ "bookslm.title": "BooksLM", "bookslm.files_indexed": "{count} fichiers indexés", "bookslm.chars_loaded": "{chars} caractères chargés", - "bookslm.placeholder": "Posez une question sur ces documents...", + "bookslm.placeholder": "", "bookslm.send": "Envoyer", "bookslm.new_conversation": "Nouvelle conversation", "bookslm.export": "Exporter la conversation", diff --git a/frontend/style.css b/frontend/style.css index 504e6cf..8b54f9a 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -9202,6 +9202,19 @@ body.popup-mode .content-area { .bookslm-status .bookslm-context-bar { flex: 1; height: 4px; border-radius: 2px; background: var(--surface2); overflow: hidden; } .bookslm-status .bookslm-context-fill { height: 100%; border-radius: 2px; background: var(--accent); transition: width 0.3s; } +/* Assistant activity indicator (visual workflow: sending/streaming/tools/done/error). */ +.bookslm-activity { display: flex; align-items: center; gap: 8px; padding: 6px 16px; + font-size: 12px; color: var(--text-secondary); border-top: 1px solid var(--border); + background: var(--bg-secondary); } +.bookslm-activity.hidden { display: none; } +.bookslm-activity.error { color: #f87171; } +.bookslm-activity.done { color: var(--success, #3fb950); } +.bookslm-activity-spinner { width: 12px; height: 12px; border: 2px solid var(--border); + border-top-color: var(--accent); border-radius: 50%; flex-shrink: 0; + animation: bookslm-spin 0.8s linear infinite; } +.bookslm-activity.done .bookslm-activity-spinner, +.bookslm-activity.error .bookslm-activity-spinner { animation: none; border-top-color: currentColor; } +@keyframes bookslm-spin { to { transform: rotate(360deg); } } .bookslm-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; } .bookslm-bubble { max-width: 85%; padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; word-wrap: break-word; } .bookslm-bubble.user { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; } @@ -9220,6 +9233,7 @@ body.popup-mode .content-area { .bookslm-input-area textarea:focus { border-color: var(--accent); outline: none; } .bookslm-input-area button { padding: 8px 16px; border-radius: 8px; border: none; background: var(--accent); color: #fff; cursor: pointer; font-size: 14px; } +.bookslm-input-area button.bookslm-btn-send { padding: 8px 12px; font-size: 16px; line-height: 1; } .bookslm-input-area button:hover { opacity: 0.9; } .bookslm-input-area button:disabled { opacity: 0.5; cursor: not-allowed; } .bookslm-input-hint { padding: 0 16px 10px; font-size: 10px; color: var(--text-secondary); diff --git a/frontend/sw.js b/frontend/sw.js index dc21778..afe7c24 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -11,7 +11,7 @@ * cache or Cloudflare does NOT clear the Service Worker Cache Storage, which is * a separate store. Bumping SW_VERSION invalidates it on every release. */ -const SW_VERSION = 'v4'; +const SW_VERSION = 'v5'; const CODE_CACHE = `obsigate-code-${SW_VERSION}`; const RUNTIME_CACHE = `obsigate-runtime-${SW_VERSION}`; const API_CACHE = `obsigate-api-${SW_VERSION}`; diff --git a/tests/frontend/ai.test.mjs b/tests/frontend/ai.test.mjs index 1e63494..65c8440 100644 --- a/tests/frontend/ai.test.mjs +++ b/tests/frontend/ai.test.mjs @@ -1024,6 +1024,52 @@ async function main() { localStorage.clear(); }); + // ── 42b. Assistant activity indicator (#82) ── + await test("activity indicator reflects the request workflow", () => { + const b = new BooksLM(); + b._panel = b._render(); + document.body.appendChild(b._panel); + const el = b._panel.querySelector(".bookslm-activity"); + assert.ok(el, "activity element present"); + assert.ok(el.classList.contains("hidden"), "hidden initially"); + b._setActivity("working", "Envoi…"); + assert.ok(!el.classList.contains("hidden"), "shown while working"); + assert.ok(el.classList.contains("working")); + assert.equal(el.querySelector(".bookslm-activity-text").textContent, "Envoi…"); + b._setActivity("error", "Échec"); + assert.ok(el.classList.contains("error")); + b._setActivity("idle"); + assert.ok(el.classList.contains("hidden"), "hidden when idle"); + b._panel.remove(); + }); + + await test("mention menu falls back to an available vault", async () => { + localStorage.clear(); + const stateMod = await import(pathToFileURL(path.join(JS_DIR, "state.js")).href); + const prev = stateMod.state.allVaults; + stateMod.state.allVaults = [{ name: "FallbackVault" }]; + let lastUrl = ""; + globalThis.fetch = async (url) => { + lastUrl = String(url); + if (lastUrl.includes("/files")) { + return { ok: true, status: 200, json: async () => ({ files: [{ path: "a.md" }] }) }; + } + return { ok: true, status: 200, json: async () => ({}) }; + }; + const b = new BooksLM(); + b._panel = b._render(); + document.body.appendChild(b._panel); + b._mode = MODE.GENERAL; + b._vault = null; + await b._showMentionMenu(""); + const menu = b._panel.querySelector(".bookslm-mention-menu"); + assert.ok(!menu.classList.contains("hidden"), "empty-query menu is shown"); + assert.ok(lastUrl.includes("FallbackVault"), "uses the fallback vault"); + stateMod.state.allVaults = prev; + b._panel.remove(); + localStorage.clear(); + }); + // ── Summary ── console.log(`\n${passCount}/${testCount} tests passed`); if (passCount !== testCount) {