11 KiB
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 :- 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'astuceorder: -1. - 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>). - Fournisseur & modèle discrets : libellé au-dessus de chaque réponse,
issu du SSE réel du backend (
provider/model). - 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).
- Ancrage en haut : après l'envoi, le post de l'utilisateur est amené tout en
haut de la zone visible (
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,scrollIntoViewblock: 'start'(instantané à l'envoi, fluide à l'ouverture d'une session), repli calculscrollTop += deltasi indisponible. - A2. Re-rendus de streaming : tant que l'ancre est active (flag
_pinnedTurnposé à l'envoi, libéré surwheel/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 descrollTop = 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-bottomdu 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: nonesur.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èsscrollIntoView: 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
--surface2aligné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 » (i18nai.steps_count) ; les lignes d'outils restent accessibles en déroulant le bloc.
C. Fournisseur & modèle discrets — ✅ livré
- C1.
_streamResponse()capturedata.provider/data.modeldu flux SSE (déjà émis par/chatet/agent) sur le message assistant. - C2.
.bookslm-msg-metaau-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 (/chatet/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, repliexecCommand) + toastbookslm.copied. - D3. Ajouter (
_insertIntoEditor) : insertion du texte à la position de fin de sélection dansstate.editorView(Forge) ; toastbookslm.insertedoubookslm.insert_no_editorsi aucun éditeur ouvert. - D4. Révélation au survol / focus clavier (
.bookslm-msg:hover,:focus-within) — icônes Lucidecopy/corner-down-left.
E. Tests & documentation — ✅ livré
- E1.
tests/frontend/ai.test.mjs: ordre chronologique, ancragescrollIntoView block:start, tag provider/modèle, barre d'actions par rôle, copie du texte brut, insertion éditeur (mockstate.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_VERSIONbump (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 SSEtoolavecstep {key, params}; le frontend résoutai.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.Queuepar 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
stepet 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 viaOBSIGATE_SEARXNG_URL) etfetch_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
tooldesources [{title, url}](_tool_sources: résultatsweb_searchplafonnés à 8, pagefetch_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_searchavertit explicitement le modèle quand l'instance SearXNG ne remonte aucun résultat (moteurs amont suspendus/CAPTCHA) :warning+unresponsive_enginesdans 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_searchinterroge successivement SearXNG, puis DuckDuckGo (HTML sans JS) puis Bing (HTML), et retient le premier fournisseur non vide (provider) ; les replis se désactivent viaOBSIGATE_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'outilcreate_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-automationou 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.