feat(agent): v4.15.0 - panneau Agent style Notion AI (mentions @ inline + skills / + actions sous chaque réponse)
- Composeur contenteditable : mentions inline (pastilles icône+nom), barre de contexte groupée documents/skills, sélecteur @ (icône+titre+chemin), skills intégrés + agent_skills + commandes admin via '/' - Actions sous chaque réponse agent : copier, insérer dans la page, feedback 👍/👎 - Backend : table agent_feedback, GET /api/agent/mentions, POST /api/agent/feedback, contexte document:<id>/page:<id>/collection:<id> résolu en contenu réel - Inclut le travail v4.14.0 (documents/espace de travail + outils + auto-titre de conversation)
This commit is contained in:
+719
-49
@@ -69,8 +69,12 @@
|
||||
.fd-ap-composer{padding:10px 14px 14px;border-top:1px solid var(--border,rgba(255,255,255,.09));position:relative}
|
||||
.fd-ap-box{background:var(--bg-tertiary,#1f1f1f);border:1px solid var(--border,rgba(255,255,255,.13));border-radius:16px;padding:10px 12px 8px;transition:border-color .12s,box-shadow .12s}
|
||||
.fd-ap-box:focus-within{border-color:rgba(35,131,226,.65);box-shadow:0 0 0 3px rgba(35,131,226,.12)}
|
||||
.fd-ap-input{width:100%;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:14px;resize:none;font-family:inherit;line-height:1.5;max-height:220px;box-sizing:border-box}
|
||||
.fd-ap-input::placeholder{color:var(--text-dim,#8a8a8a)}
|
||||
.fd-ap-input{width:100%;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:14px;resize:none;font-family:inherit;line-height:1.6;max-height:220px;box-sizing:border-box;overflow-y:auto;white-space:pre-wrap;word-break:break-word;min-height:26px;position:relative}
|
||||
.fd-ap-input.empty::before{content:attr(data-placeholder);color:var(--text-dim,#8a8a8a);position:absolute;left:0;top:0;pointer-events:none;white-space:pre-wrap;max-width:100%}
|
||||
.ment-token{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;background:rgba(35,131,226,.14);border:1px solid rgba(35,131,226,.35);color:var(--text,#eaf2ff);border-radius:6px;padding:0 3px 0 4px;font-size:13px;line-height:1.5;margin:0 1px;max-width:200px;overflow:hidden;white-space:nowrap}
|
||||
.ment-token .tk-lbl{overflow:hidden;text-overflow:ellipsis}
|
||||
.ment-token.skill{background:rgba(168,85,247,.16);border-color:rgba(168,85,247,.5);color:#f3e8ff}
|
||||
.ment-token .tk-ico{flex-shrink:0;opacity:.9}
|
||||
.fd-ap-toolbar{display:flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap}
|
||||
.fd-ap-tip{font-size:11px;color:var(--text-dim,#8f8f8f);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-ap-tools{display:flex;align-items:center;gap:6px;margin-left:auto}
|
||||
@@ -94,7 +98,48 @@
|
||||
.fd-agent-cmd-item{padding:8px 12px;font-size:12px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:3px;display:flex;gap:8px;align-items:baseline}
|
||||
.fd-agent-cmd-item:hover{background:var(--bg-hover,#2b2b2b)}
|
||||
.fd-agent-cmd-item b{color:var(--accent,#2383E2);font-family:monospace;white-space:nowrap}
|
||||
.fd-agent-cmd-item b.k-cmd{color:var(--accent,#2383E2)}
|
||||
.fd-agent-cmd-item b.k-skill{color:#c084fc}
|
||||
.fd-agent-cmd-item span{color:var(--text-dim,#999);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fd-agent-cmd-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px}
|
||||
|
||||
/* 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: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}
|
||||
|
||||
/* Chips (contexte épinglé @ / + / skills) */
|
||||
.fd-ap-chips{display:flex;flex-direction:column;gap:5px;padding:8px 4px 4px;max-height:92px;overflow-y:auto}
|
||||
.fd-chip-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||||
.fd-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(35,131,226,.10);border:1px solid rgba(35,131,226,.30);color:var(--text,#e8e8e8);border-radius:99px;padding:3px 5px 3px 9px;font-size:11.5px;max-width:220px}
|
||||
.fd-chip.skill{background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.38)}
|
||||
.fd-chip-ico{flex-shrink:0;font-size:12px}
|
||||
.fd-chip-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-chip-spacer{flex-basis:100%;height:0}
|
||||
.fd-chip-x{background:none;border:none;color:var(--text-dim,#aaa);cursor:pointer;font-size:13px;line-height:1;padding:0 4px;border-radius:50%;flex-shrink:0}
|
||||
.fd-chip-x:hover{color:#fff;background:rgba(255,255,255,.14)}
|
||||
|
||||
/* Add button (+ contexte) */
|
||||
.fd-ap-add{width:30px;height:30px;border-radius:50%;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.14));color:var(--text,#ddd);cursor:pointer;font-size:17px;display:flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1}
|
||||
.fd-ap-add:hover{background:var(--bg-hover,#303030);color:#fff}
|
||||
|
||||
/* Mention menu (@ / +) */
|
||||
.fd-ap-mention-menu{position:absolute;bottom:calc(100% + 4px);left:0;right:0;background:var(--bg-modal,#1e1e1e);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:11px;max-height:220px;overflow-y:auto;box-shadow:0 -8px 26px rgba(0,0,0,.4);z-index:70;display:none}
|
||||
.fd-ap-mention-menu.show{display:block}
|
||||
.fd-mention-item{display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:2px 4px}
|
||||
.fd-mention-item:hover,.fd-mention-item.focus{background:var(--bg-hover,#2b2b2b)}
|
||||
.fd-mention-ico{width:24px;text-align:center;flex-shrink:0;font-size:14px}
|
||||
.fd-mention-main{flex:1;min-width:0;display:flex;flex-direction:column}
|
||||
.fd-mention-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-mention-sub{font-size:10.5px;color:var(--text-dim,#999);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-mention-empty,.fd-mention-load{padding:11px 12px;font-size:12px;color:var(--text-dim,#aaa);text-align:center}
|
||||
|
||||
/* Toast */
|
||||
.fd-ap-toast{position:absolute;left:14px;right:14px;bottom:calc(100% + 10px);background:var(--bg-modal,#1c1c1c);border:1px solid rgba(35,131,226,.5);color:var(--text,#fff);font-size:12px;border-radius:9px;padding:8px 12px;box-shadow:0 10px 30px rgba(0,0,0,.45);z-index:90;display:flex;gap:8px;align-items:center;justify-content:space-between}
|
||||
.fd-ap-toast.err{border-color:rgba(227,98,98,.55)}
|
||||
.fd-ap-toast button{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:13px;padding:0}
|
||||
|
||||
[x-cloak]{display:none!important}
|
||||
</style>
|
||||
@@ -196,6 +241,14 @@
|
||||
<span class="fd-proposal-note" x-show="m.offline" x-cloak>Hors-ligne — configurez une clé API pour appliquer la modification.</span>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -207,26 +260,69 @@
|
||||
|
||||
<div class="fd-ap-composer">
|
||||
<div style="position:relative;">
|
||||
<div class="fd-agent-cmd-menu" :class="{show: commandOpen && commandSuggestions.length}">
|
||||
<template x-for="c in commandSuggestions" :key="c.cmd">
|
||||
<div class="fd-agent-cmd-item" @click="useSuggestion(c)">
|
||||
<b x-text="c.cmd"></b><span x-text="c.desc"></span>
|
||||
<div class="fd-ap-toast" x-show="toastMsg" x-cloak :class="{err: toastErr}">
|
||||
<span x-text="toastMsg"></span>
|
||||
<button type="button" @click="toastMsg=''">✕</button>
|
||||
</div>
|
||||
<div class="fd-agent-cmd-menu" :class="{show: commandOpen && commandSuggestions.length && !slashDismiss}">
|
||||
<template x-for="c in commandSuggestions" :key="c.key">
|
||||
<div>
|
||||
<div class="fd-agent-cmd-sep" x-show="c._sep" x-text="c._sep"></div>
|
||||
<div class="fd-agent-cmd-item" x-show="!c._sep" :class="{focus: c._i===cmdFocus}" @click="useSuggestion(c)" @mouseenter="cmdFocus=c._i">
|
||||
<b :class="c._admin ? 'k-cmd' : 'k-skill'" x-text="(c._admin ? c.cmd : c.cmd)"></b>
|
||||
<span x-text="c.desc"></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="fd-ap-mention-menu" :class="{show: mentionOpen}">
|
||||
<div class="fd-mention-load" x-show="mentionLoad" x-cloak>Recherche…</div>
|
||||
<template x-if="!mentionLoad && !mentionItems.length">
|
||||
<div class="fd-mention-empty">Aucun élément. Tapez un nom de document, page ou base.</div>
|
||||
</template>
|
||||
<template x-if="!mentionLoad" x-for="it in mentionItems" :key="it.token">
|
||||
<div class="fd-mention-item" :class="{focus: it._i===mentionFocus}" @click="pickMention(it)" @mouseenter="mentionFocus=it._i">
|
||||
<span class="fd-mention-ico" x-text="it.icon"></span>
|
||||
<div class="fd-mention-main">
|
||||
<div class="fd-mention-lbl" x-text="it.label"></div>
|
||||
<div class="fd-mention-sub" x-text="it.sub"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-ap-chips" x-show="contextChips.length" x-cloak>
|
||||
<div class="fd-chip-row" x-show="mentionChips.length">
|
||||
<template x-for="chip in mentionChips" :key="chip.key">
|
||||
<span class="fd-chip">
|
||||
<span class="fd-chip-ico" x-text="chip.icon"></span>
|
||||
<span class="fd-chip-lbl" x-text="chip.label"></span>
|
||||
<button class="fd-chip-x" type="button" @click="removeChip(chip.token)" title="Retirer du contexte">✕</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="fd-chip-row" x-show="skillChips.length">
|
||||
<template x-for="chip in skillChips" :key="chip.key">
|
||||
<span class="fd-chip skill">
|
||||
<span class="fd-chip-ico" x-text="chip.icon"></span>
|
||||
<span class="fd-chip-lbl" x-text="chip.label"></span>
|
||||
<button class="fd-chip-x" type="button" @click="removeChip(chip.token)" title="Retirer le skill">✕</button>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-ap-box">
|
||||
<textarea class="fd-ap-input" x-ref="cmdInput" rows="1"
|
||||
placeholder="Demandez à l'agent (ex : crée une collection Projets)… Tapez / pour les commandes"
|
||||
x-model="input" @input="resizeInput()" @keydown.enter.exact.prevent="send()"
|
||||
@keydown.enter.shift.prevent="insertNewline()"></textarea>
|
||||
<div class="fd-ap-input empty" x-ref="cmdInput" contenteditable="true" spellcheck="false" role="textbox" aria-multiline="true"
|
||||
data-placeholder="Demandez à l'agent… Tapez @ pour référencer une page/base, / pour un skill. Entrée = envoyer, Maj+Entrée = ligne"
|
||||
@input="onInput()" @keydown="onComposerKeydown($event)" @paste="onPaste($event)"></div>
|
||||
<div class="fd-ap-toolbar">
|
||||
<button class="fd-ap-add" type="button" title="Ajouter un élément au contexte (@ / +)" @click="toggleAddPicker()">+</button>
|
||||
<span class="fd-ap-tip" x-text="composerHint"></span>
|
||||
<div class="fd-ap-tools">
|
||||
<button class="fd-ap-model" :class="{off: !hasActiveProvider}" type="button" @click="modelOpen=!modelOpen" title="Choisir fournisseur et modèle">
|
||||
<span x-text="autoLabel"></span> <span style="opacity:.6">▾</span>
|
||||
</button>
|
||||
<button class="fd-ap-send" @click="send()" :disabled="streaming || !input.trim()" title="Envoyer">▲</button>
|
||||
<button class="fd-ap-send" @click="send()" :disabled="streaming || !canSend" title="Envoyer">▲</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,6 +347,39 @@
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// ── Skills intégrés (workflows « / ») et commandes admin du panneau ──
|
||||
var FD_ADMIN_CMDS = [
|
||||
{cmd:'/help', desc:'liste des commandes'},
|
||||
{cmd:'/providers', desc:'liste les fournisseurs actifs'},
|
||||
{cmd:'/provider <nom>', desc:'changer de fournisseur LLM'},
|
||||
{cmd:'/model <nom>', desc:'changer de modèle LLM'},
|
||||
{cmd:'/keys', desc:'fournisseurs avec clé API enregistrée'}
|
||||
];
|
||||
var FD_SKILLS = [
|
||||
{cmd:'/research', icon:'🔎', desc:'recherche structurée + recommandation',
|
||||
template:'Fais un état des lieux de [sujet] : contexte, avantages/inconvénients, options, puis une recommandation finale. Indique tes sources si tu en utilises.'},
|
||||
{cmd:'/create-new-skill', icon:'🛠️', desc:'crée un workflow réutilisable (skill)',
|
||||
template:'Je veux créer un skill réutilisable qui [décris le workflow]. Produis : nom, description courte, étapes et format de sortie attendu.'},
|
||||
{cmd:'/resume', icon:'📄', desc:'résumé / synthèse structurée',
|
||||
template:'Résume ce contenu de façon structurée et concise : points clés, décisions, TL;DR.'},
|
||||
{cmd:'/actions', icon:'✅', desc:'extraire les actions & to-dos',
|
||||
template:'À partir de ce contenu, liste les actions à mener avec responsable et échéance quand c\u2019est indiqué.'},
|
||||
{cmd:'/reformuler', icon:'✍️', desc:'réécriture clarté / ton',
|
||||
template:'Reformule ce contenu pour le rendre plus clair et plus concis en conservant le sens. Précise le ton souhaité (interne, client, formel…).'},
|
||||
{cmd:'/correction', icon:'🔤', desc:'correction grammaire / orthographe / style',
|
||||
template:'Corrige la grammaire, l\u2019orthographe et le style de ce contenu sans en changer le sens.'},
|
||||
{cmd:'/brainstorm', icon:'💡', desc:'générer des idées, angles, variantes',
|
||||
template:'Propose des idées variées sur [sujet] : angles, exemples concrets, variantes de messages (annonce, email, post).'},
|
||||
{cmd:'/plan', icon:'🧭', desc:'planifier / structurer un document',
|
||||
template:'Construis un plan structuré pour [sujet] : sections logiques, sous-points et ordre de rédaction.'},
|
||||
{cmd:'/ask', icon:'💬', desc:'Q&A sur un contenu référencé',
|
||||
template:'À partir du contenu référencé, réponds précisément à [ta question].'},
|
||||
{cmd:'/meeting-note', icon:'📝', desc:'compte-rendu / note de réunion',
|
||||
template:'Rédige une AI Meeting Note : Résumé, Décisions, Action items (avec responsables et échéances), Prochaines étapes.'},
|
||||
{cmd:'/livrable', icon:'📨', desc:'email / compte-rendu / message Slack',
|
||||
template:'Rédige un livrable professionnel (email, compte-rendu ou message Slack) sur [sujet / destinataire / contexte].'}
|
||||
];
|
||||
|
||||
function agentPanel(){
|
||||
return {
|
||||
open: false, modelOpen: false, tab: 'chat',
|
||||
@@ -260,6 +389,12 @@
|
||||
allProviders: [], providers: [], providerModels: [], llmProvider: 'offline',
|
||||
llmModel: '', llmKeys: [], defaultProvider: 'offline',
|
||||
activeContext: null, ctxPinnedKey: null,
|
||||
contextChips: [], skills: [],
|
||||
mentionOpen: false, mentionItems: [], mentionQuery: '', mentionLoad: false,
|
||||
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
|
||||
cmdFocus: -1, slashDismiss: false,
|
||||
toastMsg: '', toastErr: false, _toastTimer: null,
|
||||
_slashSeq: 0,
|
||||
|
||||
init(){
|
||||
var self = this;
|
||||
@@ -282,6 +417,7 @@
|
||||
this._applyState = applyState;
|
||||
this.fetchProviders();
|
||||
this.fetchAgents();
|
||||
this.fetchSkills();
|
||||
|
||||
// Exposed imperative API (used by the FAB, the sidebar, Ask AI…)
|
||||
window.fdAgent.open = function(){ self.openAgent(); };
|
||||
@@ -291,7 +427,7 @@
|
||||
var t = (text || '').trim();
|
||||
if(!t) return;
|
||||
self.openAgent();
|
||||
self.input = t;
|
||||
self.setComposerText(t);
|
||||
setTimeout(function(){ self.send(); }, 60);
|
||||
};
|
||||
window.fdAgent.generate = function(message, context){
|
||||
@@ -377,10 +513,14 @@
|
||||
},
|
||||
get composerHint(){
|
||||
var c = this.activeContext;
|
||||
if(!c) return 'Demandez ou tapez / pour les commandes';
|
||||
if(c.kind === 'page') return 'Contexte : le document est pris en compte par l\u2019agent.';
|
||||
return 'Contexte : ' + c.label;
|
||||
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';
|
||||
},
|
||||
get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
|
||||
|
||||
// ── Quick actions (contextual) ──
|
||||
get quickIntro(){
|
||||
@@ -418,7 +558,7 @@
|
||||
if(q.docRun){ this.runDocAction(q); return; }
|
||||
var self = this;
|
||||
this.ensureConversation().then(function(){
|
||||
self.input = q.prompt;
|
||||
self.setComposerText(q.prompt);
|
||||
self.send();
|
||||
}).catch(function(){});
|
||||
},
|
||||
@@ -560,6 +700,12 @@
|
||||
var self = this;
|
||||
fetch('/api/agent').then(function(r){return r.json()}).then(function(d){ self.agents = d.agents || []; }).catch(function(){});
|
||||
},
|
||||
fetchSkills(){
|
||||
var self = this;
|
||||
fetch('/api/agent/skills').then(function(r){return r.json()}).then(function(d){
|
||||
self.skills = d.skills || [];
|
||||
}).catch(function(){ self.skills = []; });
|
||||
},
|
||||
loadConversations(){
|
||||
var self = this;
|
||||
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){
|
||||
@@ -593,6 +739,7 @@
|
||||
var conv = {id:d.id, title:d.title, updated_at:new Date().toISOString()};
|
||||
self.conversations.unshift(conv); self.currentConv = conv;
|
||||
self.messages = []; self.errorMsg=''; self.tab='chat';
|
||||
self.contextChips = []; self.mentionOpen = false;
|
||||
self.$nextTick(function(){ self.resizeInput(); });
|
||||
}).catch(function(){});
|
||||
},
|
||||
@@ -609,6 +756,7 @@
|
||||
var conv = {id:d.id, title:d.title, updated_at:new Date().toISOString()};
|
||||
self.conversations.unshift(conv); self.currentConv = conv;
|
||||
self.messages = []; self.errorMsg=''; self.tab='chat';
|
||||
self.contextChips = []; self.mentionOpen = false;
|
||||
self._ensuring = null;
|
||||
return conv;
|
||||
}).catch(function(err){
|
||||
@@ -629,42 +777,311 @@
|
||||
}).catch(function(){});
|
||||
},
|
||||
|
||||
// ── Input helpers ──
|
||||
// ── Composer (contenteditable) helpers ──
|
||||
_composerEl(){ return this.$refs && this.$refs.cmdInput; },
|
||||
composerText(){
|
||||
var el = this._composerEl();
|
||||
if(!el) return (this.input || '');
|
||||
var parts = [];
|
||||
var nodes = el.childNodes;
|
||||
for(var i=0;i<nodes.length;i++){
|
||||
var n = nodes[i];
|
||||
if(n.nodeType===3) parts.push(n.nodeValue);
|
||||
else if(n.nodeType===1){
|
||||
if(n.tagName === 'BR') parts.push('\n');
|
||||
else if(n.hasAttribute && n.hasAttribute('data-ctoken')) parts.push(n.getAttribute('data-label')||'');
|
||||
else parts.push(n.textContent||'');
|
||||
}
|
||||
}
|
||||
return parts.join('').replace(/\u00a0/g, ' ').replace(/[ \t]+\n/g, '\n').trim();
|
||||
},
|
||||
composerMentions(){
|
||||
var el = this._composerEl();
|
||||
var out = [], seen = {};
|
||||
if(el){
|
||||
var spans = el.querySelectorAll('[data-ctoken]');
|
||||
for(var i=0;i<spans.length;i++){
|
||||
if(spans[i].getAttribute('data-kind') === 'skill') continue;
|
||||
var t = spans[i].getAttribute('data-token');
|
||||
if(t && !seen[t]){ seen[t]=1; out.push(t); }
|
||||
}
|
||||
}
|
||||
for(i=0;i<this.contextChips.length;i++){
|
||||
var ch = this.contextChips[i];
|
||||
if(ch.kind==='mention' && !seen[ch.token]){ seen[ch.token]=1; out.push(ch.token); }
|
||||
}
|
||||
return out;
|
||||
},
|
||||
resizeInput(){
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
var t = this._composerEl();
|
||||
if(!t) return;
|
||||
t.style.height = 'auto';
|
||||
t.style.height = Math.min(t.scrollHeight, 220) + 'px';
|
||||
var h = Math.min(t.scrollHeight, 220);
|
||||
t.style.height = h + 'px';
|
||||
t.style.overflowY = (t.scrollHeight > 220) ? 'auto' : 'hidden';
|
||||
},
|
||||
_emptyCheck(){
|
||||
var el = this._composerEl();
|
||||
if(!el) return;
|
||||
var empty = !el.textContent.trim() && !el.querySelector('[data-ctoken]');
|
||||
el.classList.toggle('empty', !!empty);
|
||||
},
|
||||
setComposerText(str){
|
||||
var el = this._composerEl();
|
||||
if(!el){ this.input = str || ''; return; }
|
||||
el.textContent = str || '';
|
||||
this.input = str || '';
|
||||
this.resizeInput();
|
||||
this._emptyCheck();
|
||||
},
|
||||
clearComposer(){
|
||||
var el = this._composerEl();
|
||||
if(el){ el.textContent = ''; }
|
||||
this.input = '';
|
||||
this.resizeInput();
|
||||
this._emptyCheck();
|
||||
},
|
||||
_focusComposer(){
|
||||
var t = this._composerEl();
|
||||
if(t) t.focus();
|
||||
},
|
||||
insertNewline(){
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
if(!t) return;
|
||||
var s = t.selectionStart, e = t.selectionEnd;
|
||||
var v = this.input || '';
|
||||
this.input = v.slice(0, s) + '\n' + v.slice(e);
|
||||
this.$nextTick(function(){ t.selectionStart = t.selectionEnd = s + 1; });
|
||||
var el = this._composerEl();
|
||||
if(!el) return;
|
||||
try{
|
||||
var sel = window.getSelection();
|
||||
if(!sel.rangeCount) return;
|
||||
var r = sel.getRangeAt(0);
|
||||
r.deleteContents();
|
||||
var br = document.createElement('br');
|
||||
r.insertNode(br);
|
||||
r.setStartAfter(br); r.collapse(true);
|
||||
this.input = this.composerText();
|
||||
}catch(e){}
|
||||
this.resizeInput();
|
||||
},
|
||||
_caretQuery(){
|
||||
try{
|
||||
var sel = window.getSelection();
|
||||
if(!sel || !sel.rangeCount || !sel.isCollapsed) return null;
|
||||
var r = sel.getRangeAt(0);
|
||||
var node = r.startContainer;
|
||||
if(node.nodeType !== 3) return null;
|
||||
var s = node.nodeValue, off = r.startOffset, i = off;
|
||||
while(i > 0 && s[i-1] !== '@' && !/\s/.test(s[i-1])) i--;
|
||||
if(i > 0 && s[i-1] === '@' && (i === 1 || /\s/.test(s[i-2]))){
|
||||
return {node: node, at: i-1, raw: s.slice(i, off)};
|
||||
}
|
||||
}catch(e){}
|
||||
return null;
|
||||
},
|
||||
_expandToAt(range){
|
||||
var node = range.startContainer;
|
||||
if(node.nodeType !== 3) return;
|
||||
var s = node.nodeValue, off = range.startOffset, i = off;
|
||||
while(i > 0 && s[i-1] !== '@' && !/\s/.test(s[i-1])) i--;
|
||||
if(i > 0 && s[i-1] === '@') range.setStart(node, i-1);
|
||||
},
|
||||
_insertToken(item, range){
|
||||
var el = this._composerEl();
|
||||
if(!el) return;
|
||||
try{
|
||||
var span = document.createElement('span');
|
||||
span.setAttribute('contenteditable','false');
|
||||
span.setAttribute('data-ctoken','1');
|
||||
span.setAttribute('data-token', item.token);
|
||||
span.setAttribute('data-label', item.label);
|
||||
span.setAttribute('data-kind', item.kind || 'mention');
|
||||
span.className = 'ment-token' + (item.kind === 'skill' ? ' skill' : '');
|
||||
var ico = document.createElement('span'); ico.className='tk-ico'; ico.textContent = ' ' + (item.icon || '');
|
||||
var lbl = document.createElement('span'); lbl.className='tk-lbl'; lbl.textContent = ' ' + item.label + ' ';
|
||||
span.appendChild(ico); span.appendChild(lbl);
|
||||
var useRange = (range && range.startContainer) ? range : null;
|
||||
if(!useRange){
|
||||
var sel = window.getSelection();
|
||||
if(sel && sel.rangeCount){
|
||||
var cand = sel.getRangeAt(0);
|
||||
if(cand && el.contains(cand.startContainer)) useRange = cand;
|
||||
}
|
||||
}
|
||||
if(useRange){
|
||||
if(this._mentionFrom !== '+' && !range) this._expandToAt(useRange);
|
||||
useRange.deleteContents();
|
||||
useRange.insertNode(span);
|
||||
var sp = document.createTextNode(' ');
|
||||
useRange.setStartAfter(span); useRange.collapse(true);
|
||||
useRange.insertNode(sp);
|
||||
useRange.setStart(sp, 1); useRange.setEnd(sp, 1);
|
||||
try{ var s2 = window.getSelection(); if(s2){ s2.removeAllRanges(); s2.addRange(useRange); } }catch(e2){}
|
||||
} else {
|
||||
if(el.textContent) el.appendChild(document.createTextNode(' '));
|
||||
el.appendChild(span);
|
||||
el.appendChild(document.createTextNode(' '));
|
||||
try{ var r2 = document.createRange(); r2.selectNodeContents(el); r2.collapse(false); var s3 = window.getSelection(); if(s3){ s3.removeAllRanges(); s3.addRange(r2); } }catch(e3){}
|
||||
}
|
||||
}catch(e){}
|
||||
this.input = this.composerText();
|
||||
this.resizeInput();
|
||||
this._emptyCheck();
|
||||
},
|
||||
_insertMentionAt(item){
|
||||
var node = this._atNode;
|
||||
if(node && node.nodeType === 3 && node.parentNode){
|
||||
try{
|
||||
var s = node.nodeValue;
|
||||
var idx = -1;
|
||||
for(var k=0;k<s.length;k++){ if(s[k] === '@'){ idx = k; break; } }
|
||||
if(idx >= 0){
|
||||
var r = document.createRange();
|
||||
r.setStart(node, idx);
|
||||
r.setEnd(node, s.length);
|
||||
this._insertToken(item, r);
|
||||
return;
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
this._insertToken(item);
|
||||
},
|
||||
_removeTokenSpan(token){
|
||||
var el = this._composerEl();
|
||||
if(!el) return;
|
||||
var spans = el.querySelectorAll('[data-ctoken][data-token="'+token+'"]');
|
||||
for(var i=0;i<spans.length;i++){
|
||||
var p = spans[i];
|
||||
if(p.parentNode){
|
||||
if(p.nextSibling && p.nextSibling.nodeType===3 && /^\s*$/.test(p.nextSibling.nodeValue)) p.parentNode.removeChild(p.nextSibling);
|
||||
p.parentNode.removeChild(p);
|
||||
}
|
||||
}
|
||||
this.input = this.composerText();
|
||||
this.resizeInput();
|
||||
this._emptyCheck();
|
||||
},
|
||||
_prevToken(node, off){
|
||||
if(node.nodeType === 3 && off === 0){
|
||||
var p = node.previousSibling;
|
||||
while(p && p.nodeType===3 && !p.nodeValue.trim()) p = p.previousSibling;
|
||||
if(p && p.nodeType===1 && p.hasAttribute && p.hasAttribute('data-ctoken')) return p;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
_nextToken(node, off){
|
||||
if(node.nodeType === 3 && off === node.nodeValue.length){
|
||||
var p = node.nextSibling;
|
||||
while(p && p.nodeType===3 && !p.nodeValue.trim()) p = p.nextSibling;
|
||||
if(p && p.nodeType===1 && p.hasAttribute && p.hasAttribute('data-ctoken')) return p;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
_removeTokenDom(span){
|
||||
var token = span.getAttribute('data-token');
|
||||
if(span.parentNode){
|
||||
if(span.previousSibling && span.previousSibling.nodeType===3 && /^\s*$/.test(span.previousSibling.nodeValue)) span.parentNode.removeChild(span.previousSibling);
|
||||
if(span.nextSibling && span.nextSibling.nodeType===3 && /^\s*$/.test(span.nextSibling.nodeValue)) span.parentNode.removeChild(span.nextSibling);
|
||||
span.parentNode.removeChild(span);
|
||||
}
|
||||
for(var i=0;i<this.contextChips.length;i++){ if(this.contextChips[i].token === token){ this.contextChips.splice(i,1); break; } }
|
||||
this.input = this.composerText();
|
||||
this.resizeInput();
|
||||
this._emptyCheck();
|
||||
},
|
||||
onPaste(e){
|
||||
e.preventDefault();
|
||||
var text = '';
|
||||
try{ if(e.clipboardData) text = e.clipboardData.getData('text/plain') || ''; }catch(err){ }
|
||||
if(!text) return;
|
||||
try{
|
||||
var sel = window.getSelection();
|
||||
if(sel && sel.rangeCount){ sel.getRangeAt(0).deleteContents(); document.execCommand('insertText', false, text); }
|
||||
}catch(err){}
|
||||
this.input = this.composerText();
|
||||
this.resizeInput();
|
||||
},
|
||||
get canSend(){ return !!((this.input || '').trim() || this.contextChips.length); },
|
||||
get mentionChips(){ return (this.contextChips || []).filter(function(c){ return c.kind === 'mention'; }); },
|
||||
get skillChips(){ return (this.contextChips || []).filter(function(c){ return c.kind === 'skill'; }); },
|
||||
get commandOpen(){ return /^\s*\//.test(this.input) && !this.streaming; },
|
||||
get commandSuggestions(){
|
||||
var raw = (this.input||'').trim();
|
||||
if(!/^\//.test(raw)) return [];
|
||||
var q = raw.replace(/^\/+/, '').toLowerCase();
|
||||
var first = (q.split(/\s+/).filter(Boolean)[0] || '');
|
||||
var all = [
|
||||
{cmd:'/help', desc:'liste des commandes'},
|
||||
{cmd:'/providers', desc:'liste les fournisseurs actifs'},
|
||||
{cmd:'/provider <nom>', desc:'changer de fournisseur LLM'},
|
||||
{cmd:'/model <nom>', desc:'changer de modèle LLM'},
|
||||
{cmd:'/keys', desc:'fournisseurs avec clé API enregistrée'}
|
||||
];
|
||||
if(!first) return all;
|
||||
return all.filter(function(c){ return c.cmd.toLowerCase().replace(/^\/+/, '').indexOf(first) === 0; });
|
||||
var first = (q.split(/\s+/).filter(Boolean)[0] || '').replace(/[^a-z0-9-]/g, '');
|
||||
function qmatch(name){
|
||||
var slug = String(name).toLowerCase().split(/[\s<]/)[0].replace(/^\/+/, '');
|
||||
if(!first) return true;
|
||||
return slug.indexOf(first) === 0 || slug.replace(/-/g, '').indexOf(first) === 0;
|
||||
}
|
||||
var out = [];
|
||||
var seq = 0;
|
||||
function pushItems(items){
|
||||
items.forEach(function(o){ o._i = seq++; out.push(o); });
|
||||
}
|
||||
var skillsPresent = false, admPresent = false;
|
||||
var built = [], dbs = [], adm = [];
|
||||
for(var i=0;i<FD_SKILLS.length;i++){
|
||||
if(qmatch(FD_SKILLS[i].cmd)){
|
||||
var s = FD_SKILLS[i];
|
||||
built.push({key:'bi-'+s.cmd, cmd:s.cmd, desc:s.desc, icon:s.icon, template:s.template, _type:'builtin'});
|
||||
}
|
||||
}
|
||||
for(i=0;i<(this.skills||[]).length;i++){
|
||||
var sk = this.skills[i];
|
||||
if(qmatch(sk.name)){
|
||||
dbs.push({key:'db-'+sk.id, cmd:'/'+sk.name, desc:(sk.description || 'Skill enregistré'), icon:'✨', _type:'db', skillId:sk.id});
|
||||
}
|
||||
}
|
||||
for(i=0;i<FD_ADMIN_CMDS.length;i++){
|
||||
if(qmatch(FD_ADMIN_CMDS[i].cmd)) adm.push({key:'adm-'+FD_ADMIN_CMDS[i].cmd, cmd:FD_ADMIN_CMDS[i].cmd, desc:FD_ADMIN_CMDS[i].desc});
|
||||
}
|
||||
skillsPresent = built.length || dbs.length;
|
||||
admPresent = adm.length;
|
||||
if(skillsPresent){
|
||||
out.push({_sep:'Skills', key:'sep-skills'});
|
||||
pushItems(built.concat(dbs));
|
||||
}
|
||||
if(skillsPresent && admPresent) out.push({_sep:'Commandes', key:'sep-adm'});
|
||||
if(admPresent) pushItems(adm);
|
||||
this._slashSeq = seq;
|
||||
return out;
|
||||
},
|
||||
// Items slash « exécutables » (skills) — non séparateurs
|
||||
_slashItems(){
|
||||
return this.commandSuggestions.filter(function(o){ return !o._sep; });
|
||||
},
|
||||
_slugSkill(slug){
|
||||
slug = String(slug || '').toLowerCase().replace(/^\/+/, '');
|
||||
for(var i=0;i<FD_SKILLS.length;i++){
|
||||
var c = FD_SKILLS[i].cmd.replace(/^\/+/, '').toLowerCase();
|
||||
if(c === slug || c.replace(/-/g, '') === slug) return FD_SKILLS[i];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
useSuggestion(c){
|
||||
var v = c.cmd.replace(/\s*<[^>]*>\s*$/, ' ');
|
||||
this.input = v;
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
if(t){ t.focus(); this.resizeInput(); }
|
||||
var self = this;
|
||||
if(!c || c._sep) return;
|
||||
if(c._admin){
|
||||
this.setComposerText(c.cmd.replace(/\s*<[^>]*>\s*$/, ' '));
|
||||
this.cmdFocus = -1;
|
||||
this._focusComposer();
|
||||
return;
|
||||
}
|
||||
if(c._type === 'db'){
|
||||
var exists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.skillId === c.skillId; });
|
||||
if(exists){ this.toast('Ce skill est déjà épinglé au contexte.'); }
|
||||
else {
|
||||
var hasSkill = this.contextChips.some(function(ch){ return ch.kind === 'skill'; });
|
||||
this.contextChips.push({key:'skill-'+c.skillId, kind:'skill', icon:c.icon||'✨',
|
||||
label:String(c.cmd).replace(/^\//, ''), token:'skill:'+c.skillId, skillId:c.skillId});
|
||||
this.clearComposer();
|
||||
this._insertToken({token:'skill:'+c.skillId, label:String(c.cmd).replace(/^\//, ''), icon:c.icon||'✨', kind:'skill'});
|
||||
this.toast('Skill épinglé — décrivez votre demande, il sera appliqué à l\u2019envoi.');
|
||||
}
|
||||
} else {
|
||||
this.setComposerText(c.template || '');
|
||||
this.toast('Complétez la demande puis envoyez (Entrée).');
|
||||
}
|
||||
this.cmdFocus = -1; this.slashDismiss = false;
|
||||
this._focusComposer();
|
||||
},
|
||||
echo(msg){
|
||||
this.messages.push({id:'cmd-'+Date.now()+Math.random(), role:'assistant', content: msg, model:''});
|
||||
@@ -676,7 +1093,7 @@
|
||||
var cmd = (parts.shift() || '').toLowerCase();
|
||||
var arg = parts.join(' ').trim();
|
||||
if(cmd === 'help' || cmd === 'aide' || cmd === '?'){
|
||||
this.echo('Commandes : /providers · /keys · /provider <nom> · /model <nom> · /help');
|
||||
this.echo('Tapez / pour choisir un skill (research, resume, brainstorm, plan, meeting-note…). Commandes : /providers · /keys · /provider <nom> · /model <nom>');
|
||||
return true;
|
||||
}
|
||||
if(cmd === 'providers'){
|
||||
@@ -723,36 +1140,289 @@
|
||||
return false;
|
||||
},
|
||||
|
||||
// ── Toast ──
|
||||
toast(msg, err){
|
||||
var self = this;
|
||||
this.toastMsg = msg || '';
|
||||
this.toastErr = !!err;
|
||||
if(this._toastTimer) clearTimeout(this._toastTimer);
|
||||
if(!msg){ return; }
|
||||
this._toastTimer = setTimeout(function(){ self.toastMsg = ''; self.toastErr = false; }, 3200);
|
||||
},
|
||||
|
||||
// ── Mentions & contexte épinglé (@ / +) ──
|
||||
toggleAddPicker(){
|
||||
if(this.mentionOpen && this._mentionFrom === '+'){ this.mentionOpen = false; this.mentionQuery = ''; return; }
|
||||
this.openMentionPicker('+');
|
||||
},
|
||||
openMentionPicker(from){
|
||||
this.mentionOpen = true;
|
||||
this._mentionFrom = from || '+';
|
||||
this.mentionQuery = '';
|
||||
this.mentionFocus = -1;
|
||||
this.searchMentions('');
|
||||
},
|
||||
searchMentions(q){
|
||||
var self = this;
|
||||
q = (q || '').trim();
|
||||
this.mentionQuery = q;
|
||||
this.mentionLoad = true;
|
||||
if(this._mentionTimer) clearTimeout(this._mentionTimer);
|
||||
this._mentionTimer = setTimeout(function(){
|
||||
fetch('/api/agent/mentions?q=' + encodeURIComponent(q))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(d){
|
||||
self.mentionItems = (d.mentions || []).map(function(it, idx){
|
||||
it._i = idx;
|
||||
return it;
|
||||
});
|
||||
self.mentionLoad = false;
|
||||
if(self.mentionItems.length && self.mentionFocus < 0) self.mentionFocus = 0;
|
||||
})
|
||||
.catch(function(){ self.mentionItems = []; self.mentionLoad = false; });
|
||||
}, q ? 180 : 0);
|
||||
},
|
||||
addChip(item){
|
||||
if(!item || !item.token) return;
|
||||
var dup = this.contextChips.some(function(ch){ return ch.token === item.token; });
|
||||
if(dup){ this.toast('Déjà dans le contexte'); return; }
|
||||
this.contextChips.push({key: item.token, kind: 'mention', icon: item.icon || '📎',
|
||||
label: item.label, token: item.token, type: item.type, refId: item.id});
|
||||
},
|
||||
pickMention(item){
|
||||
if(!item) return;
|
||||
this.addChip(item);
|
||||
if(this._mentionFrom === '@'){
|
||||
this._insertMentionAt(item);
|
||||
}
|
||||
this.mentionOpen = false;
|
||||
this.mentionQuery = '';
|
||||
this._mentionFrom = null;
|
||||
this._atNode = null;
|
||||
this._focusComposer();
|
||||
},
|
||||
removeChip(token){
|
||||
var idx = -1;
|
||||
for(var i=0;i<this.contextChips.length;i++){
|
||||
if(this.contextChips[i].token === token){ idx = i; break; }
|
||||
}
|
||||
if(idx < 0) return;
|
||||
var chip = this.contextChips[idx];
|
||||
this.contextChips.splice(idx, 1);
|
||||
this._removeTokenSpan(chip.token);
|
||||
this._emptyCheck();
|
||||
},
|
||||
onInput(){
|
||||
this.resizeInput();
|
||||
this.slashDismiss = false;
|
||||
var text = this.composerText();
|
||||
this.input = text;
|
||||
var cmdNow = /^\s*\//.test(text);
|
||||
if(cmdNow){
|
||||
if(!this._cmdWasOpen){
|
||||
var firstSkill = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
|
||||
this.cmdFocus = firstSkill ? firstSkill._i : -1;
|
||||
} else if(this.cmdFocus !== -1){
|
||||
var present = (this.commandSuggestions || []).some(function(o){ return !o._sep && o._i === this.cmdFocus; }.bind(this));
|
||||
if(!present){
|
||||
var fs = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
|
||||
this.cmdFocus = fs ? fs._i : -1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.cmdFocus = -1;
|
||||
}
|
||||
this._cmdWasOpen = cmdNow;
|
||||
// @ mention inline
|
||||
var cq = this._caretQuery();
|
||||
if(cq){
|
||||
if(this._mentionFrom !== '@'){ this._mentionFrom = '@'; this._atNode = cq.node; }
|
||||
this.mentionOpen = true;
|
||||
if(this._mentionTimer) clearTimeout(this._mentionTimer);
|
||||
this.mentionLoad = true;
|
||||
this.searchMentions(cq.raw);
|
||||
} else if(this._mentionFrom === '@'){
|
||||
this.mentionOpen = false;
|
||||
this.mentionQuery = '';
|
||||
}
|
||||
this._emptyCheck();
|
||||
},
|
||||
moveMentionFocus(delta){
|
||||
if(!this.mentionItems.length) return;
|
||||
var n = this.mentionItems.length;
|
||||
var idx = this.mentionItems.findIndex(function(it){ return it._i === this.mentionFocus; }.bind(this));
|
||||
if(idx < 0) idx = delta > 0 ? -1 : 0;
|
||||
idx = (idx + delta + n) % n;
|
||||
this.mentionFocus = this.mentionItems[idx]._i;
|
||||
},
|
||||
moveSlashFocus(delta){
|
||||
var items = this._slashItems().filter(function(o){ return !o._admin; });
|
||||
if(!items.length) return;
|
||||
var idx = items.findIndex(function(o){ return o._i === this.cmdFocus; }.bind(this));
|
||||
if(idx < 0) idx = delta > 0 ? -1 : 0;
|
||||
idx = (idx + delta + items.length) % items.length;
|
||||
this.cmdFocus = items[idx]._i;
|
||||
},
|
||||
onComposerKeydown(e){
|
||||
var key = e.key;
|
||||
if(key === 'Backspace' || key === 'Delete'){
|
||||
try{
|
||||
var sel = window.getSelection();
|
||||
if(sel && sel.rangeCount){
|
||||
var r = sel.getRangeAt(0);
|
||||
if(r.collapsed){
|
||||
var tok = key === 'Backspace'
|
||||
? this._prevToken(r.startContainer, r.startOffset)
|
||||
: this._nextToken(r.startContainer, r.startOffset);
|
||||
if(tok){ e.preventDefault(); this._removeTokenDom(tok); return; }
|
||||
}
|
||||
}
|
||||
}catch(err){}
|
||||
}
|
||||
if(key === 'Escape'){
|
||||
if(this.mentionOpen){ e.preventDefault(); e.stopPropagation(); this.mentionOpen = false; this._mentionFrom = null; this._atNode = null; return; }
|
||||
if(this.commandOpen){ e.preventDefault(); e.stopPropagation(); this.slashDismiss = true; this.cmdFocus = -1; return; }
|
||||
return; // laisse fermer le panneau
|
||||
}
|
||||
if(key === 'ArrowDown' || key === 'ArrowUp'){
|
||||
var delta = key === 'ArrowDown' ? 1 : -1;
|
||||
if(this.mentionOpen && this.mentionItems.length){ e.preventDefault(); this.moveMentionFocus(delta); return; }
|
||||
if(this.commandOpen && !this.slashDismiss && this._slashItems().length){ e.preventDefault(); this.moveSlashFocus(delta); return; }
|
||||
return;
|
||||
}
|
||||
// Entrée / Tab avec un menu ouvert (skills / mentions)
|
||||
if(this.mentionOpen && this.mentionItems.length){
|
||||
if(key === 'Enter' || key === 'Tab'){
|
||||
e.preventDefault();
|
||||
var mi = this.mentionItems.find(function(it){ return it._i === this.mentionFocus; }.bind(this)) || this.mentionItems[0];
|
||||
this.pickMention(mi);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(this.commandOpen && !this.slashDismiss && this._slashItems().length){
|
||||
var items = this._slashItems();
|
||||
if(key === 'Enter' || key === 'Tab'){
|
||||
var focusItem = items.find(function(o){ return o._i === this.cmdFocus; }.bind(this));
|
||||
if(focusItem){
|
||||
if(!focusItem._admin){ e.preventDefault(); this.useSuggestion(focusItem); return; }
|
||||
e.preventDefault(); this.send(); return;
|
||||
}
|
||||
var queryTyped = (this.composerText() || '').replace(/^\/+/, '').trim().length > 0;
|
||||
var onlyAdmin = items.every(function(o){ return o._admin; });
|
||||
var onlySkill = !onlyAdmin && items.every(function(o){ return !o._admin; });
|
||||
if(onlySkill && queryTyped){
|
||||
e.preventDefault(); this.useSuggestion(items[0]); return;
|
||||
}
|
||||
if(onlyAdmin){
|
||||
e.preventDefault(); this.send(); return;
|
||||
}
|
||||
e.preventDefault(); return;
|
||||
}
|
||||
}
|
||||
// Entrée : envoyer (Maj+Entrée = nouvelle ligne)
|
||||
if(key === 'Enter'){
|
||||
if(e.shiftKey){ e.preventDefault(); this.insertNewline(); }
|
||||
else { e.preventDefault(); this.send(); }
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
// ── Actions sous chaque réponse agent ──
|
||||
copyResponse(m){
|
||||
var self = this;
|
||||
var text = (m && m.content) || '';
|
||||
if(!text) return;
|
||||
function fallback(){
|
||||
try{
|
||||
var ta = document.createElement('textarea');
|
||||
ta.value = text;
|
||||
ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||
document.body.appendChild(ta);
|
||||
ta.select();
|
||||
var ok = document.execCommand('copy');
|
||||
document.body.removeChild(ta);
|
||||
self.toast(ok ? 'Réponse copiée.' : 'Copie impossible.', !ok);
|
||||
}catch(err){ self.toast('Copie impossible.', true); }
|
||||
}
|
||||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(text).then(function(){ self.toast('Réponse copiée.'); }).catch(function(){ fallback(); });
|
||||
} else { fallback(); }
|
||||
},
|
||||
insertIntoPage(m){
|
||||
var c = FlowDeckCtx.current();
|
||||
if(!c || c.kind !== 'page'){ this.toast('Aucune page ouverte pour insérer cette réponse.', true); return; }
|
||||
if(!window.fdApplyDocument){ this.toast('Insertion indisponible ici.', true); return; }
|
||||
var text = (m && m.content) || '';
|
||||
if(!text){ return; }
|
||||
var ok = window.fdApplyDocument(text, m && m.mode && !m.applied ? m.mode : 'insert');
|
||||
this.toast(ok ? 'Réponse insérée dans la page.' : 'Insertion impossible (page non éditable).', !ok);
|
||||
},
|
||||
feedback(m, rating){
|
||||
var self = this;
|
||||
if(!m || m.fb) return;
|
||||
var payload = {rating: rating, content: ((m.content) || '').slice(0, 1000)};
|
||||
if(this.currentConv && this.currentConv.id) payload.conversation_id = this.currentConv.id;
|
||||
if(m.id && /^\d+$/.test(String(m.id))) payload.message_id = Number(m.id);
|
||||
fetch('/api/agent/feedback', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
}).then(function(r){ return r.json(); }).then(function(d){
|
||||
if(d && d.status === 'recorded'){ m.fb = rating; }
|
||||
self.toast(rating === 'up' ? 'Merci pour votre retour 👍' : 'Retour enregistré 👎', false);
|
||||
}).catch(function(){ self.toast('Retour non envoyé.', true); });
|
||||
},
|
||||
|
||||
// ── Send / SSE ──
|
||||
async send(){
|
||||
var self = this;
|
||||
if(self.streaming) return;
|
||||
var text = (self.input || '').trim();
|
||||
if(!text) return;
|
||||
if(self.commandOpen && self.handleCommand(text)){ self.input=''; this.resizeInput(); return; }
|
||||
if(!text && !self.contextChips.length) return;
|
||||
if(self.commandOpen && self.handleCommand(text)){ self.clearComposer(); return; }
|
||||
// skill intégré tapé « en dur » (« /research », « /meeting-note … ») → applique son modèle
|
||||
if(self.commandOpen){
|
||||
var firstTok = (text.split(/\s+/)[0] || '');
|
||||
var sk = self._slugSkill(firstTok);
|
||||
if(sk){
|
||||
var rest = text.slice(String(firstTok).length).trim();
|
||||
text = sk.template + (rest ? '\n\n' + rest : '');
|
||||
}
|
||||
}
|
||||
// barre oblique seule (après Escape du menu /) → on ne l'envoie pas
|
||||
if(/^\/+$/.test(text)){ self.clearComposer(); return; }
|
||||
var ment = self.composerMentions();
|
||||
if(!text){
|
||||
if(ment.length){ text = 'Analyse le contexte référencé.'; }
|
||||
else { return; }
|
||||
}
|
||||
try { await self.ensureConversation(); } catch(e){ return; }
|
||||
self.input = '';
|
||||
this.resizeInput();
|
||||
var rawText = text;
|
||||
self.clearComposer();
|
||||
self.errorMsg = '';
|
||||
self.mentionOpen = false;
|
||||
|
||||
// A writing/content request typed while a document is open → route it to
|
||||
// headless text generation and offer the result with Apply / Reject,
|
||||
// instead of sending the agent into tool calls (search/create).
|
||||
if(self.activeContext && self.activeContext.kind === 'page' && self._isContentRequest(text)){
|
||||
if(rawText && self.activeContext && self.activeContext.kind === 'page' && self._isContentRequest(rawText)){
|
||||
self.refreshContext();
|
||||
return self.runDocAction({prompt: text, label: text, mode: self._docModeFor(text)});
|
||||
return self.runDocAction({prompt: rawText, label: rawText, mode: self._docModeFor(rawText)});
|
||||
}
|
||||
|
||||
self.streaming = true;
|
||||
self.liveReasoning = '…';
|
||||
self.messages.push({id:'u-'+Date.now(), role:'user', content: text});
|
||||
if(rawText) self.messages.push({id:'u-'+Date.now(), role:'user', content: rawText});
|
||||
self.scrollBottom();
|
||||
|
||||
var payload = {message: text};
|
||||
var payload = {message: rawText};
|
||||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||||
if(self.llmModel) payload.model = self.llmModel;
|
||||
var ctxText = self.contextPayload();
|
||||
if(ctxText) payload.context = ctxText;
|
||||
if(ment && ment.length) payload.mentions = ment;
|
||||
var skillChip = (self.contextChips || []).find(function(ch){ return ch.kind === 'skill'; });
|
||||
if(skillChip && skillChip.skillId) payload.skill_id = skillChip.skillId;
|
||||
|
||||
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
|
||||
method:'POST',
|
||||
@@ -779,7 +1449,7 @@
|
||||
var msg = ev.status==='success'
|
||||
? (ev.tool+' ✓ '+(ev.message||''))
|
||||
: (ev.tool+' ✗ '+(ev.detail||'erreur'));
|
||||
self.messages.push({id:'a-'+Date.now()+Math.random(), role:'assistant', content: msg});
|
||||
self.messages.push({id:'a-'+Date.now()+Math.random(), role:'assistant', content: msg, act:true});
|
||||
}
|
||||
else if(ev.type==='final'){
|
||||
self.messages.push({id:'f-'+Date.now(), role:'assistant', content: ev.content});
|
||||
|
||||
Reference in New Issue
Block a user