177 lines
12 KiB
Markdown
177 lines
12 KiB
Markdown
# #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-<ctx>` et
|
||
`bookslm-history-<ctx>` à 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). |