# #94–#97 — Assistant IA : historique permanent, accès sidebar, bouton rond et panneau « + » > **Statut :** ✅ Livré (en attente de validation utilisateur) > **Effort :** ~7 jours | **Impact :** 🟡 > **Références :** [Roadmap](../ROADMAP.md) · [Changelog](../../CHANGELOG.md) - **Description :** quatre améliorations de l'Assistant IA livrées ensemble : 1. **#94** — bouton de soumission **circulaire** avec icône Lucide `arrow-up` (remplace l'avion ✈️). 2. **#95** — **historique permanent** des conversations **persisté côté backend** (les échanges survivent aux rechargements de page et aux navigateurs). 3. **#96** — accès rapide à l'**historique depuis la sidebar de navigation** (onglet dédié). 4. **#97** — panneau **« + » extensible** remplaçant « Attach an image » (modules : fichiers, image, contextes, skills, Deep Research, web, Canva). ## A. Backend — persistance des conversations (#95) — ✅ livré - [x] **`backend/ai_history.py`** (nouveau) : stockage JSON par utilisateur (`data/ai_history/{username}.json`), écriture atomique (tmp + `shutil.move`), plafond `MAX_SESSIONS = 200` (purge des plus anciennes), tri par `updatedAt` desc. - [x] `list_sessions`, `get_session`, `upsert_session`, `delete_session` ; la liste renvoie des **résumés sans messages** (`_summary` : titre, mode, vault, contexte, `message_count`, `preview`) pour rester légère. - [x] **`backend/bookslm_routes.py`** : modèle Pydantic `BookslmSession` et 4 endpoints : - `GET /api/ai/bookslm/history` — liste des conversations (résumés). - `GET /api/ai/bookslm/history/{session_id}` — conversation complète (404 si absente). - `PUT /api/ai/bookslm/history/{session_id}` — création/mise à jour (l'`id` du corps est forcé à l'`id` du path → jamais d'écriture sous une autre clé). - `DELETE /api/ai/bookslm/history/{session_id}` — suppression (booléen `ok`). - [x] Isolation par utilisateur (`require_auth`) ; saisies défensives (username/id vides) → `[]` ou `None`. ## B. Frontend — sync serveur + cache local (#95) — ✅ livré - [x] `frontend/js/bookslm.js` : clé local globale `bookslm-sessions-all-{username}` ; **migration** des anciennes clés périmées `bookslm-sessions-` et `bookslm-history-` à la première ouverture. - [x] `_loadHistory(preferredSessionId)` **async** au chargement du panneau : lecture serveur (`GET /history`), hydratation du localStorage, repli hors-ligne silencieux si le serveur ne répond pas. - [x] Synchronisation **debounced 600 ms** (`_syncHistoryToServer` → `_flushHistoryToServer`) via `_dirtySessionIds` / `_deletedIds` : `PUT`/`DELETE` seulement pour les sessions modifiées ; pas d'appel réseau à la simple ouverture. - [x] `_positionSession` : nouvelle session insérée en tête, session rechargée remise à jour. - [x] `openContext(opts, preferredSessionId)` **async** ; `openWithSession(sessionOrId)` public pour ouvrir une conversation connue (utilisé par la sidebar #96). - [x] `_notifyHistoryChanged()` : événement `bookslm:history-updated` pour rafraîchir la sidebar #96 sans rechargement. ## C. Frontend — sidebar & historique (#96) — ✅ livré - [x] `frontend/index.html` : cinquième onglet `#sidebar-tab-ai` (icône Lucide `messages-square`) et panneau `#sidebar-panel-ai` (`#ai-history-list`, `#ai-history-empty`). - [x] `frontend/js/config.js` : `loadAISessionList()` / `renderAIHistoryList()` (réutilise les classes `.recent-*` de l'UI), placeholder « Aucune conversation », listener `bookslm:history-updated` monté par `initSidebarTabs()` et actif quand l'onglet `ai` est affiché. - [x] Le clic sur une conversation l'ouvre dans le panneau Assistant IA (via `openWithSession`) et bascule la sidebar. ## D. Frontend — panneau « + » extensible (#97) — ✅ livré - [x] Bouton « Attach an image » remplacé par un bouton **« + »** circulaire (`frontend/js/bookslm.js`, `.bookslm-btn-plus`). - [x] Panneau overlay `.bookslm-ext-menu` (dropdown au-dessus de la zone de saisie, fermeture au clic extérieur / Échap) listant les modules. - [x] Architecture **modulaire** : registre `_extensions` (objet d'enregistrement simple) — chaque entrée : `id`, icône, libellé i18n, action ; `renderExtMenu()` construit la liste ; un nouveau module s'ajoute en une entrée. - [x] Modules livrés : **Fichiers** (sélecteur général `.bookslm-files-input`), **Image** (joindre une image), **Contextes/fichiers**, **Skills**, **Deep Research** (mode agent + prompt pré-rempli), **Recherche sur Internet**, **Canva**. - [x] `web`/`canva` affichés **désactivés** avec badge « Bientôt » (`.bookslm-ext-soon`) — le catalogue d'outils #92 les alimentera. - [x] `_startDeepResearch()` : active le mode agent, injecte le prompt Deep Research et déclenche l'envoi. ## E. UI — bouton rond & icône (#94) — ✅ livré - [x] `.bookslm-btn-send` circulaire (40 px, `border-radius: 50%`), icône Lucide `arrow-up` à la place de l'emoji ✈️, aligné en bas à droite de la zone de saisie. - [x] i18n FR/EN : `bookslm.add_options`, `bookslm.ext_files`, `bookslm.ext_image_hint`, `bookslm.ext_contexts`, `bookslm.ext_skills`, `bookslm.ext_deep_research`, `bookslm.ext_web`, `bookslm.ext_canva`, `bookslm.coming_soon`, `bookslm.soon`, `bookslm.ext_more_coming`, `bookslm.deep_research_prompt`, `bookslm.deep_research_started`, `bookslm.mode_*`. ## F. Tests — ✅ livré - [x] `tests/test_bookslm.py` : `TestBooksLMSessionHistoryEndpoints` (7) + `TestAIGatewayHistoryStore` (4) — liste résumée sans messages, full 404, upsert qui force l'id et isole par utilisateur, delete, cap à 200, purge. - [x] `tests/frontend/ai.test.mjs` : persistance/reouverture/suppression de sessions, migration de l'historique legacy, menu d'historique. - [x] Vérifs : pytest **1088 passed / 6 skipped**, ruff 0, mypy 0 (71 fichiers), frontend `ai.test.mjs` **84/84**, `unit.test.mjs` 9/9, `validate-imports` 38 modules. ## H. Complément — filtre de la sidebar et menu « + » (BUG-048, #98) — ✅ livré En retour utilisateur sur la version 2.4.0 : *(1) **Filtre fonctionnel sur l'onglet « Historique IA » (sidebar) — #98** — la barre de filtrage globale de la sidebar agissait uniquement sur les onglets Fichiers/Tags. Elle s'applique désormais aussi à l'historique IA :* - Utile `filterAIHistory(query)` (`frontend/js/config.js`) : filtre le cache de sessions `_aiSessionsCache` (peuplé par `loadAISessionList()`) sur **titre, aperçu, répertoire, contexte ou libellé de mode** traduit, insensible à la casse et aux accents (`_aiNorm` : `NFD` + suppression des diacritiques + `toLowerCase`). Une requête sans résultat affiche `bookslm.history_no_match` (nouvelle clé i18n FR/EN) **dans la liste**, en plus de l'état vide d'origine ; un champ vide restaure tout. - Routage dans `initSidebarFilter` (`frontend/js/sidebar.js`) : la saisie (debounce 220 ms), le bouton casse `Aa` et le bouton « × » dirigent vers `filterAIHistory` quand l'onglet IA est actif (`state.activeSidebarTab`), au lieu de `filterTagCloud`. Le placeholder devient `sidebar.filter_ai` (« Filtrer l'historique IA… » / « Filter AI history… ») — résolu dans `switchSidebarTab` (config.js), qui re-charge aussi la liste à chaque entrée dans l'onglet en ré-appliquant la requête courante. *(2) **Menu « + » du panneau Assistant — BUG-048** — « ajouter des contextes » ouvrait un menu « @ » jamais affiché (et idem pour « ajouter des skills » → « / ») :* le clic sur une entrée du panneau `.bookslm-ext-menu` remontait au gestionnaire `click` du panneau (`_hideMenus()` + `_menuSeq++`), qui annulait le rendu asynchrone du menu ouvert juste après. `e.stopPropagation()` est ajouté sur chaque entrée de `_renderExtMenu()` (bookslm.js) : les menus « @ » (contextes) et « / » (skills) restent affichés. Le bouton d'ouverture porte bien l'icône Lucide `plus` (vérifié par test JSDOM). **Points d'attention** - Le filtrage reste **client-side** : la charge est triviale vu le cap de 200 sessions (`MAX_SESSIONS`). Un futur filtrage serveur n'est justifié que si la rétention croît. - `filterAIHistory` est exportée en plus de `loadAISessionList` : `validate-imports` couvre le nouveau contrat d'import de `sidebar.js`. ### Tests du complément - `tests/frontend/ai-sidebar.test.mjs` (nouveau, 6 tests) : rendu complet, filtre par titre (casse), accents (« cafe » → « café au lait »), aperçu/répertoire, restauration complète, message « aucune correspondance ». - `tests/frontend/ai.test.mjs` (+3) : clic « Contextes » → menu « @ » visible listé, clic « Skills » → menu « / » visible listé, icône `plus` présente sur le bouton « + ». - Vérifs : frontend IA **87/87**, `ai-sidebar` **6/6**, `unit` 9/9, 9 suites JSDOM vertes, `validate-imports` 38 modules ; backend inchangé (pytest / ruff / mypy valides). ## G. Points d'attention - Le localStorage reste un **cache** : le serveur est la source de vérité (`data/ai_history/`). En cas de données locales corrompues, un `localStorage.clear()` réinitialise proprement. - `MAX_SESSIONS = 200` : le comportement de purge est testé (`TestAIGatewayHistoryStore`) ; la rétention configurable (item #95 du backlog) pourra s'appuyer sur ce plafond. - Les modules web/Canva du panneau « + » sont volontairement désactivés tant que l'écosystème d'outils phase 2 (#92) n'est pas livré. ## I. Complément — icône « + » et pastille Deep Research (BUG-049, #100) — ✅ livré *(1) **BUG-049 — l'icône du bouton « + » n'était pas visible.*** Le SVG Lucide était bien rendu, mais la règle générique `.bookslm-input-area button { padding: 8px 16px; background: var(--accent); color:#fff }` l'emportait en **spécificité** sur `.bookslm-btn-plus` (une classe seule). Le bouton conservait `width:32px` avec `padding: 8px 16px` → **largeur de contenu = 0 px**, donc SVG à `width: 0px` (invisible). Correctif CSS : sélecteur porté à `.bookslm-input-area button.bookslm-btn-plus` (et `:hover`), qui reprend la main (`padding:0`, fond transparent, couleur `--text-secondary`). Vérifié en navigateur (Playwright) : `svgWidth` passe de `0px` à `18px`. *(2) **#100 — Deep Research devient une pastille (comme les skills).*** Auparavant, cliquer sur « Deep Research » injectait la directive de recherche dans la zone de saisie. Désormais : - `_startDeepResearch()` active le **mode Agent** (si nécessaire), positionne le drapeau `_activeDeepResearch` et rend une **pastille** `.bookslm-chip-deep-research` (`_renderAttachments()`), sans rien écrire dans le composeur. - La pastille se retire via son « × » (comme les chips skills/fichiers) et remet le drapeau à `false`. - La directive (`bookslm.deep_research_prompt`) est injectée **au moment de l'envoi** dans le `message` du payload (`_sendMessage()`), sans polluer le message affiché à l'utilisateur. - Message d'information mis à jour (`bookslm.deep_research_started`) : « Deep Research activé — ajoutez votre question puis envoyez. » ### Tests du complément - `tests/frontend/ai.test.mjs` (+1) : le clic sur « Deep Research » ajoute une pastille, laisse le composeur vide, positionne le drapeau, et le retrait de la pastille remet le drapeau à `false`. - Vérification navigateur du bouton « + » (Playwright, instance de test).