fix(agent): boutons d'action a icones seules + contexte d'ouverture en puce de mention (@) au lieu du bandeau statique
FlowDeck CI / test (push) Failing after 11s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
2026-09-06 12:53:16 -04:00
parent 27c9eb98db
commit 571d78115b
2 changed files with 61 additions and 27 deletions
+5 -3
View File
@@ -8,9 +8,9 @@
> propose des **skills** de travail réutilisables en plus des commandes admin existantes. > propose des **skills** de travail réutilisables en plus des commandes admin existantes.
### Sous chaque réponse de l'agent ### Sous chaque réponse de l'agent
- **⧉ copy response** : copie la réponse dans le presse-papiers. - **⧉ / ➕ / 👍 / 👎** : boutons **à icônes seules** (infobulle au survol) — copier la réponse,
- **➕ insert into this page** : insère la réponse dans la page ouverte (désactivé sans page éditable). insérer dans la page ouverte, feedback 👍/👎 enregistré en base.
- **👍 / 👎 Share feedback** : enregistre le retour en base (`agent_feedback`) avec confirmation. - Le bandeau statique « Contexte : … » du haut du panneau est supprimé.
### Contexte épinglé — commande « @ » / bouton « + » (style Notion AI) ### Contexte épinglé — commande « @ » / bouton « + » (style Notion AI)
- Le composeur est maintenant un éditeur **contenteditable** : les références sélectionnées - 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 - **@** 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 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). 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**. - À l'envoi, chaque référence est transmise comme **mention** ; le backend résout son **contenu réel**.
### Skills — commande « / » ### Skills — commande « / »
+56 -24
View File
@@ -105,7 +105,7 @@
/* Action bar under each assistant post */ /* Action bar under each assistant post */
.fd-agent-msg-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px;align-items:center} .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: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.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} .fd-act-btn:disabled,.fd-act-btn.disabled{opacity:.4;cursor:not-allowed}
@@ -179,11 +179,6 @@
<button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button> <button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button>
</div> </div>
<div class="fd-ap-ctx" x-show="activeContext" x-cloak>
<span class="fd-ctx-ico" x-text="activeContext ? activeContext.icon : ''"></span>
<span class="fd-ctx-lbl" x-text="activeContext ? ('Contexte : ' + activeContext.label) : ''"></span>
</div>
<div class="fd-ap-noconfig" x-show="!hasActiveProvider"> <div class="fd-ap-noconfig" x-show="!hasActiveProvider">
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span> <span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
<a href="/settings" target="_blank" @click.stop>Configurer → Agent &amp; IA</a> <a href="/settings" target="_blank" @click.stop>Configurer → Agent &amp; IA</a>
@@ -243,10 +238,10 @@
</template> </template>
<template x-if="m.role==='assistant' && !m.generating && m.content && !m.act"> <template x-if="m.role==='assistant' && !m.generating && m.content && !m.act">
<div class="fd-agent-msg-actions"> <div class="fd-agent-msg-actions">
<button class="fd-act-btn" type="button" @click="copyResponse(m)" title="Copier la réponse">⧉ copy response</button> <button class="fd-act-btn" type="button" @click="copyResponse(m)" title="Copier la réponse">⧉</button>
<button class="fd-act-btn" type="button" @click="insertIntoPage(m)" :disabled="!canInsert" title="Insérer dans la page ouverte">➕ insert into this page</button> <button class="fd-act-btn" type="button" @click="insertIntoPage(m)" :disabled="!canInsert" title="Insérer dans la page ouverte">➕</button>
<button class="fd-act-btn" :class="{on: m.fb==='up'}" type="button" @click="feedback(m,'up')" :disabled="!!m.fb && m.fb!=='up'">👍 Share feedback</button> <button class="fd-act-btn" :class="{on: m.fb==='up'}" type="button" @click="feedback(m,'up')" :disabled="!!m.fb && m.fb!=='up'" title="Feedback positif">👍</button>
<button class="fd-act-btn" :class="{on: m.fb==='down'}" type="button" @click="feedback(m,'down')" :disabled="!!m.fb && m.fb!=='down'">👎 Share feedback</button> <button class="fd-act-btn" :class="{on: m.fb==='down'}" type="button" @click="feedback(m,'down')" :disabled="!!m.fb && m.fb!=='down'" title="Feedback négatif">👎</button>
</div> </div>
</template> </template>
</div> </div>
@@ -493,32 +488,43 @@
}, },
toggle(){ this.open ? this.close() : this.openAgent(); }, 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){ refreshContext(force){
var ctx = FlowDeckCtx.current(); var ctx = FlowDeckCtx.current();
if(!ctx){ this.activeContext = null; return; } 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. // Always follow a currently-open document so follow-up messages use it.
if(ctx.kind === 'page' || force || ctx.key !== this.activeContext.key){ if(ctx.kind === 'page' || force || ctx.key !== this.activeContext.key){
this.activeContext = ctx; 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'); }, get chatContextActive(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
contextPayload(){ contextPayload(){
this.refreshContext(); this.refreshContext();
var c = this.activeContext; var c = this.activeContext;
if(!c) return ''; 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); 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(){ get composerHint(){
var c = this.activeContext; var chips = this.contextChips.length;
var ctx; var base = 'Demandez, tapez / pour un skill, @ pour référencer un élément';
if(!c) ctx = 'Demandez, tapez / pour un skill, @ pour référencer un élément'; var extra = chips ? ' · ' + chips + ' réf. épinglée(s)' : '';
else if(c.kind === 'page') ctx = 'Contexte : le document ouvert est pris en compte par l\u2019agent.'; return base + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne';
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';
}, },
get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); }, get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
@@ -1515,6 +1521,12 @@
// data-fd-context), (2) the currently open document (page editor), // 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. // (3) the route the user is on. Every screen of the app yields a context.
var FlowDeckCtx = (function(){ 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(){ function labelFromTitle(){
try{ try{
var t = document.title || ''; var t = document.title || '';
@@ -1531,7 +1543,14 @@
if(!raw) return null; if(!raw) return null;
var firstLine = raw.split('\n')[0] || 'Document'; var firstLine = raw.split('\n')[0] || 'Document';
var title = firstLine.replace(/^Titre du document\s*:\s*/i, '').trim() || 'Document en cours'; 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){} }catch(e){}
return null; return null;
@@ -1558,7 +1577,11 @@
var kind = 'section'; var kind = 'section';
var t; var t;
if(p === '/' || p === '/dashboard'){ icon='🏠'; label='Accueil'; kind='home'; } 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)){ else if(/^\/library/.test(p)){
icon='📚'; kind='library'; icon='📚'; kind='library';
var tab = (window.location.search.match(/tab=([^&]+)/)||[])[1] || ''; 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(/^\/local-workspace/.test(p)){ icon='🗂️'; label='Espace local'; kind='workspace'; }
else if(/^\/workspace($|\?)/.test(p)){ icon='🗂️'; label='Espace de travail'; 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(/^\/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)){ else if(/^\/board\//.test(p)){
icon='📋'; kind='board'; icon='📋'; kind='board';
if(/^\/board\/library/.test(p)){ icon='📚'; label='Bibliothèque'; kind='library'; } if(/^\/board\/library/.test(p)){ icon='📚'; label='Bibliothèque'; kind='library'; }
@@ -1585,7 +1613,11 @@
label = labelFromTitle() || 'FlowDeck'; label = labelFromTitle() || 'FlowDeck';
kind='section'; 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 { return {
current: function(){ current: function(){