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

11 KiB

#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 · BooksLM #76 · Assistant UX #80 · Changelog

  • 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é

  • 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.
  • 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).
  • 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.
  • 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é.
  • 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.
  • 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é

  • B1. Ordre naturel (user puis assistant, empilés, gap: 24px).
  • B2. Utilisateur : bulle --surface2 alignée à droite, max-width: 80%, coins arrondis 18px.
  • B3. Assistant : width: 100%, pas de fond ni de padding de bulle — le markdown occupe toute la largeur (titres, listes, code, sources inchangés).
  • 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é

  • C1. _streamResponse() capture data.provider / data.model du flux SSE (déjà émis par /chat et /agent) sur le message assistant.
  • C2. .bookslm-msg-meta au-dessus du bloc assistant : « fournisseur · modèle » (persisté avec la session, visible au rechargement).
  • 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é

  • D1. _appendActionBar() sous chaque bloc non vide : bouton Copier pour les deux rôles ; bouton Ajouter pour l'assistant uniquement.
  • D2. Copier : texte brut du message (navigator.clipboard, repli execCommand) + toast bookslm.copied.
  • 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.
  • D4. Révélation au survol / focus clavier (.bookslm-msg:hover, :focus-within) — icônes Lucide copy / corner-down-left.

E. Tests & documentation — ✅ livré

  • 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.
  • E2. i18n FR/EN : bookslm.copied, bookslm.insert, bookslm.insert_hint, bookslm.inserted, bookslm.insert_no_editor, ai.steps_count.
  • 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é

  • 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é).
  • 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).
  • 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.
  • 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 : … ».
  • 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.
  • 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.
  • 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.
  • 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.