Files
ObsiGate/docs/features/ai-assistant-conversation-ux.md
T
bruno 56b46cde0e
CI / lint (push) Successful in 1m32s
CI / security (push) Successful in 1m0s
CI / test (push) Successful in 3m8s
CI / build (push) Successful in 56s
CI / e2e (push) Successful in 10m44s
fix(ai): chaine de repli web_search (SearXNG -> DuckDuckGo -> Bing, BUG-051)
2026-09-16 23:13:44 -04:00

159 lines
11 KiB
Markdown

# #91 — Assistant IA — Zone de discussion façon Notion : post ancré en haut, fournisseur/modèle discret & barre d'actions
> **Statut :** ✅ Livré
> **Effort :** 0,5-1 jour | **Impact :** 🟡
> **Références :** [Roadmap](../ROADMAP.md) · [BooksLM #76](./bookslm.md) · [Assistant UX #80](./ai-assistant-ux.md) · [Changelog](../../CHANGELOG.md)
- **Description :** Refonte de la présentation de la fenêtre de résultats de
l'assistant IA (`frontend/js/bookslm.js`, `frontend/style.css`) sur le modèle de
l'assistant Notion :
1. **Ancrage en haut** : après l'envoi, le post de l'utilisateur est amené tout en
haut de la zone visible (`scrollIntoView({ block: 'start' })` +
`scroll-margin-top`) ; la réponse se diffuse **en dessous**, lisible sans
défilement. Plus de scroll-into-bottom ni d'astuce `order: -1`.
2. **Fil Notion-style** : messages en ordre chronologique ; post utilisateur =
bulle arrondie alignée à droite (max 80 %) ; réponse assistant = texte pleine
largeur **sans bulle de fond** ; les appels d'outils du mode agent se replient
dans un bloc discret « N étapes » (`<details>`).
3. **Fournisseur & modèle discrets** : libellé au-dessus de chaque réponse,
issu du SSE réel du backend (`provider`/`model`).
4. **Barre d'actions au survol** : sous chaque bloc non vide, « Copier »
(utilisateur et assistant) ; sous une réponse, « Ajouter » qui insère le texte
dans le document ouvert dans l'éditeur Forge (aucun bouton factice).
## A. Positionnement automatique (ancre en haut) — ✅ livré
- [x] **A1.** `_renderMessages({ anchor: true })` appelé à l'envoi du message, à
l'ouverture d'un contexte, au rechargement d'une session et à la reprise après
confirmation agent : cible = dernier `.bookslm-msg.user`, `scrollIntoView`
`block: 'start'` (instantané à l'envoi, fluide à l'ouverture d'une session),
repli calcul `scrollTop += delta` si indisponible.
- [x] **A2.** Re-rendus de streaming : tant que l'ancre est active (flag
`_pinnedTurn` posé à l'envoi, libéré sur `wheel`/`touchmove`/`mousedown`), chaque
re-rendu — frames de streaming **et** rendu final — ré-ancre instantanément la
question en haut ; la réponse s'allonge sous la question sans jamais déplacer la
vue. Hors épinglage, la position de scroll est préservée (plus de
`scrollTop = scrollHeight`).
- [x] **A3.** Question **au bord** : plus de `scroll-margin-top` (le moindre écart
laissait visible la fin de la réponse précédente au-dessus du post) ; le post
s'aligne exactement sur le haut de la zone de défilement.
- [x] **A3bis.** **Remplissage dynamique** : une réponse courte ne remplit pas la
fenêtre — le navigateur bloquait alors le défilement et la question restait à
mi-hauteur. Le `padding-bottom` du fil est augmenté de la place manquante
(`target - max`) tant que l'ancre est active, puis retiré au dé-épinglage ; si
le fil tient entièrement dans la fenêtre, aucun remplissage n'est ajouté.
- [x] **A3ter.** **Anti-dérive** : `overflow-anchor: none` sur `.bookslm-messages`
(Chrome ré-ancrait le défilement à chaque reconstruction du fil — un token =
un re-rendu — laissant un décalage constant de ~33 px) et passe de correction
après `scrollIntoView` : la géométrie mesurée (`getBoundingClientRect`) fait foi,
pas la demande de défilement. Vérifié en live : 5 questions, 0 décalage.
- [x] **A4.** Seule la zone du fil défile : `.bookslm-messages { flex: 1; overflow-y: auto }`
(en-tête, toolbar, statut et barre de saisie fixes — structure déjà en place).
## B. Fil chronologique Notion-style — ✅ livré
- [x] **B1.** Ordre naturel (user puis assistant, empilés, `gap: 24px`).
- [x] **B2.** Utilisateur : bulle `--surface2` alignée à droite, `max-width: 80%`,
coins arrondis 18px.
- [x] **B3.** Assistant : `width: 100%`, pas de fond ni de padding de bulle —
le markdown occupe toute la largeur (titres, listes, code, sources inchangés).
- [x] **B4.** Mode agent : `_renderToolActivity()` → `<details class="bookslm-tool-trace">`
avec `<summary>` « N étapes » (i18n `ai.steps_count`) ; les lignes d'outils
restent accessibles en déroulant le bloc.
## C. Fournisseur & modèle discrets — ✅ livré
- [x] **C1.** `_streamResponse()` capture `data.provider` / `data.model` du flux SSE
(déjà émis par `/chat` et `/agent`) sur le message assistant.
- [x] **C2.** `.bookslm-msg-meta` au-dessus du bloc assistant :
« fournisseur · modèle » (persisté avec la session, visible au rechargement).
- [x] **C3.** Le backend émet le modèle **réellement utilisé** :
`_effective_model(provider, req.model)` résout le défaut du fournisseur quand le
client ne précise pas de modèle (avant : `req.model or ""` → tag réduit au seul
fournisseur). Idem dans les deux flux SSE (`/chat` et `/agent`).
## D. Barre d'actions — ✅ livré
- [x] **D1.** `_appendActionBar()` sous chaque bloc non vide : bouton Copier pour
les deux rôles ; bouton Ajouter pour l'assistant uniquement.
- [x] **D2.** Copier : texte brut du message (`navigator.clipboard`, repli
`execCommand`) + toast `bookslm.copied`.
- [x] **D3.** Ajouter (`_insertIntoEditor`) : insertion du texte à la position de
fin de sélection dans `state.editorView` (Forge) ; toast `bookslm.inserted` ou
`bookslm.insert_no_editor` si aucun éditeur ouvert.
- [x] **D4.** Révélation au survol / focus clavier (`.bookslm-msg:hover`,
`:focus-within`) — icônes Lucide `copy` / `corner-down-left`.
## E. Tests & documentation — ✅ livré
- [x] **E1.** `tests/frontend/ai.test.mjs` : ordre chronologique, ancrage
`scrollIntoView block:start`, tag provider/modèle, barre d'actions par rôle,
copie du texte brut, insertion éditeur (mock `state.editorView`), bloc
repliable des étapes, absence de barre pour contenu vide.
- [x] **E2.** i18n FR/EN : `bookslm.copied`, `bookslm.insert`, `bookslm.insert_hint`,
`bookslm.inserted`, `bookslm.insert_no_editor`, `ai.steps_count`.
- [x] **E3.** CHANGELOG + Roadmap ; `SW_VERSION` bump (cache bust).
## F. Points d'attention
- Le tag fournisseur provient du **backend réel** (SSE), donc correct même avec la
sélection « par défaut » du fournisseur.
- Le streaming appelle `_renderMessages()` à chaque chunk : l'ancre est posée une
seule fois à l'envoi, les re-rendus préservent ensuite le scroll — pas de sauts.
- L'ancien épinglage flex `order: -1` (première itération de #91) est **remplacé** :
l'ordre est redevenu chronologique, l'ancrage est fait par défilement explicite.
- Les pouces haut/bas Notion ne sont **pas** repris : aucun signal n'existe
côté backend ; « Ajouter » (insertion éditeur réelle) les remplace utilement.
## G. Section « steps » enrichie + outils web (complément #91) — ✅ livré
- [x] **G1.** Chaque étape est décrite par le **backend** (`backend/tools/labels.py`) :
événement SSE `tool` avec `step {key, params}` ; le frontend résout
`ai.step.<key>` (FR/EN) → phrases humaines : « Recherche dans le vault : pizza »,
« Fichier lu : notes/a.md », etc. Outil inconnu → libellé générique (jamais cassé).
- [x] **G2.** Événements en **direct** : les steps sont poussés sur une file
`asyncio.Queue` par la boucle agent et émis dès leur exécution — le bloc
« N étapes » grandit pendant que l'assistant travaille (plus de liste fin de run).
- [x] **G3.** Sous-section **« Réflexion ▶ / ▼ »** : quand le modèle émet un texte
intermédiaire avec ses appels d'outils, il est publié comme event SSE `step` et
rendu comme une **sous-section repliable** à l'intérieur du bloc d'étapes —
le chevron passe de ▶ à ▼ à l'ouverture et le texte (jusqu'à 1 200 caractères)
s'affiche en dessous, en retrait sur un filet vertical.
- [x] **G4.** Nouveaux outils principaux côté **web** (`backend/tools/web.py`,
risques READ, scope IN_APP, SSRF-guard + limite de taille) :
`web_search` (SearXNG auto-hébergé, configurable via `OBSIGATE_SEARXNG_URL`)
et `fetch_url` (lecture d'une page publique, HTML → texte).
Steps affichés : « Recherche sur le web : … » / « Page web consultée : … ».
- [x] **G5.** **Sources web** : le backend enrichit l'événement SSE `tool` de
`sources [{title, url}]` (`_tool_sources` : résultats `web_search` plafonnés à 8,
page `fetch_url`) et le frontend affiche une sous-section « Sources (N) » **ouverte
par défaut** avec les liens cliquables (`target="_blank"`, `rel="noopener
noreferrer"`). Aucun résultat → pas de section.
- [x] **G6.** **En-tête d'étapes** : libellé « N étapes » suivi du chevron
▶ / ▼ (fin du préfixe « > »), et **indicateur animé** (trois points en
pulsation décalée, pur CSS, respecte `prefers-reduced-motion`) devant le libellé
pendant l'exécution. La **barre de chargement** au-dessus de la zone de saisie est
supprimée ; en chat simple, l'indicateur s'affiche dans la bulle de réponse en
attente. Les états d'ouverture (étapes, réflexion, sources) sont mémorisés sur le
message : le re-rendu d'un token ne referme jamais ce que l'utilisateur a ouvert.
- [x] **G7.** `web_search` avertit explicitement le modèle quand l'instance SearXNG
ne remonte aucun résultat (moteurs amont suspendus/CAPTCHA) : `warning` +
`unresponsive_engines` dans le résultat — sans ce signal, l'assistant relançait la
même recherche jusqu'au quota d'outils.
- [x] **G8.** **Chaîne de repli web (BUG-051)** : `web_search` interroge successivement
SearXNG, puis DuckDuckGo (HTML sans JS) puis Bing (HTML), et retient le premier
fournisseur non vide (`provider`) ; les replis se désactivent via
`OBSIGATE_WEB_FALLBACK=0`. Évite que l'assistant conclue « pas d'accès à internet »
quand l'instance SearXNG est bloquée par ses moteurs amont.
### Outils restants — documentés pour le futur (hors #91)
La catégorie Notion « étapes » peut s'étendre ; chaque futur outil devra être un
tool du registre (`@tool`) + un libellé dans `labels.py` + deux clés i18n.
Priorités proposées (à transformer en items `#NN` quand implémentés) :
- **Créer/supprimer un fichier depuis une réponse** : déjà couvert par les cartes
d'action `create_file` / l'outil `create_file` — exposer une action « Créer la
note » dans la barre d'actions (post-traitement du texte copié).
- **Insérer dans le document courant** : l'équivalent « Ajouter » côté assistant
est livré (G) ; l'export vers une sélection précise de l'éditeur reste possible.
- **Convertir un format / tableur / document** : outils `convert_format`,
`create_spreadsheet` (csv/xlsx via backend) — risque WRITE (confirmation).
- **Calendrier / tâches** : intégrer l'API existante `n8n-automation` ou un
serveur MCP externe (la porte MCP est déjà ouverte côté ObsiGate).
- **Courriel / messagerie** : via le serveur MCP externe (Gmail/IMPT SMTP) —
jamais de clé en dur : passer par Infisical.
- **Sources connectées (Slack, GitHub, Drive)** : uniquement par **MCP externe**
(`docs/features/ai-tools-mcp.md`), pas d'outils natifs — l'attaque surface
reste dans le registre + rate-limit + audit.