diff --git a/CHANGELOG.md b/CHANGELOG.md index 1851aa6..5fd4d5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ > propose des **skills** de travail réutilisables en plus des commandes admin existantes. ### Sous chaque réponse de l'agent -- **⧉ copy response** : copie la réponse dans le presse-papiers. -- **➕ insert into this page** : insère la réponse dans la page ouverte (désactivé sans page éditable). -- **👍 / 👎 Share feedback** : enregistre le retour en base (`agent_feedback`) avec confirmation. +- **⧉ / ➕ / 👍 / 👎** : boutons **à icônes seules** (infobulle au survol) — copier la réponse, + insérer dans la page ouverte, feedback 👍/👎 enregistré en base. +- Le bandeau statique « Contexte : … » du haut du panneau est supprimé. ### Contexte épinglé — commande « @ » / bouton « + » (style Notion AI) - Le composeur est maintenant un éditeur **contenteditable** : les références sélectionnées @@ -20,6 +20,8 @@ - **@** ouvre le sélecteur (icône + titre + chemin) listant documents, pages et bases ; l'élément choisi devient un jeton inline **et** une puce de contexte. **+** épingle un élément au contexte sans l'insérer inline. Les puces persistent jusqu'au retrait (et sont vidées en nouveau chat). +- **Contexte d'ouverture** : ouvrir l'agent sur un document (`/pages/:id`) ou une base (`/db/:id`) + ajoute automatiquement ce contexte comme **mention** (puce épinglée) — plus de bandeau statique. - À l'envoi, chaque référence est transmise comme **mention** ; le backend résout son **contenu réel**. ### Skills — commande « / » diff --git a/app/templates/agent_panel.html b/app/templates/agent_panel.html index 33b8cb9..1c1fb47 100644 --- a/app/templates/agent_panel.html +++ b/app/templates/agent_panel.html @@ -105,7 +105,7 @@ /* Action bar under each assistant post */ .fd-agent-msg-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px;align-items:center} -.fd-act-btn{display:inline-flex;align-items:center;gap:4px;background:transparent;border:1px solid var(--border,rgba(255,255,255,.09));color:var(--text-dim,#9aa);border-radius:7px;padding:3px 8px;font-size:11px;cursor:pointer;transition:background .12s,color .12s,border-color .12s;line-height:1.5} +.fd-act-btn{display:inline-flex;align-items:center;justify-content:center;gap:0;background:transparent;border:1px solid var(--border,rgba(255,255,255,.09));color:var(--text-dim,#9aa);border-radius:7px;width:30px;height:26px;font-size:13px;cursor:pointer;transition:background .12s,color .12s,border-color .12s;line-height:1} .fd-act-btn:hover{background:var(--bg-hover,#262626);color:var(--text,#fff);border-color:rgba(255,255,255,.25)} .fd-act-btn.on{background:rgba(35,131,226,.18);border-color:rgba(35,131,226,.55);color:#fff} .fd-act-btn:disabled,.fd-act-btn.disabled{opacity:.4;cursor:not-allowed} @@ -179,11 +179,6 @@ -
- - -
-
⚠️ Aucun fournisseur activé et fonctionnel. Configurer → Agent & IA @@ -243,10 +238,10 @@
@@ -493,32 +488,43 @@ }, toggle(){ this.open ? this.close() : this.openAgent(); }, - // ── Context (pinned at open, refreshed when a new document is open) ── + // ── Context (reflected as an @-style mention chip in the composer) ── refreshContext(force){ var ctx = FlowDeckCtx.current(); if(!ctx){ this.activeContext = null; return; } - if(!this.activeContext) { this.activeContext = ctx; return; } + if(!this.activeContext) { this.activeContext = ctx; this._syncContextChip(); return; } // Always follow a currently-open document so follow-up messages use it. if(ctx.kind === 'page' || force || ctx.key !== this.activeContext.key){ this.activeContext = ctx; + this._syncContextChip(); } }, + _syncContextChip(){ + var c = this.activeContext; + if(!c || !c.mentionToken) return; + var exists = this.contextChips.some(function(ch){ return ch.token === c.mentionToken; }); + if(exists) return; + this.contextChips.push({ + key: c.mentionToken, kind: 'mention', type: c.refType || 'document', + icon: c.icon || '📎', label: c.title || c.label, token: c.mentionToken, refId: c.refId + }); + }, get chatContextActive(){ return !!(this.activeContext && this.activeContext.kind === 'page'); }, contextPayload(){ this.refreshContext(); var c = this.activeContext; if(!c) return ''; + // Si le contexte est déjà épinglé comme mention (@), le backend résout son + // contenu via context_builder — on évite de dupliquer le texte du document. + if(c.mentionToken && this.contextChips.some(function(ch){ return ch.token === c.mentionToken; })) return ''; if(c.text && c.text.trim()) return c.text.trim().slice(0, 9000); - return c.label ? ('Contexte : ' + c.label + '.').slice(0, 2000) : ''; + return c.title ? ('Contexte : ' + c.title + '.').slice(0, 2000) : ''; }, get composerHint(){ - var c = this.activeContext; - var ctx; - if(!c) ctx = 'Demandez, tapez / pour un skill, @ pour référencer un élément'; - else if(c.kind === 'page') ctx = 'Contexte : le document ouvert est pris en compte par l\u2019agent.'; - else ctx = 'Contexte : ' + c.label; - var extra = this.contextChips.length ? ' · ' + this.contextChips.length + ' réf. épinglée(s)' : ''; - return ctx + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne'; + var chips = this.contextChips.length; + var base = 'Demandez, tapez / pour un skill, @ pour référencer un élément'; + var extra = chips ? ' · ' + chips + ' réf. épinglée(s)' : ''; + return base + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne'; }, get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); }, @@ -1515,6 +1521,12 @@ // data-fd-context), (2) the currently open document (page editor), // (3) the route the user is on. Every screen of the app yields a context. var FlowDeckCtx = (function(){ + function idFromPath(prefix){ + try{ + var m = (window.location.pathname || '').match(new RegExp('^\\/?' + prefix + '\\/(\\d+)')); + return m ? m[1] : null; + }catch(e){ return null; } + } function labelFromTitle(){ try{ var t = document.title || ''; @@ -1531,7 +1543,14 @@ if(!raw) return null; var firstLine = raw.split('\n')[0] || 'Document'; var title = firstLine.replace(/^Titre du document\s*:\s*/i, '').trim() || 'Document en cours'; - return {kind:'page', icon:'📄', label:'Document « ' + title + ' »', text: raw, key:'page'}; + var pid = idFromPath('pages'); + return { + kind:'page', icon:'📄', label:'Document « ' + title + ' »', text: raw, key:'page', + title: title, + mentionToken: pid ? ('document:' + pid) : null, + refId: pid ? Number(pid) : null, + refType: pid ? 'document' : null + }; } }catch(e){} return null; @@ -1558,7 +1577,11 @@ var kind = 'section'; var t; if(p === '/' || p === '/dashboard'){ icon='🏠'; label='Accueil'; kind='home'; } - else if(/^\/pages\/\d+/.test(p)){ icon='📄'; label=labelFromTitle() || 'Page en cours'; kind='page'; } + else if(/^\/pages\/\d+/.test(p)){ + icon='📄'; kind='page'; label=labelFromTitle() || 'Page en cours'; + var pid = idFromPath('pages'); + t = pid ? ('document:' + pid) : null; + } else if(/^\/library/.test(p)){ icon='📚'; kind='library'; var tab = (window.location.search.match(/tab=([^&]+)/)||[])[1] || ''; @@ -1570,7 +1593,12 @@ else if(/^\/local-workspace/.test(p)){ icon='🗂️'; label='Espace local'; kind='workspace'; } else if(/^\/workspace($|\?)/.test(p)){ icon='🗂️'; label='Espace de travail'; kind='workspace'; } else if(/^\/gitea-workspace/.test(p)){ icon='🔗'; label='Workspace Gitea'; kind='workspace'; } - else if(/^\/db\//.test(p)){ icon='🗄️'; label='Base de données'; kind='db'; t = labelFromTitle(); if(t) label = 'Base — ' + t; } + else if(/^\/db\//.test(p)){ + icon='🗄️'; kind='db'; + label = labelFromTitle() || 'Base de données'; + var cid = idFromPath('db'); + t = cid ? ('collection:' + cid) : null; + } else if(/^\/board\//.test(p)){ icon='📋'; kind='board'; if(/^\/board\/library/.test(p)){ icon='📚'; label='Bibliothèque'; kind='library'; } @@ -1585,7 +1613,11 @@ label = labelFromTitle() || 'FlowDeck'; kind='section'; } - return {kind: kind, icon: icon, label: label, text: '', key: p}; + var mentionToken = (kind === 'page' && pid) ? ('document:' + pid) : ((kind === 'db' && cid) ? ('collection:' + cid) : null); + return {kind: kind, icon: icon, label: label, text: '', key: p, + title: label, mentionToken: mentionToken, + refId: mentionToken ? Number(String(mentionToken).split(':')[1]) : null, + refType: mentionToken ? String(mentionToken).split(':')[0] : null}; } return { current: function(){