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.
### 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 « / »
+56 -24
View File
@@ -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 @@
<button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button>
</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">
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
<a href="/settings" target="_blank" @click.stop>Configurer → Agent &amp; IA</a>
@@ -243,10 +238,10 @@
</template>
<template x-if="m.role==='assistant' && !m.generating && m.content && !m.act">
<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="insertIntoPage(m)" :disabled="!canInsert" title="Insérer dans la page ouverte">➕ insert into this page</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==='down'}" type="button" @click="feedback(m,'down')" :disabled="!!m.fb && m.fb!=='down'">👎 Share feedback</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">➕</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'" title="Feedback négatif">👎</button>
</div>
</template>
</div>
@@ -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(){