- Table agent_connectors (migration 32) : colonnes plates name, url, secret_encrypted, enabled, status, detail — config_json remis (YAGNI, les scopes OAuth des phases 6-7 ajouteront le leur). - app/services/connectors.py : 1 fichier au lieu du package connectors/ — 3 natifs (gitea, github, web) servis à la volée avec statut sans réseau, CRUD des personnels (URL validée par _validate_url = garde SSRF, clé chiffrée Fernet et jamais renvoyée — seul has_secret), probe() qui persiste status/detail, connector_fetch() borné à 20 000 car. - API /api/agent/connectors : GET, POST (400 URL privée), PATCH, DELETE, POST /connectors/probe — 401 sans session, CSRF global. OpenAPI 519 chemins. - Outil LLM connector_fetch (26e outil) : un seul outil qui dispatche vers Gitea/GitHub/web/personnalisé (id ou nom ou kind + path + query) au lieu d'un outil par connecteur ; désactivé/inconnu = erreur outil, jamais de run cassé. - Menu + : section « Connecteurs » (catalogue avec badge ✓/✗/⚠/?), fiche par connecteur (Tester, Activer/Désactiver, Supprimer — masqués pour les natifs), formulaire « Ajouter un connecteur personnalisé » (clé en type=password). - Tests : tests/test_v755_connectors.py (13) — 3 natifs, roundtrip Fernet de la clé, 5 URLs refusées (localhost, 127.0.0.1, metadata cloud, ftp:, file:), toggle persisté, 401, probe OK/erreur, outil complet, câblage menu ; test_v751/v753/v754 adaptés (1 seule section « bientôt » = plugins). Suite complète 1306 verts (-n auto), ruff 0, eslint 0 problème.
2476 lines
117 KiB
JavaScript
2476 lines
117 KiB
JavaScript
|
||
(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].'}
|
||
];
|
||
// ── Menu + (v7.51.0) : sections à deux niveaux. `soon` = section pas encore
|
||
// livrée → affichée mais non cliquable (le menu ne promet rien que le code ne fait).
|
||
var FD_PLUS_MENU = [
|
||
{key:'files', icon:'📎', label:'Ajouter des fichiers ou répertoires',
|
||
sub:'Rechercher ou parcourir le workspace', action:'files', chev:true},
|
||
{key:'skills', icon:'✨', label:'Compétences-skills',
|
||
sub:'Deep research, Skill-creator… · Gérer · Parcourir', action:'skills', chev:true},
|
||
{key:'connectors', icon:'🔌', label:'Connecteurs',
|
||
sub:'Parcourir les connecteurs · ajouter un personnalisé', action:'connectors', chev:true},
|
||
{key:'canvases', icon:'🎨', label:'Design System – Canevas',
|
||
sub:'Galerie de canevas : créer, insérer, enregistrer', action:'canvases', chev:true},
|
||
{key:'plugins', icon:'🧩', label:'Add plugins',
|
||
sub:'Modules on/off — bientôt (phase 8)', disabled:true},
|
||
{key:'memory', icon:'🧠', label:'Mémoire (on/off)',
|
||
sub:'Résumés des échanges injectés à chaque run', action:'memory'}
|
||
];
|
||
var FD_PLUS_FILES = [
|
||
{key:'f-search', icon:'🔍', label:'Rechercher…',
|
||
sub:'Documents, bases, pages par nom', action:'search', chev:true},
|
||
{key:'f-browse', icon:'📁', label:'Parcourir…',
|
||
sub:'Dossiers et fichiers du workspace', action:'browse', chev:true}
|
||
];
|
||
// Libellés affichés des skills builtin (le slug /commande reste inchangé).
|
||
var FD_SKILL_ALIAS = {'research': 'Deep research', 'create-new-skill': 'Skill-creator'};
|
||
|
||
function agentPanel(){
|
||
return {
|
||
open: false, modelOpen: false, tab: 'chat',
|
||
agents: [], conversations: [], currentConv: null,
|
||
messages: [], input: '', streaming: false, liveReasoning: '',
|
||
provider: 'offline', providerAvailable: false, errorMsg: '', notice: '',
|
||
allProviders: [], providers: [], providerModels: [], llmProvider: 'offline',
|
||
llmModel: '', llmKeys: [], defaultProvider: 'offline',
|
||
activeContext: null, ctxPinnedKey: null,
|
||
contextChips: [], skills: [], gallery: [],
|
||
mentionOpen: false, mentionItems: [], mentionQuery: '', mentionLoad: false,
|
||
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
|
||
plusOpen: false, plusSection: 'root', plusFocus: -1, plusLoading: false,
|
||
plusBrowse: [], plusCrumb: [],
|
||
skillForm: {id: null, name: '', description: '', prompt_template: ''},
|
||
galleryFilter: '',
|
||
canvases: [], canvas: null,
|
||
connectors: [], connector: null,
|
||
connectorForm: {name: '', url: '', secret: ''},
|
||
cmdFocus: -1, slashDismiss: false,
|
||
toastMsg: '', toastErr: false, _toastTimer: null,
|
||
_livePh: null,
|
||
previewVisible: false, previewTitle: '', previewMeta: '', previewStart: '', previewEnd: '',
|
||
_previewCache: null, _previewTimer: null, _longPressTimer: null, _suppressNextClick: false,
|
||
_slashSeq: 0,
|
||
|
||
init(){
|
||
var self = this;
|
||
var el = document.getElementById('fd-agent-panel');
|
||
|
||
function applyState(){
|
||
if(el){ if(self.open){ el.classList.add('open'); el.setAttribute('aria-hidden','false'); } else { el.classList.remove('open'); el.setAttribute('aria-hidden','true'); } }
|
||
}
|
||
|
||
window.addEventListener('fd-agent-toggle', function(e){
|
||
if(e && e.detail){
|
||
if(e.detail.close){ self.close(); return; }
|
||
if(e.detail.agentId){ self.agentId = e.detail.agentId; }
|
||
self.openAgent(); return;
|
||
}
|
||
// legacy toggle semantics
|
||
if(self.open){ self.close(); } else { self.openAgent(); }
|
||
});
|
||
|
||
this._applyState = applyState;
|
||
this._initResize();
|
||
this.fetchProviders();
|
||
this.fetchAgents();
|
||
this.fetchSkills();
|
||
|
||
// Exposed imperative API (used by the FAB, the sidebar, Ask AI…)
|
||
window.fdAgent.open = function(){ self.openAgent(); };
|
||
window.fdAgent.close = function(){ self.close(); };
|
||
window.fdAgent.toggle = function(){ self.open ? self.close() : self.openAgent(); };
|
||
window.fdAgent.ask = function(text){
|
||
var t = (text || '').trim();
|
||
if(!t) return;
|
||
self.openAgent();
|
||
self.setComposerText(t);
|
||
setTimeout(function(){ self.send(); }, 60);
|
||
};
|
||
// Open the panel on a given conversation (sidebar Chat history).
|
||
window.fdAgent.openConversation = function(id){
|
||
self.openAgent();
|
||
self.loadConversation(id);
|
||
};
|
||
// Headless text generation (no tools) via /agent/generate — the clean
|
||
// path used by "Ask AI" / "AI meeting note" / summarization. Sending a
|
||
// plain write request through the tool-enabled conversation engine sends
|
||
// a tools/tool_choice schema that some providers (e.g. DeepSeek) reject
|
||
// with a 400. This endpoint returns {ok, text, offline, model}.
|
||
window.fdAgent.generate = function(message, context){
|
||
message = (message || '').trim();
|
||
if(!message) return Promise.resolve('');
|
||
var payload = {prompt: message};
|
||
if(context && context.trim()) payload.context = String(context).slice(0, 20000);
|
||
return fetch('/api/agent/generate', {
|
||
method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||
body: JSON.stringify(payload)
|
||
}).then(function(resp){
|
||
return resp.json();
|
||
}).then(function(d){
|
||
if(d && d.ok){ return d.text || ''; }
|
||
throw new Error((d && d.error) ? d.error : 'Erreur IA');
|
||
});
|
||
};
|
||
},
|
||
|
||
// ── Resizable panel width (drag handle on the left edge, persisted) ──
|
||
_apMinWidth(){ return Math.min(360, window.innerWidth); },
|
||
_apMaxWidth(){ return Math.max(this._apMinWidth(), Math.min(Math.round(window.innerWidth * 0.9), 1200)); },
|
||
_apClampWidth(w){
|
||
return Math.round(Math.max(this._apMinWidth(), Math.min(this._apMaxWidth(), w || 0)));
|
||
},
|
||
_apApplyWidth(w){
|
||
var panel = document.getElementById('fd-agent-panel');
|
||
if(!panel) return;
|
||
var width = this._apClampWidth(w);
|
||
panel.style.setProperty('--fd-agent-width', width + 'px');
|
||
return width;
|
||
},
|
||
_initResize(){
|
||
var self = this;
|
||
var panel = document.getElementById('fd-agent-panel');
|
||
var handle = panel && panel.querySelector('.fd-ap-resize');
|
||
if(!panel || !handle) return;
|
||
|
||
// Restore the user width (clamped to the current viewport).
|
||
try {
|
||
var saved = parseInt(localStorage.getItem('fd_agent_width') || '', 10);
|
||
if(saved) this._apApplyWidth(saved);
|
||
} catch{ /* stockage indisponible */ }
|
||
|
||
var startX = 0, startWidth = 0, dragging = false;
|
||
var persist = function(){
|
||
try { localStorage.setItem('fd_agent_width', panel.style.getPropertyValue('--fd-agent-width')); } catch{ /* ignoré */ }
|
||
};
|
||
|
||
handle.addEventListener('pointerdown', function(e){
|
||
if(e.button !== 0) return;
|
||
e.preventDefault();
|
||
try { handle.setPointerCapture(e.pointerId); } catch{ /* ignoré */ }
|
||
startX = e.clientX;
|
||
startWidth = panel.offsetWidth;
|
||
dragging = true;
|
||
document.body.classList.add('fd-ap-resizing');
|
||
});
|
||
handle.addEventListener('pointermove', function(e){
|
||
if(!dragging) return;
|
||
// The panel is anchored to the right edge → dragging left widens it.
|
||
self._apApplyWidth(startWidth - (e.clientX - startX));
|
||
});
|
||
var endDrag = function(e){
|
||
if(!dragging) return;
|
||
dragging = false;
|
||
document.body.classList.remove('fd-ap-resizing');
|
||
persist();
|
||
try { handle.releasePointerCapture(e.pointerId); } catch{ /* ignoré */ }
|
||
};
|
||
handle.addEventListener('pointerup', endDrag);
|
||
handle.addEventListener('pointercancel', endDrag);
|
||
handle.addEventListener('dblclick', function(){
|
||
self._apApplyWidth(470);
|
||
persist();
|
||
});
|
||
// Keyboard a11y: ← / → adjust by 20px, Home resets to the default.
|
||
handle.addEventListener('keydown', function(e){
|
||
var step = e.shiftKey ? 60 : 20, cur = panel.offsetWidth;
|
||
if(e.key === 'ArrowLeft'){ self._apApplyWidth(cur + step); }
|
||
else if(e.key === 'ArrowRight'){ self._apApplyWidth(cur - step); }
|
||
else if(e.key === 'Home'){ self._apApplyWidth(470); }
|
||
else { return; }
|
||
e.preventDefault();
|
||
persist();
|
||
});
|
||
// Keep the width usable when the window shrinks / grows.
|
||
window.addEventListener('resize', function(){
|
||
self._apApplyWidth(panel.offsetWidth);
|
||
});
|
||
},
|
||
|
||
// ── Open / close (robust) ──
|
||
openAgent(){
|
||
if(this.open) { this.refreshContext(); return; }
|
||
this.open = true;
|
||
this.refreshContext(true);
|
||
if(this._applyState) this._applyState();
|
||
this.loadConversations();
|
||
var self = this;
|
||
this.$nextTick(function(){
|
||
var t = self.$refs && self.$refs.cmdInput;
|
||
if(t){ t.focus(); self.resizeInput(); }
|
||
});
|
||
},
|
||
close(){
|
||
if(!this.open) return;
|
||
this.open = false;
|
||
this.modelOpen = false;
|
||
this.hidePreview();
|
||
if(this._applyState) this._applyState();
|
||
},
|
||
toggle(){ this.open ? this.close() : this.openAgent(); },
|
||
|
||
// ── 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; 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.title ? ('Contexte : ' + c.title + '.').slice(0, 2000) : '';
|
||
},
|
||
get composerHint(){
|
||
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./skill épinglé(s)' : '';
|
||
return base + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne';
|
||
},
|
||
get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
|
||
|
||
// ── Quick actions (contextual) ──
|
||
get quickIntro(){
|
||
var c = this.activeContext;
|
||
if(c && c.kind === 'page') return 'Comment puis-je vous aider sur ce document ?';
|
||
return 'Que voulez-vous faire ?';
|
||
},
|
||
get quickActions(){
|
||
var c = this.activeContext || {};
|
||
var out = [];
|
||
if(c.kind === 'page'){
|
||
out.push({icon:'📄', label:'Résumer ce document', prompt:'Résume ce document de façon structurée et concise.', docRun:true, mode:'insert'});
|
||
out.push({icon:'🌐', label:'Traduire cette page', prompt:'Traduis le contenu de ce document en anglais, en conservant sa structure (titres, listes, paragraphes).', docRun:true, mode:'replace'});
|
||
out.push({icon:'✍️', label:'Proposer des améliorations', prompt:'Analyse ce document et propose une version améliorée (structure, clarté, complétude) sans en perdre le sens.', docRun:true, mode:'insert'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Rédige une « AI Meeting Note » structurée à partir de ce document : en-tête avec date, sections Résumé, Décisions, Action items et Prochaines étapes.', docRun:true, mode:'insert'});
|
||
} else if(c.kind === 'db'){
|
||
out.push({icon:'🗄️', label:'Analyser cette base', prompt:'Analyse cette base de données et résume sa structure, ses propriétés et ses données.'});
|
||
out.push({icon:'📊', label:'Créer une vue', prompt:'Propose une vue adaptée (board, tableau, calendrier…) pour cette base et explique pourquoi.'});
|
||
out.push({icon:'📈', label:'Synthèse des données', prompt:'Produis une synthèse des données présentes dans cette base.'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré.'});
|
||
} else if(c.kind === 'tasks'){
|
||
out.push({icon:'✅', label:'Mes tâches', prompt:'Liste mes tâches et suggère les priorités de la journée.'});
|
||
out.push({icon:'🗓️', label:'Planifier ma journée', prompt:'Aide-moi à organiser ma journée à partir de mes tâches.'});
|
||
out.push({icon:'🤖', label:'Question générale', prompt:'Explique-moi comment fonctionne My Tasks dans FlowDeck.'});
|
||
} else {
|
||
out.push({icon:'🗄️', label:'Créer une collection', prompt:'Crée une collection « Projets » avec les propriétés Status et Priorité, puis une vue board.', new:true});
|
||
out.push({icon:'📋', label:'Créer un board', prompt:'Crée un board pour suivre le travail de l\u2019équipe.'});
|
||
out.push({icon:'❓', label:'Comment utiliser FlowDeck ?', prompt:'Guide-moi sur les fonctionnalités principales de FlowDeck et comment les utiliser.'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré.'});
|
||
}
|
||
return out;
|
||
},
|
||
runQuick(q){
|
||
if(!q || !q.prompt) return;
|
||
if(q.docRun){ this.runDocAction(q); return; }
|
||
var self = this;
|
||
this.ensureConversation().then(function(){
|
||
self.setComposerText(q.prompt);
|
||
self.send();
|
||
}).catch(function(){});
|
||
},
|
||
|
||
// ── Document actions (no tools) → proposition à approuver / rejeter ──
|
||
_isContentRequest(t){
|
||
var low = (t || '').toLowerCase();
|
||
var kws = ['écris','écrire','écrit un','rédige','rédiger','génère','générer','produis',
|
||
'développe','texte sur','paragraphe','article sur','rédaction','résume','résumer',
|
||
'résumé de','traduis','traduire','améliore','améliorer','note de réunion',
|
||
'série tv','série','contenu de la page','poème','histoire sur','sujet de','courriel','email sur'];
|
||
for(var i=0;i<kws.length;i++){ if(low.indexOf(kws[i]) !== -1) return true; }
|
||
return false;
|
||
},
|
||
_docModeFor(t){
|
||
var low = (t || '').toLowerCase();
|
||
if(low.indexOf('traduis') !== -1 || low.indexOf('traduire') !== -1 || low.indexOf('translate') !== -1) return 'replace';
|
||
return 'insert';
|
||
},
|
||
runDocAction(q){
|
||
var self = this;
|
||
this.ensureConversation().then(function(){
|
||
self.errorMsg = '';
|
||
self.messages.push({id:'udoc-'+Date.now(), role:'user', content: q.label});
|
||
self.refreshContext();
|
||
var ctxText = (self.activeContext && self.activeContext.kind === 'page' && self.activeContext.text) ? self.activeContext.text : '';
|
||
var thinkId = 'g-'+Date.now()+Math.random();
|
||
self.messages.push({id: thinkId, role:'assistant', generating: true});
|
||
self.scrollBottom();
|
||
|
||
var payload = {prompt: q.prompt};
|
||
if(ctxText) payload.context = ctxText;
|
||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||
if(self.llmModel) payload.model = self.llmModel;
|
||
|
||
fetch('/api/agent/generate', {
|
||
method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify(payload)
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||
if(d && d.ok && d.text){
|
||
self.messages.push({
|
||
id:'prop-'+Date.now()+Math.random(), role:'assistant',
|
||
content: d.text, proposal: true, mode: q.mode || 'insert',
|
||
offline: !!d.offline, model: d.model || '', applied: false, rejected: false
|
||
});
|
||
} else {
|
||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||
content: '⚠️ ' + ((d && d.error) || 'Échec de la génération'), model:''});
|
||
}
|
||
self.scrollBottom();
|
||
}).catch(function(err){
|
||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||
content: '⚠️ ' + ((err && err.message) || 'Erreur réseau'), model:''});
|
||
self.scrollBottom();
|
||
});
|
||
}).catch(function(){});
|
||
},
|
||
// ── A20 ph3 : x-html (markdown) → x-init + effet réactif ──
|
||
bindMarkdown(el, m){
|
||
Alpine.effect(() => { el.innerHTML = this.renderMarkdown(m.content); });
|
||
},
|
||
approveProposal(m){
|
||
if(!m || m.applied || m.offline) return;
|
||
var ok = window.fdApplyDocument ? window.fdApplyDocument(m.content, m.mode) : false;
|
||
m.applied = true;
|
||
m.rejected = false;
|
||
if(!ok){
|
||
m.content = '⚠️ Aucun document éditable n\u2019est ouvert pour appliquer cette proposition.';
|
||
}
|
||
this.$nextTick(function(){ this.scrollBottom(); });
|
||
},
|
||
rejectProposal(m){
|
||
if(!m || m.applied) return;
|
||
m.applied = true;
|
||
m.rejected = true;
|
||
this.$nextTick(function(){ this.scrollBottom(); });
|
||
},
|
||
|
||
// ── Providers / models ──
|
||
fetchProviders(){
|
||
var self = this;
|
||
fetch('/api/agent/providers').then(function(r){return r.json()}).then(function(d){
|
||
self.provider = d.provider || 'offline';
|
||
self.providerAvailable = !!d.available;
|
||
self.allProviders = d.providers || [];
|
||
self.llmKeys = d.keys || [];
|
||
self.defaultProvider = d.default_provider || d.provider || 'offline';
|
||
self.providers = self.allProviders.filter(function(p){ return p.functional; });
|
||
if(!self.providers.some(function(x){ return x.id === 'offline'; })){
|
||
self.providers.push({id:'offline', name:'Hors-ligne', functional:true, verified:true, models: [], default_model:'', requires_key:false});
|
||
}
|
||
var want = self.defaultProvider;
|
||
if(!self.providers.some(function(x){ return x.id === want; })) want = 'offline';
|
||
if(want === 'offline'){
|
||
var firstReal = self.providers.find(function(x){ return x.id !== 'offline'; });
|
||
if(firstReal) want = firstReal.id;
|
||
}
|
||
if(!self.providers.some(function(x){ return x.id === self.llmProvider; })) self.llmProvider = want;
|
||
if(!self.llmModel) self.llmModel = d.model || '';
|
||
self.applyModels();
|
||
}).catch(function(){});
|
||
},
|
||
applyModels(){
|
||
var self = this;
|
||
var p = self.providers.find(function(x){ return x.id === self.llmProvider; });
|
||
self.providerModels = (p && p.models) || [];
|
||
if(self.providerModels.length && self.providerModels.indexOf(self.llmModel) === -1){
|
||
self.llmModel = (p && p.default_model) || self.providerModels[0];
|
||
}
|
||
},
|
||
onProviderChange(){
|
||
var self = this;
|
||
var p = self.providers.find(function(x){ return x.id === self.llmProvider; });
|
||
self.providerModels = (p && p.models) || [];
|
||
self.llmModel = (p && p.default_model) || ((p && p.models && p.models[0]) || '');
|
||
self.patchConversation();
|
||
},
|
||
get hasActiveProvider(){ return (this.providers || []).some(function(p){ return p.id !== 'offline'; }); },
|
||
get unverifiedCount(){
|
||
return (this.allProviders || []).filter(function(p){ return p.configured && !p.verified && p.id !== 'offline'; }).length;
|
||
},
|
||
providerName(id){
|
||
var p = (this.allProviders || []).find(function(x){ return x.id === id; });
|
||
return (p && p.name) || id;
|
||
},
|
||
get providerModelLabel(){
|
||
var real = this.llmProvider && this.llmProvider !== 'offline';
|
||
if(!real) return this.hasActiveProvider ? 'Choisissez un fournisseur' : 'Mode hors-ligne (mock)';
|
||
var label = this.providerName(this.llmProvider);
|
||
return this.llmModel ? (label + ' · ' + this.llmModel) : label;
|
||
},
|
||
get autoLabel(){
|
||
if(!this.llmProvider || this.llmProvider === 'offline'){
|
||
return this.hasActiveProvider ? 'Choisir un modèle' : 'Hors-ligne (mock)';
|
||
}
|
||
var base = this.llmModel || this.providerName(this.llmProvider);
|
||
return 'Auto · ' + base;
|
||
},
|
||
|
||
// ── Conversations ──
|
||
fetchAgents(){
|
||
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 = []; });
|
||
// Galerie de skills (marketplace, v6.6.0) — presets installables en 1 clic.
|
||
fetch('/api/agent/skills/gallery').then(function(r){return r.json()}).then(function(d){
|
||
var installed = {};
|
||
(self.skills || []).forEach(function(s){ installed[String(s.name).toLowerCase()] = true; });
|
||
self.gallery = (d.gallery || []).filter(function(g){
|
||
return !installed[String(g.name).toLowerCase()];
|
||
});
|
||
}).catch(function(){ self.gallery = []; });
|
||
},
|
||
// Installe un preset de galerie puis l'épingle au contexte (phase 5).
|
||
installGallerySkill(slug, icon, name){
|
||
var self = this;
|
||
fetch('/api/agent/skills/gallery/' + encodeURIComponent(slug) + '/install', {
|
||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'}, body: '{}'
|
||
}).then(function(r){
|
||
return r.json().then(function(d){ return {ok: r.ok, d: d}; });
|
||
}).then(function(res){
|
||
if(!res.ok){
|
||
self.toast((res.d && res.d.detail) || 'Installation impossible.', true);
|
||
return;
|
||
}
|
||
self.toast('Skill « ' + (res.d.name || name || slug) + ' » installée.');
|
||
self.fetchSkills();
|
||
self._pinDbSkill(res.d.id, res.d.name || name || slug, res.d.icon || icon || '📦');
|
||
}).catch(function(){ self.toast('Installation impossible (réseau).', true); });
|
||
},
|
||
// Épingle un skill enregistré (id DB) dans les chips de contexte.
|
||
_pinDbSkill(id, name, icon){
|
||
var exists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.skillId === id; });
|
||
if(exists){ this.toast('Ce skill est déjà épinglé au contexte.'); return; }
|
||
var pin = {key:'skill-'+id, kind:'skill', icon: icon || '✨',
|
||
label: String(name || '').replace(/^\//, ''), token:'skill:'+id, skillId:id};
|
||
this.contextChips.push(pin);
|
||
// Ne jamais effacer un message déjà tapé (le token s'ajoute à la fin).
|
||
if(!this.composerText().trim()) this.clearComposer();
|
||
this._insertToken({token:pin.token, label:pin.label, icon:pin.icon, kind:'skill'});
|
||
this.toast('Skill épinglé — décrivez votre demande, il sera appliqué à l\u2019envoi.');
|
||
},
|
||
showHistory(){ this.tab = 'history'; this.loadConversations(); },
|
||
loadConversations(){
|
||
var self = this;
|
||
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){
|
||
self.conversations = d.conversations || [];
|
||
if(self.conversations.length && !self.currentConv){
|
||
self.currentConv = self.conversations[0];
|
||
self.loadConversation(self.currentConv.id);
|
||
}
|
||
}).catch(function(){});
|
||
},
|
||
loadConversation(id){
|
||
var self = this;
|
||
this.tab='chat';
|
||
this.errorMsg = '';
|
||
this.hidePreview();
|
||
fetch('/api/agent/conversations/'+id).then(function(r){return r.json()}).then(function(d){
|
||
self.currentConv = d.conversation;
|
||
self.messages = (d.messages||[]).map(function(m){ return {id:m.id, role:m.role, content:m.content, model:m.model, tool_calls:m.tool_calls_json}; });
|
||
var prov = d.conversation && d.conversation.provider;
|
||
if(prov && (!self.providers.length || self.providers.some(function(x){ return x.id === prov; }))){ self.llmProvider = prov; }
|
||
self.applyModels();
|
||
if(d.conversation && d.conversation.model && self.providerModels.indexOf(d.conversation.model) !== -1){ self.llmModel = d.conversation.model; }
|
||
// Rebuild the steps accordion from the audit trail (actions only —
|
||
// the reasoning thoughts are not persisted, only shown live).
|
||
self.attachPersistedSteps(id);
|
||
}).catch(function(){});
|
||
},
|
||
attachPersistedSteps(id){
|
||
var self = this;
|
||
fetch('/api/agent/conversations/'+id+'/actions').then(function(r){return r.json()}).then(function(d){
|
||
var acts = d.actions || [];
|
||
if(!acts.length) return;
|
||
var lastAsst = null;
|
||
for(var i=self.messages.length-1;i>=0;i--){
|
||
var m = self.messages[i];
|
||
if(m.role==='assistant' && !m.proposal){ lastAsst = m; break; }
|
||
}
|
||
if(!lastAsst) return;
|
||
var steps = acts.map(function(a){
|
||
var res = null;
|
||
try{ res = a.result_json ? JSON.parse(a.result_json) : {}; }catch{ res = {}; }
|
||
var msg = (res && res.message) ? res.message : '';
|
||
return agentPanel._actionStep({
|
||
tool: a.tool_name, status: a.status, target_id: a.target_id,
|
||
message: msg, detail: msg || ''
|
||
});
|
||
});
|
||
lastAsst.steps = steps;
|
||
lastAsst.stepsOpen = false;
|
||
}).catch(function(){});
|
||
},
|
||
get previewEmptyHint(){ return 'Aucun échange enregistré pour cette conversation.'; },
|
||
historyClick(id){
|
||
if(this._suppressNextClick){ this._suppressNextClick = false; return; }
|
||
this.loadConversation(id);
|
||
},
|
||
_previewSummary(msgs){
|
||
var first = '', last = '';
|
||
for(var i=0;i<msgs.length;i++){
|
||
var r = msgs[i].role;
|
||
var t = (msgs[i].content || '');
|
||
if(r==='user'){ if(!first && t.trim()) first = agentPanel._strip(t); }
|
||
else if(r==='assistant'){ if(t.trim()) last = agentPanel._strip(t); }
|
||
}
|
||
return { first: first, last: last, total: msgs.length };
|
||
},
|
||
previewConversation(c, ev){
|
||
var self = this;
|
||
// On touch: a quick tap triggers a synthetic mouseenter AFTER touchend —
|
||
// skip it so we don't flash the preview before navigating (the long-press
|
||
// timer handles the preview itself).
|
||
if(ev && ev.type === 'mouseenter' && self._suppressEnterUntil && Date.now() < self._suppressEnterUntil) return;
|
||
var rect = (ev && ev.currentTarget) ? ev.currentTarget.getBoundingClientRect() : null;
|
||
var cache = self._previewCache || (self._previewCache = {});
|
||
function show(sum){
|
||
if(!sum) return;
|
||
self.previewTitle = c.title || 'New conversation';
|
||
var dateStr = (c.updated_at || '').replace('T', ' ').slice(0, 16);
|
||
self.previewMeta = sum.total + ' message(s)' + (dateStr ? ' · ' + dateStr : '');
|
||
self.previewStart = sum.first || '';
|
||
self.previewEnd = sum.last || '';
|
||
self.previewVisible = true;
|
||
self.cancelLongPress();
|
||
self.$nextTick(function(){
|
||
var el = self.$refs && self.$refs.convPreview;
|
||
if(!el) return;
|
||
var pw = el.offsetWidth || 290;
|
||
var m = 10;
|
||
var x = rect ? (rect.left - pw - m) : m;
|
||
if(x < m && rect) x = rect.right + m;
|
||
var y = rect ? Math.min(rect.top, window.innerHeight - el.offsetHeight - m) : m;
|
||
if(y < m) y = m;
|
||
el.style.left = Math.round(x) + 'px';
|
||
el.style.top = Math.round(y) + 'px';
|
||
});
|
||
}
|
||
if(cache[c.id]){ show(cache[c.id]); return; }
|
||
fetch('/api/agent/conversations/'+c.id).then(function(r){ return r.json(); }).then(function(d){
|
||
var sum = self._previewSummary(d.messages || []);
|
||
cache[c.id] = sum;
|
||
show(sum);
|
||
}).catch(function(){ self.schedulePreviewHide(); });
|
||
},
|
||
schedulePreviewHide(){
|
||
var self = this;
|
||
if(this._previewTimer) clearTimeout(this._previewTimer);
|
||
this._previewTimer = setTimeout(function(){ self.previewVisible = false; self._previewTimer = null; }, 220);
|
||
},
|
||
cancelPreviewHide(){
|
||
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
|
||
},
|
||
startLongPress(c, ev){
|
||
var self = this;
|
||
this.cancelLongPress();
|
||
this._longPressTimer = setTimeout(function(){
|
||
self._suppressNextClick = true;
|
||
self.previewConversation(c, ev);
|
||
}, 550);
|
||
},
|
||
cancelLongPress(){
|
||
if(this._longPressTimer){ clearTimeout(this._longPressTimer); this._longPressTimer = null; }
|
||
},
|
||
onTouchEnd(){
|
||
// A quick tap: suppress the synthetic mouseenter preview flash right
|
||
// before the click navigates into the conversation.
|
||
this.cancelLongPress();
|
||
this._suppressEnterUntil = Date.now() + 400;
|
||
},
|
||
onTouchCancel(){
|
||
this.cancelLongPress();
|
||
},
|
||
hidePreview(){
|
||
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
|
||
this.previewVisible = false;
|
||
},
|
||
newConversation(){
|
||
var self = this;
|
||
var body = {title:'Nouvelle conversation'};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
fetch('/api/agent/conversations', {method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify(body)})
|
||
.then(function(r){return r.json()}).then(function(d){
|
||
var conv = {id:d.id, title:d.title, memory_enabled:d.memory_enabled,
|
||
updated_at:new Date().toISOString()};
|
||
self.conversations.unshift(conv); self.currentConv = conv;
|
||
self.messages = []; self.errorMsg=''; self.notice=''; self.tab='chat';
|
||
self.contextChips = []; self.mentionOpen = false;
|
||
self.$nextTick(function(){ self.resizeInput(); });
|
||
}).catch(function(){});
|
||
},
|
||
ensureConversation(){
|
||
var self = this;
|
||
if(self.currentConv && self.currentConv.id) return Promise.resolve(self.currentConv);
|
||
if(self._ensuring) return self._ensuring;
|
||
var body = {title:'Nouvelle conversation'};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
self._ensuring = fetch('/api/agent/conversations', {
|
||
method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
var conv = {id:d.id, title:d.title, memory_enabled:d.memory_enabled,
|
||
updated_at:new Date().toISOString()};
|
||
self.conversations.unshift(conv); self.currentConv = conv;
|
||
self.messages = []; self.errorMsg=''; self.notice=''; self.tab='chat';
|
||
self.contextChips = []; self.mentionOpen = false;
|
||
self._ensuring = null;
|
||
return conv;
|
||
}).catch(function(err){
|
||
self._ensuring = null;
|
||
self.errorMsg = (err && err.message) ? err.message : 'Impossible de créer la conversation';
|
||
throw err;
|
||
});
|
||
return self._ensuring;
|
||
},
|
||
patchConversation(){
|
||
var self = this;
|
||
if(!self.currentConv) return;
|
||
var body = {};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
fetch('/api/agent/conversations/'+self.currentConv.id, {
|
||
method:'PATCH', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||
}).catch(function(){});
|
||
},
|
||
|
||
// ── 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;
|
||
},
|
||
_composePrompt(text){
|
||
var chips = this.contextChips || [];
|
||
var segs = [];
|
||
// contextes épinglés (@ documents / bases / pages)
|
||
chips.forEach(function(ch){
|
||
if(ch.kind === 'mention'){ segs.push('Contexte « ' + (ch.label || '') + ' »'); }
|
||
});
|
||
// skills épinglés : template intégré, ou libellé pour un skill enregistré
|
||
// (son prompt complet est injecté via skill_ids côté serveur)
|
||
chips.forEach(function(ch){
|
||
if(ch.kind === 'skill'){
|
||
if(ch.builtin && ch.template){ segs.push(ch.template); }
|
||
else { segs.push('Skill « ' + (ch.label || '') + ' »'); }
|
||
}
|
||
});
|
||
var t = (text || '').trim();
|
||
if(t){ segs.push(t); }
|
||
return segs.join('\n\n');
|
||
},
|
||
resizeInput(){
|
||
var t = this._composerEl();
|
||
if(!t) return;
|
||
t.style.height = 'auto';
|
||
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 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 { /* volontaire */ }
|
||
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 { /* volontaire */ }
|
||
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 { /* volontaire */ }
|
||
} 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 { /* volontaire */ }
|
||
}
|
||
}catch { /* volontaire */ }
|
||
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 { /* volontaire */ }
|
||
}
|
||
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 { /* volontaire */ }
|
||
if(!text) return;
|
||
try{
|
||
var sel = window.getSelection();
|
||
if(sel && sel.rangeCount){ sel.getRangeAt(0).deleteContents(); document.execCommand('insertText', false, text); }
|
||
}catch { /* volontaire */ }
|
||
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] || '').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 = [], gal = [];
|
||
var installedNames = {};
|
||
for(var j=0;j<(this.skills||[]).length;j++){
|
||
installedNames[String(this.skills[j].name).toLowerCase()] = true;
|
||
}
|
||
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});
|
||
}
|
||
}
|
||
// Galerie (marketplace) — presets pas encore installés.
|
||
for(i=0;i<(this.gallery||[]).length;i++){
|
||
var g = this.gallery[i];
|
||
if(installedNames[String(g.name).toLowerCase()]) continue;
|
||
if(qmatch(g.slug)){
|
||
gal.push({key:'gal-'+g.slug, cmd:'/'+g.slug, desc:(g.description || 'Skill galerie'),
|
||
icon:(g.icon || '📦'), _type:'gallery', slug:g.slug, skillName:g.name});
|
||
}
|
||
}
|
||
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(gal.length){
|
||
out.push({_sep:'Galerie', key:'sep-gal'});
|
||
pushItems(gal);
|
||
}
|
||
if((skillsPresent || gal.length) && 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){
|
||
if(!c || c._sep) return;
|
||
if(c._admin){
|
||
this.setComposerText(c.cmd.replace(/\s*<[^>]*>\s*$/, ' '));
|
||
this.cmdFocus = -1;
|
||
this._focusComposer();
|
||
return;
|
||
}
|
||
if(c._type === 'gallery'){
|
||
this.installGallerySkill(c.slug, c.icon, c.skillName);
|
||
this.cmdFocus = -1; this.slashDismiss = false;
|
||
this._focusComposer();
|
||
return;
|
||
}
|
||
if(c._type === 'db'){
|
||
this._pinDbSkill(c.skillId, c.cmd, c.icon);
|
||
} else {
|
||
var slug = String(c.cmd).replace(/^\/+/, '').toLowerCase().replace(/\s+/g, '-');
|
||
var builtinExists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.token === 'builtin:' + slug; });
|
||
if(builtinExists){ this.toast('Ce skill est déjà épinglé au contexte.'); }
|
||
else {
|
||
this.contextChips.push({key:'bi-'+slug, kind:'skill', icon:c.icon||'✨',
|
||
label:slug, token:'builtin:'+slug, skillId:null, builtin:true, template:c.template||''});
|
||
// On n\u2019injecte PAS le texte du skill dans la bo\u00eete d\u2019\u00e9dition :
|
||
// la requ\u00eate est compos\u00e9e en arri\u00e8re-plan \u00e0 l\u2019envoi.
|
||
this._focusComposer();
|
||
this.toast('Skill épinglé — votre demande sera composée avec lui à l\u2019envoi.');
|
||
}
|
||
}
|
||
this.cmdFocus = -1; this.slashDismiss = false;
|
||
this._focusComposer();
|
||
},
|
||
echo(msg){
|
||
this.messages.push({id:'cmd-'+Date.now()+Math.random(), role:'assistant', content: msg, model:''});
|
||
this.scrollBottom();
|
||
},
|
||
handleCommand(text){
|
||
var parts = text.replace(/^\/+/, '').trim().split(/\s+/);
|
||
var cmd = (parts.shift() || '').toLowerCase();
|
||
var arg = parts.join(' ').trim();
|
||
if(cmd === 'help' || cmd === 'aide' || cmd === '?'){
|
||
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'){
|
||
var list = this.providers.filter(function(x){ return x.id !== 'offline'; })
|
||
.map(function(x){ return x.id + (x.verified ? ' ✓' : ''); }).join(', ');
|
||
this.echo('Fournisseurs actifs : ' + (list || 'aucun — configurez-en un dans Paramètres → Agent & IA'));
|
||
return true;
|
||
}
|
||
if(cmd === 'keys'){
|
||
var saved = this.llmKeys.map(function(k){ return k.provider + (k.verified ? ' ✓' : ' (à tester)'); }).join(', ');
|
||
this.echo('Clés enregistrées : ' + (saved || 'aucune — configurez-les dans Paramètres → Agent & IA'));
|
||
return true;
|
||
}
|
||
if(cmd === 'provider'){
|
||
var avail = this.providers.map(function(x){ return x.id; });
|
||
if(!arg){
|
||
this.echo('Utilisez : /provider <nom>. Fournisseurs : ' + avail.join(', '));
|
||
return true;
|
||
}
|
||
var p = this.providers.find(function(x){ return x.id === arg.toLowerCase(); });
|
||
if(!p){
|
||
this.echo('Fournisseur inconnu : ' + arg + ' (disponibles : ' + avail.join(', ') + ')');
|
||
return true;
|
||
}
|
||
this.llmProvider = p.id;
|
||
this.onProviderChange();
|
||
if(p.models && p.models.length && this.providerModels.indexOf(this.llmModel) === -1){
|
||
this.llmModel = (p.default_model) || p.models[0];
|
||
}
|
||
this.patchConversation();
|
||
this.echo('Provider activé : ' + p.id + ' · modèle : ' + (this.llmModel || 'défaut'));
|
||
return true;
|
||
}
|
||
if(cmd === 'model'){
|
||
if(!arg){
|
||
this.echo('Utilisez : /model <nom>. Modèles pour ' + this.llmProvider + ' : ' + this.providerModels.join(', '));
|
||
return true;
|
||
}
|
||
this.llmModel = arg;
|
||
this.patchConversation();
|
||
this.echo('Modèle activé : ' + arg + ' (' + this.llmProvider + ')');
|
||
return true;
|
||
}
|
||
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);
|
||
},
|
||
|
||
// ── Menu + (sections) & mentions (@ / recherche) ──
|
||
toggleAddPicker(){
|
||
if(this.mentionOpen){
|
||
this.mentionOpen = false; this.mentionQuery = '';
|
||
this._mentionFrom = null; this._atNode = null;
|
||
}
|
||
if(this.plusOpen){ this.closePlus(); return; }
|
||
this.plusOpen = true;
|
||
this.plusSection = 'root';
|
||
this.plusFocus = 0;
|
||
},
|
||
closePlus(){
|
||
this.plusOpen = false;
|
||
this.plusSection = 'root';
|
||
this.plusCrumb = [];
|
||
this.plusBrowse = [];
|
||
this.plusFocus = -1;
|
||
},
|
||
get plusTitle(){
|
||
var s = this.plusSection;
|
||
if(s === 'files') return 'Fichiers & répertoires';
|
||
if(s === 'browse') return 'Parcourir le workspace';
|
||
if(s === 'skills') return 'Compétences-skills';
|
||
if(s === 'skills-manage') return 'Gérer les compétences';
|
||
if(s === 'skills-gallery') return 'Parcourir les compétences';
|
||
if(s === 'skills-edit') return this.skillForm.id ? 'Modifier le skill' : 'Nouveau skill';
|
||
if(s === 'connectors') return 'Connecteurs';
|
||
if(s === 'connector') return this.connector ? this.connector.name : 'Connecteur';
|
||
if(s === 'connector-new') return 'Ajouter un connecteur';
|
||
if(s === 'canvases') return 'Design System – Canevas';
|
||
if(s === 'canvas') return this.canvas ? this.canvas.name : 'Canevas';
|
||
return 'Ajouter au contexte';
|
||
},
|
||
get plusItems(){
|
||
var s = this.plusSection;
|
||
if(s === 'files') return FD_PLUS_FILES;
|
||
if(s === 'browse') return this.plusBrowse;
|
||
if(s === 'skills') return this._skillsItems();
|
||
if(s === 'skills-manage') return this._manageItems();
|
||
if(s === 'skills-gallery') return this._galleryItems();
|
||
if(s === 'skills-edit') return []; // formulaire, pas de liste
|
||
if(s === 'connectors') return this._connectorItems();
|
||
if(s === 'connector') return this._connectorActionItems();
|
||
if(s === 'connector-new') return [];
|
||
if(s === 'canvases') return this._canvasItems();
|
||
if(s === 'canvas') return this._canvasActionItems();
|
||
if(s === 'root'){
|
||
// l'entrée Mémoire reflète l'état réel du toggle de la conversation
|
||
var on = this.memoryOn;
|
||
return FD_PLUS_MENU.map(function(it){
|
||
if(it.key !== 'memory') return it;
|
||
return {key:'memory', icon: on ? '🧠' : '💭', label:'Mémoire (on/off)',
|
||
sub: on ? 'Activée — résumés des échanges injectés à chaque run'
|
||
: 'Désactivée — contexte strict du run courant',
|
||
action:'memory'};
|
||
});
|
||
}
|
||
return FD_PLUS_MENU;
|
||
},
|
||
// État du toggle mémoire de la conversation courante (défaut : activée).
|
||
get memoryOn(){
|
||
if(!this.currentConv) return true;
|
||
var v = this.currentConv.memory_enabled;
|
||
return v === undefined || v === null ? true : !!Number(v);
|
||
},
|
||
// Bascule le toggle mémoire (PATCH /api/agent/conversations/{id}).
|
||
toggleMemory(){
|
||
var self = this, conv = this.currentConv;
|
||
if(!conv || !conv.id){
|
||
this.toast('Ouvrez une conversation pour activer la mémoire.', true);
|
||
return;
|
||
}
|
||
var next = this.memoryOn ? 0 : 1;
|
||
fetch('/api/agent/conversations/' + conv.id, {
|
||
method: 'PATCH', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||
body: JSON.stringify({memory_enabled: next})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Bascule impossible.', true); return; }
|
||
conv.memory_enabled = next;
|
||
self.toast(next ? 'Mémoire activée — résumés injectés à chaque run.'
|
||
: 'Mémoire désactivée — contexte strict du run courant.');
|
||
}).catch(function(){ self.toast('Bascule impossible (réseau).', true); });
|
||
},
|
||
// Sous-menu Compétences : builtin (alias affiché) + installés, puis les 2 actions.
|
||
_skillsItems(){
|
||
var out = [];
|
||
FD_SKILLS.forEach(function(c){
|
||
var slug = String(c.cmd).replace(/^\//, '');
|
||
out.push({key:'bi-'+slug, icon:c.icon, label:FD_SKILL_ALIAS[slug] || c.cmd,
|
||
sub:c.desc, action:'skill',
|
||
suggestion:{key:'bi-'+slug, cmd:c.cmd, icon:c.icon, template:c.template, _type:'builtin'}});
|
||
});
|
||
(this.skills || []).forEach(function(sk){
|
||
out.push({key:'db-'+sk.id, icon:'✨', label:sk.name,
|
||
sub:sk.description || 'Skill enregistré', action:'skill',
|
||
suggestion:{key:'db-'+sk.id, cmd:'/'+sk.name, icon:'✨', _type:'db', skillId:sk.id}});
|
||
});
|
||
out.push({key:'sep-actions', _sep:'Gérer / parcourir'});
|
||
out.push({key:'a-manage', icon:'🛠️', label:'Gérer les compétences',
|
||
sub:'Créer, modifier, supprimer, exporter / importer', action:'skills-manage', chev:true});
|
||
out.push({key:'a-browse', icon:'🗂️', label:'Parcourir les compétences',
|
||
sub:'Galerie de presets installables en 1 clic', action:'skills-gallery', chev:true});
|
||
return out;
|
||
},
|
||
// Sous-section Gérer : liste des skills + création + import.
|
||
_manageItems(){
|
||
var out = [];
|
||
(this.skills || []).forEach(function(sk){
|
||
out.push({key:'m-'+sk.id, icon:'✨', label:sk.name,
|
||
sub:sk.description || 'Cliquer pour modifier', action:'skill-edit', chev:true, skillId:sk.id});
|
||
});
|
||
if(!out.length) out.push({key:'m-none', _sep:'Aucun skill enregistré'});
|
||
out.push({key:'m-new', icon:'+', label:'Nouveau skill',
|
||
sub:'Nom, description, prompt', action:'skill-new', chev:true});
|
||
out.push({key:'m-sep', _sep:'Import'});
|
||
out.push({key:'m-import', icon:'📥', label:'Importer un skill (JSON)',
|
||
sub:'Document exporté depuis une autre instance', action:'skill-import'});
|
||
return out;
|
||
},
|
||
// Sous-section Parcourir : presets de la galerie, filtrables.
|
||
_galleryItems(){
|
||
var f = String(this.galleryFilter || '').toLowerCase();
|
||
var out = [];
|
||
(this.gallery || []).forEach(function(g){
|
||
var hay = (String(g.name || '') + ' ' + (g.description || '') + ' ' + (g.slug || '')).toLowerCase();
|
||
if(f && hay.indexOf(f) < 0) return;
|
||
out.push({key:'g-'+g.slug, icon:g.icon || '📦', label:g.name,
|
||
sub:g.description || 'Preset de la galerie', action:'skill-install',
|
||
slug:g.slug, skillName:g.name});
|
||
});
|
||
if(!out.length) out.push({key:'g-none', _sep: f ? 'Aucun résultat' : 'Galerie vide'});
|
||
return out;
|
||
},
|
||
// ── Connecteurs (v7.55.0) ──
|
||
fetchConnectors(){
|
||
var self = this;
|
||
fetch('/api/agent/connectors').then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
self.connectors = d.connectors || [];
|
||
// garde la fiche ouverte alignée sur la liste rafraîchie
|
||
var sel = self.connector;
|
||
if(sel){
|
||
var found = null;
|
||
self.connectors.forEach(function(c){
|
||
if((sel.id != null && c.id === sel.id) || (sel.id == null && c.kind === sel.kind)) found = c;
|
||
});
|
||
self.connector = found;
|
||
}
|
||
}).catch(function(){ self.connectors = []; });
|
||
},
|
||
_connectorStatus(c){
|
||
var mark = c.status === 'ok' ? '✓ ' : (c.status === 'error' ? '✗ '
|
||
: (c.status === 'missing' ? '⚠ ' : '? '));
|
||
return mark + (c.detail || c.status) + (c.enabled ? '' : ' · désactivé');
|
||
},
|
||
_connectorItems(){
|
||
var self = this;
|
||
var out = (this.connectors || []).map(function(c){
|
||
return {key:'cn-' + (c.id != null ? c.id : c.kind), icon: c.builtin ? '🌐' : '🔌',
|
||
label: c.name, sub: self._connectorStatus(c),
|
||
action:'connector', connector:c, chev:true};
|
||
});
|
||
if(!out.length) out.push({key:'cn-none', _sep:'Aucun connecteur'});
|
||
out.push({key:'cn-sep', _sep:'Ajout'});
|
||
out.push({key:'cn-new', icon:'+', label:'Ajouter un connecteur personnalisé',
|
||
sub:'URL publique + clé API', action:'connector-new', chev:true});
|
||
return out;
|
||
},
|
||
_connectorActionItems(){
|
||
var c = this.connector;
|
||
if(!c) return [];
|
||
var items = [
|
||
{key:'cn-meta', _sep: (c.builtin ? 'Connecteur natif · ' : 'Connecteur · ') + c.name},
|
||
{key:'cn-probe', icon:'↻', label:'Tester le connecteur',
|
||
sub:'Appel de contrôle — status + détail', action:'connector-probe'}
|
||
];
|
||
if(!c.builtin){
|
||
items.push({key:'cn-toggle', icon: c.enabled ? '⏸' : '▶',
|
||
label: c.enabled ? 'Désactiver' : 'Activer',
|
||
sub:'Désactivé = lecture refusée par l\u2019outil', action:'connector-toggle'});
|
||
items.push({key:'cn-del', icon:'🗑️', label:'Supprimer le connecteur',
|
||
sub:'Supprime aussi sa clé', action:'connector-delete'});
|
||
}
|
||
return items;
|
||
},
|
||
_connectorRef(c){
|
||
c = c || this.connector;
|
||
return c ? (c.id != null ? String(c.id) : c.kind) : '';
|
||
},
|
||
connectorProbe(){
|
||
var self = this;
|
||
fetch('/api/agent/connectors/probe', {
|
||
method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||
body: JSON.stringify({connector: this._connectorRef()})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Test impossible.', true); return; }
|
||
self.toast('Connecteur : ' + res.d.status + ' — ' + (res.d.detail || ''));
|
||
self.fetchConnectors();
|
||
}).catch(function(){ self.toast('Test impossible (réseau).', true); });
|
||
},
|
||
connectorToggle(){
|
||
var self = this, c = this.connector;
|
||
if(!c || c.builtin || c.id == null) return;
|
||
fetch('/api/agent/connectors/' + c.id, {
|
||
method:'PATCH', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||
body: JSON.stringify({enabled: !c.enabled})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast('Bascule impossible.', true); return; }
|
||
self.toast(res.d.enabled ? 'Connecteur activé.' : 'Connecteur désactivé.');
|
||
self.fetchConnectors();
|
||
}).catch(function(){ self.toast('Bascule impossible (réseau).', true); });
|
||
},
|
||
connectorDelete(){
|
||
var self = this, c = this.connector;
|
||
if(!c || c.builtin || c.id == null) return;
|
||
if(!window.confirm('Supprimer le connecteur « ' + c.name + ' » ?')) return;
|
||
fetch('/api/agent/connectors/' + c.id, {method:'DELETE', headers:{'X-CSRF-Token': getCsrf()}})
|
||
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast('Suppression impossible.', true); return; }
|
||
self.toast('Connecteur supprimé.');
|
||
self.connector = null;
|
||
self.plusSection = 'connectors'; self.plusFocus = 0;
|
||
self.fetchConnectors();
|
||
}).catch(function(){ self.toast('Suppression impossible (réseau).', true); });
|
||
},
|
||
connectorCreate(){
|
||
var self = this, f = this.connectorForm;
|
||
if(!(f.name || '').trim() || !(f.url || '').trim()){
|
||
this.toast('Nom et URL requis.', true); return;
|
||
}
|
||
fetch('/api/agent/connectors', {
|
||
method:'POST', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||
body: JSON.stringify({name: f.name, url: f.url, secret: f.secret || ''})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, d:d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Création impossible.', true); return; }
|
||
self.toast('Connecteur « ' + res.d.name + ' » ajouté.');
|
||
self.connectorForm = {name:'', url:'', secret:''};
|
||
self.plusSection = 'connectors'; self.plusFocus = 0;
|
||
self.fetchConnectors();
|
||
}).catch(function(){ self.toast('Création impossible (réseau).', true); });
|
||
},
|
||
// ── Design System – Canevas (v7.53.0) ──
|
||
fetchCanvases(){
|
||
var self = this;
|
||
fetch('/board/api/page-templates').then(function(r){ return r.json(); })
|
||
.then(function(d){ self.canvases = d.templates || []; })
|
||
.catch(function(){ self.canvases = []; });
|
||
},
|
||
_canvasItems(){
|
||
return (this.canvases || []).map(function(t){
|
||
return {key: 'cv-' + (t.builtin ? ('b-' + t.key) : ('u-' + t.id)),
|
||
icon: t.icon || '📄', label: t.name,
|
||
sub: (t.description || (t.builtin ? 'Canevas intégré' : 'Canevas personnel')),
|
||
action: 'canvas', canvas: t, chev: true};
|
||
});
|
||
},
|
||
_canvasActionItems(){
|
||
var c = this.canvas;
|
||
if(!c) return [];
|
||
var pageId = this._openPageId();
|
||
return [
|
||
{key:'cv-meta', _sep: (c.builtin ? 'Canevas intégré · ' : 'Canevas personnel · ') + c.name},
|
||
{key:'cv-create', icon:'+', label:'Créer une page à partir du canevas',
|
||
sub:'Nouvelle page dans le workspace courant', action:'canvas-create'},
|
||
{key:'cv-insert', icon:'⇩', label:'Insérer dans le document ouvert',
|
||
sub: pageId ? 'Ajoute les blocs en fin de page' : 'Aucun document éditable ouvert',
|
||
action:'canvas-insert', disabled: !pageId},
|
||
{key:'cv-save', icon:'⭐', label:'Enregistrer le document ouvert comme canevas',
|
||
sub: pageId ? 'Réutilisable depuis ce menu' : 'Aucun document ouvert',
|
||
action:'canvas-save', disabled: !pageId}
|
||
];
|
||
},
|
||
// Id de la page ouverte dans l'éditeur (refId du contexte, sinon l'URL).
|
||
_openPageId(){
|
||
try{
|
||
var c = window.FlowDeckCtx && window.FlowDeckCtx.current();
|
||
if(c && c.kind === 'page' && c.refId != null && /^\d+$/.test(String(c.refId))) return String(c.refId);
|
||
}catch{ /* volontaire */ }
|
||
var m = /\/pages\/(\d+)/.exec(window.location.pathname);
|
||
return m ? m[1] : null;
|
||
},
|
||
_canvasUrl(suffix, t){
|
||
t = t || this.canvas;
|
||
return t.builtin
|
||
? ('/board/api/page-templates/0/' + suffix + '?key=' + encodeURIComponent(t.key))
|
||
: ('/board/api/page-templates/' + t.id + '/' + suffix);
|
||
},
|
||
canvasCreate(){
|
||
var self = this, c = this.canvas;
|
||
if(!c) return;
|
||
// le builtin se choisit par `key` DANS LE BODY (route /use), pas en query
|
||
fetch(this._canvasUrl('use'), {
|
||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||
body: JSON.stringify(c.builtin ? {key: c.key} : {})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Création impossible.', true); return; }
|
||
self.toast('Page créée : « ' + ((res.d && res.d.title) || c.name) + ' » (Workspace).');
|
||
self.plusSection = 'canvases'; self.plusFocus = 0;
|
||
}).catch(function(){ self.toast('Création impossible (réseau).', true); });
|
||
},
|
||
canvasInsert(){
|
||
var self = this, c = this.canvas;
|
||
if(!c) return;
|
||
fetch(this._canvasUrl('blocks')).then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
var E = window.E;
|
||
if(!E || !E.blocks || !d || !Array.isArray(d.blocks)){
|
||
self.toast('Aucun document éditable ouvert.', true); return;
|
||
}
|
||
if(window.ensureBlockIds) window.ensureBlockIds(d.blocks);
|
||
try{
|
||
if(E.sync) E.sync();
|
||
var empty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph'
|
||
&& !(E.blocks[0].content || '').trim();
|
||
E.blocks = empty ? d.blocks : E.blocks.concat(d.blocks);
|
||
E.dirty = true;
|
||
if(E.autoSave) E.autoSave();
|
||
if(E.render) E.render();
|
||
self.toast('Canevas inséré dans le document.');
|
||
}catch{ self.toast('Insertion impossible.', true); }
|
||
}).catch(function(){ self.toast('Lecture du canevas impossible.', true); });
|
||
},
|
||
canvasSave(){
|
||
var self = this, c = this.canvas;
|
||
var pageId = this._openPageId();
|
||
if(!c || !pageId) return;
|
||
var def = (this.activeContext && this.activeContext.label) || c.name || 'Mon canevas';
|
||
var name = window.prompt('Nom du canevas à enregistrer :', def);
|
||
if(!name || !name.trim()) return;
|
||
name = name.trim();
|
||
fetch('/board/api/page-templates', {
|
||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: name, page_id: Number(pageId)})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Enregistrement impossible.', true); return; }
|
||
self.toast('Canevas enregistré : « ' + name + ' »');
|
||
self.fetchCanvases();
|
||
}).catch(function(){ self.toast('Enregistrement impossible (réseau).', true); });
|
||
},
|
||
_plusSelectable(){
|
||
var out = [];
|
||
this.plusItems.forEach(function(it, i){ if(!it.disabled && !it._sep) out.push(i); });
|
||
return out;
|
||
},
|
||
movePlusFocus(delta){
|
||
var sel = this._plusSelectable();
|
||
if(!sel.length) return;
|
||
var idx = sel.indexOf(this.plusFocus);
|
||
if(idx < 0) idx = delta > 0 ? -1 : 0;
|
||
this.plusFocus = sel[(idx + delta + sel.length) % sel.length];
|
||
this._scrollPlusItem();
|
||
},
|
||
_scrollPlusItem(){
|
||
var menu = this.$refs && this.$refs.plusMenu;
|
||
if(!menu) return;
|
||
var el = menu.querySelector('.fd-mention-item.focus');
|
||
if(el && el.scrollIntoView){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
|
||
},
|
||
plusAction(it){
|
||
if(!it || it.disabled || it._sep) return;
|
||
if(it.action === 'files'){ this.plusSection = 'files'; this.plusFocus = 0; return; }
|
||
if(it.action === 'search'){ this.closePlus(); this.openMentionPicker('+'); return; }
|
||
if(it.action === 'browse'){
|
||
this.plusSection = 'browse'; this.plusCrumb = []; this.plusLoadBrowse(null); return;
|
||
}
|
||
// ── Compétences (v7.52.0) ──
|
||
if(it.action === 'memory'){ this.toggleMemory(); return; }
|
||
if(it.action === 'skills'){ this.plusSection = 'skills'; this.plusFocus = 0; return; }
|
||
if(it.action === 'skill'){ this.closePlus(); this.useSuggestion(it.suggestion); return; }
|
||
if(it.action === 'skills-manage'){
|
||
this.plusSection = 'skills-manage'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'skills-gallery'){
|
||
this.galleryFilter = ''; this.fetchSkills();
|
||
this.plusSection = 'skills-gallery'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'skill-new'){
|
||
this.skillForm = {id:null, name:'', description:'', prompt_template:''};
|
||
this.plusSection = 'skills-edit'; this.plusFocus = -1; return;
|
||
}
|
||
if(it.action === 'skill-edit'){
|
||
var sk = null, wanted = it.skillId;
|
||
(this.skills || []).forEach(function(x){ if(x.id === wanted) sk = x; });
|
||
if(!sk) return;
|
||
this.skillForm = {id: sk.id, name: sk.name, description: sk.description || '',
|
||
prompt_template: sk.prompt_template || ''};
|
||
this.plusSection = 'skills-edit'; this.plusFocus = -1; return;
|
||
}
|
||
if(it.action === 'skill-install'){
|
||
this.installGallerySkill(it.slug, it.icon, it.skillName); return;
|
||
}
|
||
if(it.action === 'skill-import'){
|
||
if(this.$refs.skillImport) this.$refs.skillImport.click();
|
||
return;
|
||
}
|
||
// ── Connecteurs (v7.55.0) ──
|
||
if(it.action === 'connectors'){
|
||
this.fetchConnectors(); this.plusSection = 'connectors'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'connector'){
|
||
this.connector = it.connector; this.plusSection = 'connector'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'connector-new'){
|
||
this.connectorForm = {name:'', url:'', secret:''};
|
||
this.plusSection = 'connector-new'; this.plusFocus = -1; return;
|
||
}
|
||
if(it.action === 'connector-probe'){ this.connectorProbe(); return; }
|
||
if(it.action === 'connector-toggle'){ this.connectorToggle(); return; }
|
||
if(it.action === 'connector-delete'){ this.connectorDelete(); return; }
|
||
// ── Design System – Canevas (v7.53.0) ──
|
||
if(it.action === 'canvases'){
|
||
this.fetchCanvases(); this.plusSection = 'canvases'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'canvas'){
|
||
this.canvas = it.canvas; this.plusSection = 'canvas'; this.plusFocus = 0; return;
|
||
}
|
||
if(it.action === 'canvas-create'){ this.canvasCreate(); return; }
|
||
if(it.action === 'canvas-insert'){ this.canvasInsert(); return; }
|
||
if(it.action === 'canvas-save'){ this.canvasSave(); return; }
|
||
if(it._folder){ // entrer dans un sous-dossier
|
||
this.plusCrumb.push({id: it._folder, name: it.label});
|
||
this.plusLoadBrowse(it._folder);
|
||
return;
|
||
}
|
||
if(it.token){ this._mentionFrom = null; this._atNode = null; this.closePlus(); this.pickMention(it); return; }
|
||
this.closePlus(); // sections futures (phases 2-8)
|
||
},
|
||
plusBack(){
|
||
var sec = this.plusSection;
|
||
if(sec === 'browse' && this.plusCrumb.length){
|
||
this.plusCrumb.pop();
|
||
this.plusLoadBrowse(this.plusCrumb.length ? this.plusCrumb[this.plusCrumb.length - 1].id : null);
|
||
return;
|
||
}
|
||
if(sec === 'skills-edit'){ this.plusSection = 'skills-manage'; this.plusFocus = 0; return; }
|
||
if(sec === 'skills-manage' || sec === 'skills-gallery'){
|
||
this.plusSection = 'skills'; this.plusFocus = 0; return;
|
||
}
|
||
if(sec === 'canvas'){ this.plusSection = 'canvases'; this.plusFocus = 0; return; }
|
||
if(sec === 'connector' || sec === 'connector-new'){
|
||
this.plusSection = 'connectors'; this.plusFocus = 0; return;
|
||
}
|
||
this.plusSection = 'root';
|
||
this.plusFocus = 0;
|
||
},
|
||
plusGoto(i){ // fil d'Ariane : -1 = racine
|
||
this.plusCrumb = i < 0 ? [] : this.plusCrumb.slice(0, i + 1);
|
||
this.plusLoadBrowse(i < 0 ? null : this.plusCrumb[this.plusCrumb.length - 1].id);
|
||
},
|
||
plusLoadBrowse(parentId){
|
||
// Un niveau par appel : /api/nav/menu (dossier = icon 'folder').
|
||
var self = this;
|
||
this.plusLoading = true;
|
||
fetch('/api/nav/menu' + (parentId ? ('?parent_id=' + parentId) : ''))
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
var items = [];
|
||
// icon de /api/nav/menu = nom SVG ('folder'|'edit'|'image'|'file') → emoji
|
||
var ICONS = {folder:'📁', edit:'📄', image:'🖼️', file:'📎'};
|
||
(d.items || []).forEach(function(x){
|
||
var isFolder = x.icon === 'folder';
|
||
items.push({
|
||
key: 'br-' + x.id, icon: ICONS[x.icon] || '📄',
|
||
label: x.name, sub: isFolder ? 'Dossier — ouvrir' : 'Document — épingler',
|
||
token: (isFolder ? 'folder:' : 'document:') + x.id,
|
||
type: isFolder ? 'folder' : 'document', id: x.id,
|
||
_folder: isFolder ? x.id : null
|
||
});
|
||
});
|
||
if(self.plusCrumb.length){
|
||
var cur = self.plusCrumb[self.plusCrumb.length - 1];
|
||
items.unshift({key: 'pin', icon: '📌',
|
||
label: 'Épingler le dossier « ' + cur.name + ' »',
|
||
sub: 'Ajoute le dossier entier au contexte',
|
||
token: 'folder:' + cur.id, type: 'folder', id: cur.id});
|
||
}
|
||
self.plusBrowse = items;
|
||
self.plusLoading = false;
|
||
self.plusFocus = items.length ? 0 : -1;
|
||
self.$nextTick(function(){ self._scrollPlusItem(); });
|
||
})
|
||
.catch(function(){ self.plusBrowse = []; self.plusLoading = false; self.plusFocus = -1; });
|
||
},
|
||
// ── Gérer les compétences : CRUD / export / import (v7.52.0) ──
|
||
saveSkill(){
|
||
var self = this, f = this.skillForm;
|
||
if(!(f.name || '').trim()){ this.toast('Le nom est requis.', true); return; }
|
||
fetch(f.id ? '/api/agent/skills/' + f.id : '/api/agent/skills', {
|
||
method: f.id ? 'PATCH' : 'POST',
|
||
headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||
body: JSON.stringify({name: f.name, description: f.description || '',
|
||
prompt_template: f.prompt_template || ''})
|
||
}).then(function(r){
|
||
return r.json().then(function(d){ return {ok: r.ok, d: d}; });
|
||
}).then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Enregistrement impossible.', true); return; }
|
||
self.toast('Skill enregistré.');
|
||
self.fetchSkills();
|
||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||
}).catch(function(){ self.toast('Enregistrement impossible (réseau).', true); });
|
||
},
|
||
deleteSkill(){
|
||
var self = this, f = this.skillForm;
|
||
if(!f.id) return;
|
||
if(!window.confirm('Supprimer le skill « ' + f.name + ' » ?')) return;
|
||
fetch('/api/agent/skills/' + f.id, {method: 'DELETE', headers: {'X-CSRF-Token': getCsrf()}})
|
||
.then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Suppression impossible.', true); return; }
|
||
self.toast('Skill supprimé.');
|
||
// le chip épinglé ne doit pas survivre au skill
|
||
self.contextChips = self.contextChips.filter(function(ch){
|
||
return !(ch.kind === 'skill' && ch.skillId === f.id);
|
||
});
|
||
self.skillForm = {id: null, name: '', description: '', prompt_template: ''};
|
||
self.fetchSkills();
|
||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||
}).catch(function(){ self.toast('Suppression impossible (réseau).', true); });
|
||
},
|
||
exportSkill(){
|
||
var self = this, f = this.skillForm;
|
||
if(!f.id) return;
|
||
fetch('/api/agent/skills/' + f.id + '/export')
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(doc){
|
||
var blob = new Blob([JSON.stringify(doc, null, 2)], {type: 'application/json'});
|
||
var a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = String((doc && doc.name) || 'skill').replace(/[^\w.-]+/g, '-') + '.json';
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||
setTimeout(function(){ URL.revokeObjectURL(a.href); }, 1000);
|
||
}).catch(function(){ self.toast('Export impossible.', true); });
|
||
},
|
||
importSkillFile(ev){
|
||
var self = this;
|
||
var input = ev.target, file = input.files && input.files[0];
|
||
if(!file) return;
|
||
file.text().then(function(txt){
|
||
var payload;
|
||
try{ payload = JSON.parse(txt); }
|
||
catch{ self.toast('Fichier JSON invalide.', true); return; }
|
||
return fetch('/api/agent/skills/import', {
|
||
method: 'POST', headers: {'X-CSRF-Token': getCsrf(), 'Content-Type': 'application/json'},
|
||
body: JSON.stringify({payload: payload, overwrite: true})
|
||
}).then(function(r){ return r.json().then(function(d){ return {ok: r.ok, d: d}; }); })
|
||
.then(function(res){
|
||
if(!res.ok){ self.toast((res.d && res.d.detail) || 'Import impossible.', true); return; }
|
||
self.toast('Skill importé : ' + ((res.d && res.d.name) || '') + ' (pas de doublon).');
|
||
self.fetchSkills();
|
||
self.plusSection = 'skills-manage'; self.plusFocus = 0;
|
||
});
|
||
}).catch(function(){ self.toast('Lecture du fichier impossible.', true); })
|
||
.finally(function(){ input.value = ''; });
|
||
},
|
||
openMentionPicker(from){
|
||
this.plusOpen = false;
|
||
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(){
|
||
var ws = (window.appState && window.appState.localWsId) || 0;
|
||
fetch('/api/agent/mentions?q=' + encodeURIComponent(q) + '&workspace_id=' + ws)
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
var flat = [], gi = 0;
|
||
(d.sections || []).forEach(function(grp){
|
||
var items = grp.items || [];
|
||
if(!items.length) return;
|
||
flat.push({_sep: grp.label || '', key: 'sep-' + (grp.key || gi), _kind: 'sep'});
|
||
items.forEach(function(it){ it._i = gi++; flat.push(it); });
|
||
});
|
||
self.mentionItems = flat;
|
||
self.mentionLoad = false;
|
||
var sel = self._selectableMentionItems();
|
||
if(sel.length) self.mentionFocus = sel[0]._i;
|
||
self.$nextTick(function(){ self._scrollMentionItem(); });
|
||
})
|
||
.catch(function(){ self.mentionItems = []; self.mentionLoad = false; });
|
||
}, q ? 180 : 0);
|
||
},
|
||
get hasMentionItems(){ return (this.mentionItems || []).some(function(it){ return !it._sep; }); },
|
||
_selectableMentionItems(){
|
||
return (this.mentionItems || []).filter(function(it){ return it && it._sep !== true && it._kind !== 'sep'; });
|
||
},
|
||
setMentionFocus(i){
|
||
this.mentionFocus = i;
|
||
this._scrollMentionItem();
|
||
},
|
||
_scrollMentionItem(){
|
||
var menu = this.$refs && this.$refs.mentionMenu;
|
||
if(!menu) return;
|
||
var el = menu.querySelector('.fd-mention-item.focus');
|
||
if(el && el.scrollIntoView){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
|
||
},
|
||
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();
|
||
},
|
||
chipHref(chip){
|
||
if(!chip || chip.kind !== 'mention') return null;
|
||
var id;
|
||
if(chip.refId != null) id = Number(chip.refId);
|
||
else {
|
||
var tk = String(chip.token || '').split(':');
|
||
if(tk.length === 2 && /^\d+$/.test(tk[1])) id = Number(tk[1]);
|
||
}
|
||
if(id == null || isNaN(id)) return null;
|
||
if(chip.type === 'collection') return '/db/' + id;
|
||
return '/pages/' + id; // document & pages de collection s'ouvrent dans l'éditeur
|
||
},
|
||
chipAction(chip){
|
||
if(!chip || chip.kind !== 'mention') return '';
|
||
return chip.type === 'collection' ? 'Ouvrir la base de données' : 'Ouvrir le document';
|
||
},
|
||
openChip(chip){
|
||
var href = this.chipHref(chip);
|
||
if(!href){ this.toast('Impossible d\u2019ouvrir cet élément.', true); return; }
|
||
window.location.assign(href);
|
||
},
|
||
onInput(){
|
||
this.resizeInput();
|
||
this.slashDismiss = false;
|
||
if(this.plusOpen) this.closePlus(); // tape = on écrit le message, le menu se referme
|
||
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){
|
||
var sel = this._selectableMentionItems();
|
||
if(!sel.length) return;
|
||
var n = sel.length;
|
||
var idx = sel.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 = sel[idx]._i;
|
||
this._scrollMentionItem();
|
||
},
|
||
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;
|
||
var menu = this.$refs && this.$refs.cmdMenu;
|
||
if(menu){
|
||
var el = menu.querySelector('.fd-agent-cmd-item.focus');
|
||
if(el && el.offsetTop != null){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
|
||
}
|
||
},
|
||
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 { /* volontaire */ }
|
||
}
|
||
if(key === 'Escape'){
|
||
if(this.plusOpen){ e.preventDefault(); e.stopPropagation(); this.closePlus(); return; }
|
||
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.plusOpen){ e.preventDefault(); this.movePlusFocus(delta); return; }
|
||
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 le menu + ouvert
|
||
if(this.plusOpen){
|
||
if(key === 'Enter' || key === 'Tab'){
|
||
e.preventDefault();
|
||
var psel = this._plusSelectable();
|
||
if(!psel.length) return;
|
||
var pidx = psel.indexOf(this.plusFocus) >= 0 ? this.plusFocus : psel[0];
|
||
this.plusAction(this.plusItems[pidx]);
|
||
return;
|
||
}
|
||
}
|
||
// Entrée / Tab avec un menu ouvert (skills / mentions)
|
||
if(this.mentionOpen){
|
||
var mSel = this._selectableMentionItems();
|
||
if((key === 'Enter' || key === 'Tab') && mSel.length){
|
||
e.preventDefault();
|
||
var mi = mSel.find(function(it){ return it._i === this.mentionFocus; }.bind(this)) || mSel[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 ──
|
||
// Render agent markdown responses (headings, tables, code, lists, …) to
|
||
// HTML so the panel displays content like Notion (no raw `##` / `**`).
|
||
renderMarkdown(md){
|
||
function escHtml(s){ return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
function inline(t){
|
||
t = escHtml(t);
|
||
return t
|
||
.replace(/`([^`]+)`/g,'<code>$1</code>')
|
||
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
|
||
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
|
||
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
|
||
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
|
||
}
|
||
if(!md) return '';
|
||
var src = String(md);
|
||
// Trim leading/trailing blank lines to avoid stray <p></p>
|
||
var lines = src.split(/\r?\n/);
|
||
var html = '';
|
||
var i = 0;
|
||
function flush(block){
|
||
if(!block) return '';
|
||
if(block.type === 'ul' || block.type === 'ol'){
|
||
var tag = block.type === 'ul' ? 'ul' : 'ol';
|
||
return '<'+tag+' class="fd-md-list">' + block.items.map(function(it){
|
||
var li = inline(it.text);
|
||
if(it.checked != null){
|
||
return '<li><input type="checkbox" disabled'+(it.checked?' checked':'')+'> '+li+'</li>';
|
||
}
|
||
return '<li>'+li+'</li>';
|
||
}).join('') + '</'+tag+'>';
|
||
}
|
||
if(block.type === 'quote'){
|
||
return '<blockquote class="fd-md-quote">'+block.items.map(function(t){ return '<p>'+inline(t)+'</p>'; }).join('')+'</blockquote>';
|
||
}
|
||
if(block.type === 'p'){
|
||
return block.text ? '<p>'+inline(block.text)+'</p>' : '';
|
||
}
|
||
return '';
|
||
}
|
||
while(i < lines.length){
|
||
var raw = lines[i];
|
||
var t = raw.trim();
|
||
// Fenced code block
|
||
if(t.startsWith('```')){
|
||
var code = [];
|
||
i++;
|
||
while(i < lines.length && !lines[i].trim().startsWith('```')){
|
||
code.push(lines[i]); i++;
|
||
}
|
||
i++; // skip closing ```
|
||
html += '<pre class="fd-md-code"><code>' + escHtml(code.join('\n')) + '</code></pre>';
|
||
continue;
|
||
}
|
||
// Table: | a | b | followed by | --- | --- |
|
||
if(t.startsWith('|') && i+1 < lines.length && /^\|[\s\-:|]+\|?$/.test(lines[i+1].trim())){
|
||
var headerCells = t.replace(/^\||\|$/g,'').split('|').map(function(c){return c.trim();});
|
||
var rows = [];
|
||
var j = i+2;
|
||
while(j < lines.length && lines[j].trim().startsWith('|')){
|
||
rows.push(lines[j].trim().replace(/^\||\|$/g,'').split('|').map(function(c){return c.trim();}));
|
||
j++;
|
||
}
|
||
var thead = '<tr>'+headerCells.map(function(c){return '<th>'+inline(c)+'</th>';}).join('')+'</tr>';
|
||
var tb = rows.map(function(r){
|
||
var cells = [];
|
||
for(var k=0;k<headerCells.length;k++) cells.push(r[k]||'');
|
||
return '<tr>'+cells.map(function(c){return '<td>'+inline(c)+'</td>';}).join('')+'</tr>';
|
||
}).join('');
|
||
html += '<div class="fd-md-table-wrap"><table class="fd-md-table"><thead>'+thead+'</thead><tbody>'+tb+'</tbody></table></div>';
|
||
i = j;
|
||
continue;
|
||
}
|
||
// Horizontal rule
|
||
if(/^(-{3,}|\*{3,}|_{3,})$/.test(t)){ html += '<hr class="fd-md-hr">'; i++; continue; }
|
||
// Blockquote (accumulate consecutive)
|
||
if(t.startsWith('> ')){
|
||
var q = {type:'quote', items:[]};
|
||
while(i < lines.length && lines[i].trim().startsWith('> ')){
|
||
q.items.push(lines[i].trim().substring(2)); i++;
|
||
}
|
||
html += flush(q); continue;
|
||
}
|
||
// Lists (accumulate consecutive ul/ol, incl. checkboxes)
|
||
if(/^[-*+] /.test(t) || /^\d+\. /.test(t)){
|
||
var ul = /^[-*+] /.test(t) ? 'ul' : 'ol';
|
||
var b = {type: ul, items: []};
|
||
while(i < lines.length){
|
||
var lt = lines[i].trim();
|
||
if(ul === 'ul' && /^[-*+] /.test(lt)){
|
||
var mt = lt.substring(2);
|
||
var chk = mt.match(/^\[([ xX])\]\s*/);
|
||
if(chk){ b.items.push({text: mt.substring(3), checked: chk[1]!==' '}); }
|
||
else b.items.push({text: mt});
|
||
i++;
|
||
} else if(ul === 'ol' && /^\d+\. /.test(lt)){
|
||
b.items.push({text: lt.replace(/^\d+\. /,'')}); i++;
|
||
} else break;
|
||
}
|
||
html += flush(b); continue;
|
||
}
|
||
// Headings
|
||
var hm = t.match(/^(#{1,4})\s+(.*)$/);
|
||
if(hm){ html += '<h'+hm[1].length+'>'+inline(hm[2])+'</h'+hm[1].length+'>'; i++; continue; }
|
||
// Paragraph (accumulate consecutive non-empty non-special)
|
||
if(t){
|
||
var para = [];
|
||
while(i < lines.length){
|
||
var pt = lines[i].trim();
|
||
if(!pt) break;
|
||
if(/^(```|#{1,4}\s|> |[-*+] |\d+\. |\||-{3,}$)/.test(pt)) break;
|
||
para.push(pt); i++;
|
||
}
|
||
html += para.map(function(p){ return '<p>'+inline(p)+'</p>'; }).join('');
|
||
continue;
|
||
}
|
||
i++;
|
||
}
|
||
return html;
|
||
},
|
||
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{ 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: {'X-CSRF-Token': getCsrf(), '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); });
|
||
},
|
||
|
||
toggleSteps(m){
|
||
if(!m) return;
|
||
m.stepsOpen = !m.stepsOpen;
|
||
this.$nextTick(function(){ this.scrollBottom(); });
|
||
},
|
||
toggleStep(s){
|
||
if(!s) return;
|
||
s.open = !s.open;
|
||
},
|
||
|
||
// ── Send / SSE ──
|
||
async send(){
|
||
var self = this;
|
||
if(self.streaming) return;
|
||
var text = (self.input || '').trim();
|
||
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();
|
||
var skillIds = [];
|
||
(self.contextChips || []).forEach(function(ch){ if(ch.kind==='skill' && ch.skillId) skillIds.push(ch.skillId); });
|
||
var hasSkills = skillIds.length > 0 || (self.contextChips || []).some(function(ch){ return ch.kind==='skill'; });
|
||
if(!text){
|
||
if(ment.length){ text = 'Analyse le contexte référencé.'; }
|
||
else if(!hasSkills){ return; }
|
||
}
|
||
try { await self.ensureConversation(); } catch{ return; }
|
||
var rawText = text;
|
||
// La requête est composée EN ARRIÈRE-PLAN (hors de la boîte d'édition) :
|
||
// contexte1 + contexte2 + skill-1 + skill-2 + texte.
|
||
var composed = this._composePrompt(rawText);
|
||
var skillLabels = (self.contextChips || []).filter(function(ch){ return ch.kind==='skill'; })
|
||
.map(function(ch){ return ch.label; });
|
||
var displayText = rawText || (ment.length ? 'Analyse le contexte référencé.'
|
||
: ('Skills : ' + skillLabels.join(', ')));
|
||
self.clearComposer();
|
||
self.errorMsg = '';
|
||
self.mentionOpen = false;
|
||
|
||
// Une demande d'écriture avec document ouvert et SANS skill/contexte épinglé →
|
||
// génération headless avec Appliquer/Rejeter. Dès qu'on épingle un skill ou un
|
||
// contexte, on passe par l'agent (outils + skills + mentions).
|
||
if(rawText && self.activeContext && self.activeContext.kind === 'page'
|
||
&& self._isContentRequest(rawText) && !self.contextChips.length){
|
||
self.refreshContext();
|
||
return self.runDocAction({prompt: rawText, label: rawText, mode: self._docModeFor(rawText)});
|
||
}
|
||
|
||
self.streaming = true;
|
||
self.liveReasoning = '…';
|
||
self._livePh = null;
|
||
var runSteps = [];
|
||
var ph = null;
|
||
function ensurePlaceholder(finalContent){
|
||
if(ph) return ph;
|
||
ph = {id:'r-'+Date.now(), role:'assistant', generating: !finalContent,
|
||
content: finalContent || '', steps: [], stepsOpen: true,
|
||
model: self.autoLabel || ''};
|
||
self.messages.push(ph);
|
||
self._livePh = ph;
|
||
return ph;
|
||
}
|
||
function addStep(s){
|
||
runSteps.push(s);
|
||
if(ph){ ph.steps = runSteps.slice(); ph.generating = true; }
|
||
if(self._livePh){ self._livePh.steps = runSteps.slice(); }
|
||
}
|
||
self.messages.push({id:'u-'+Date.now(), role:'user', content: displayText});
|
||
self.scrollBottom();
|
||
|
||
var payload = {message: composed};
|
||
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;
|
||
if(skillIds.length) payload.skill_ids = skillIds;
|
||
|
||
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
|
||
method:'POST',
|
||
headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'},
|
||
body: JSON.stringify(payload)
|
||
}).then(function(resp){
|
||
if(!resp.ok){ return resp.json().then(function(j){ throw new Error(j.detail || ('HTTP '+resp.status)); }); }
|
||
var reader = resp.body.getReader();
|
||
var dec = new TextDecoder();
|
||
var respBuf = '';
|
||
function read(){
|
||
return reader.read().then(function(res){
|
||
if(res.done){ return; }
|
||
respBuf += dec.decode(res.value);
|
||
var parts = respBuf.split('\n\n');
|
||
respBuf = parts.pop();
|
||
parts.forEach(function(p){
|
||
var line = p.replace(/^data:\s*/, '').trim();
|
||
if(!line) return;
|
||
try{
|
||
var ev = JSON.parse(line);
|
||
if(ev.type==='reasoning'){
|
||
// A thought: collapsed by default, expandable to its full text.
|
||
addStep({type:'thought', title:agentPanel._thoughtTitle(ev.content),
|
||
content:(ev.content||'').replace(/\s+$/,''), open:false});
|
||
}
|
||
else if(ev.type==='action'){
|
||
addStep(agentPanel._actionStep(ev));
|
||
}
|
||
else if(ev.type==='final'){
|
||
var fph = ensurePlaceholder(ev.content);
|
||
fph.generating = false;
|
||
fph.content = ev.content;
|
||
fph.steps = runSteps.slice();
|
||
fph.stepsOpen = false;
|
||
fph.model = self.autoLabel || '';
|
||
if(window.__fdInsertAI){ try{ var h=window.__fdInsertAI; window.__fdInsertAI=null; h(ev.content||''); }catch { /* volontaire */ } }
|
||
}
|
||
else if(ev.type==='notice'){ self.notice = ev.message; }
|
||
else if(ev.type==='error'){
|
||
var eph = ensurePlaceholder();
|
||
eph.generating = false;
|
||
eph.content = '⚠️ ' + ev.message;
|
||
eph.steps = runSteps.slice();
|
||
self.errorMsg = ev.message;
|
||
}
|
||
}catch { /* volontaire */ }
|
||
});
|
||
self.scrollBottom();
|
||
return read();
|
||
});
|
||
}
|
||
return read();
|
||
}).catch(function(err){ self.errorMsg = (err && err.message) ? err.message : 'Erreur agent'; })
|
||
.finally(function(){
|
||
self.streaming = false; self.liveReasoning=''; self._livePh = null;
|
||
self.fetchConversationsRefresh();
|
||
});
|
||
},
|
||
fetchConversationsRefresh(){
|
||
var self = this;
|
||
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){
|
||
self.conversations = d.conversations || [];
|
||
// keep the open conversation in sync so its auto-updated title shows
|
||
// immediately in the header and in a later History screen.
|
||
var cur = self.currentConv;
|
||
if(cur && cur.id){
|
||
var match = self.conversations.find(function(c){ return c.id === cur.id; });
|
||
if(match){ cur.title = match.title; cur.updated_at = match.updated_at; }
|
||
}
|
||
}).catch(function(){});
|
||
},
|
||
scrollBottom(){
|
||
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
|
||
}
|
||
};
|
||
}
|
||
|
||
document.addEventListener('alpine:init', function(){
|
||
Alpine.data('agentPanel', agentPanel);
|
||
});
|
||
window.agentPanel = agentPanel;
|
||
// Plain-text helper for the history preview (strip markdown noise).
|
||
agentPanel._strip = function(t){
|
||
if(!t) return '';
|
||
return String(t)
|
||
.replace(/```[\s\S]*?```/g, ' [code] ')
|
||
.replace(/[#*_>`~]/g, '')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
.slice(0, 280);
|
||
};
|
||
// Short title for a reasoning thought (first non-empty line, truncated).
|
||
agentPanel._thoughtTitle = function(text){
|
||
var s = (text || '').trim();
|
||
if(!s) return 'Réflexion';
|
||
// Skip leading blank lines and get first meaningful line
|
||
var lines = s.split(/\r?\n/);
|
||
var first = '';
|
||
for(var i=0;i<lines.length;i++){
|
||
var ln = lines[i].trim();
|
||
if(ln){ first = ln; break; }
|
||
}
|
||
if(!first) return 'Réflexion';
|
||
first = first.replace(/^#+\s*/, ''); // strip markdown heading prefix
|
||
return first.length <= 72 ? first : first.slice(0,72).trimEnd() + '…';
|
||
};
|
||
// Past-tense label for each known agent action.
|
||
agentPanel._actionLabel = {
|
||
read_workspaces:'Espaces de travail listés', search_workspace:'Recherche effectuée',
|
||
read_document:'Document lu', read_collection:'Collection lue', read_page:'Page lue',
|
||
create_document:'Document créé', create_collection:'Collection créée',
|
||
create_page:'Page créée', create_view:'Vue créée', add_property:'Propriété ajoutée',
|
||
add_relation:'Relation ajoutée', update_page:'Page mise à jour',
|
||
write_blocks:'Contenu écrit', delete_document:'Document supprimé',
|
||
delete_page:'Page supprimée', delete_collection:'Collection supprimée',
|
||
apply_template:'Template appliqué', create_sub_item:'Sous-élément créé',
|
||
add_dependency:'Dépendance ajoutée', read_gitea_issues:'Issues Gitea lues',
|
||
create_gitea_issue:'Issue Gitea créée', sync_gitea:'Issues Gitea synchronisées',
|
||
web_search:'Recherche web effectuée', fetch_url:'Page web lue',
|
||
search_code:'Recherche GitHub effectuée'
|
||
};
|
||
// Build an action step object from an SSE event or a persisted action row.
|
||
agentPanel._actionStep = function(ev){
|
||
var ok = ev.status !== 'error';
|
||
var tool = String(ev.tool || '');
|
||
var label = agentPanel._actionLabel[tool] || tool.replace(/_/g,' ');
|
||
var extra = '';
|
||
if(ok && ev.target_id != null && ev.target_id !== '' && ev.target_id !== 0){
|
||
extra = ' → #' + ev.target_id;
|
||
}
|
||
if(ok && ev.message && ev.message.length <= 46){
|
||
extra += (extra ? ' — ' : ' · ') + ev.message;
|
||
}
|
||
return {
|
||
type:'action', tool:tool,
|
||
status: ok ? 'success' : 'error',
|
||
label: label + extra,
|
||
detail: ok ? '' : (ev.detail || ev.message || '')
|
||
};
|
||
};
|
||
|
||
// ═══════════ Context reader — document (page editor) ═══════════
|
||
window.fdReadPageContext = function(){
|
||
try{
|
||
var tEl = document.getElementById('_titleEl');
|
||
if(!tEl) return '';
|
||
var title = (tEl.textContent || '').trim();
|
||
var ct = document.getElementById('_blocksCt');
|
||
var text = '';
|
||
if(ct){
|
||
var kids = ct.children || [];
|
||
for(var i = 0; i < kids.length; i++){
|
||
var el = kids[i];
|
||
if(!el || !el.textContent) continue;
|
||
var t = el.textContent.replace(/\s+/g, ' ').trim();
|
||
if(t) text += t + '\n';
|
||
}
|
||
text = text.trim();
|
||
}
|
||
if(!title && !text) return '';
|
||
var out = title ? ('Titre du document : ' + title) : 'Document ouvert';
|
||
if(text) out += '\n\n' + text;
|
||
return out.slice(0, 9000);
|
||
}catch{ return ''; }
|
||
};
|
||
if(!window.fdGetPageContext) window.fdGetPageContext = window.fdReadPageContext;
|
||
|
||
// ═══════════ FlowDeckCtx — universal context detection ═══════════
|
||
// Priority: (1) an explicit marker set by a screen (window.fdViewContext /
|
||
// 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{ return null; }
|
||
}
|
||
function labelFromTitle(){
|
||
try{
|
||
var t = document.title || '';
|
||
t = t.replace(/^\s*FlowDeck\s*-\s*/i, '');
|
||
t = t.replace(/\s*[-|]\s*FlowDeck\s*$/i, '');
|
||
if(t && t.toLowerCase() !== 'home' && t.toLowerCase() !== 'flowdeck') return t;
|
||
}catch { /* volontaire */ }
|
||
return '';
|
||
}
|
||
function docContext(){
|
||
try{
|
||
if(window.fdGetPageContext){
|
||
var raw = String(window.fdGetPageContext() || '').trim();
|
||
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';
|
||
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 { /* volontaire */ }
|
||
return null;
|
||
}
|
||
function marker(){
|
||
try{
|
||
var m = window.fdViewContext;
|
||
if(m && (m.label || m.detail)){
|
||
var label = m.label || m.detail;
|
||
return {kind: m.kind || 'section', icon: m.icon || '📍', label: String(label), text: m.text || '', key: 'marker'};
|
||
}
|
||
var b = document.body;
|
||
if(b && b.getAttribute && b.getAttribute('data-fd-context')){
|
||
var lbl = b.getAttribute('data-fd-context');
|
||
return {kind:'section', icon:'📍', label:lbl, text:'', key:'marker'};
|
||
}
|
||
}catch { /* volontaire */ }
|
||
return null;
|
||
}
|
||
function byPath(){
|
||
var p = window.location.pathname || '/';
|
||
var icon = '📍';
|
||
var label = '';
|
||
var kind = 'section';
|
||
var t;
|
||
if(p === '/' || p === '/dashboard'){ icon='🏠'; label='Accueil'; kind='home'; }
|
||
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] || '';
|
||
label = tab ? ('Bibliothèque — ' + tab.charAt(0).toUpperCase() + tab.slice(1)) : 'Bibliothèque';
|
||
}
|
||
else if(/^\/trash/.test(p)){ icon='🗑️'; label='Corbeille'; kind='trash'; }
|
||
else if(/^\/my-tasks/.test(p)){ icon='✅'; label='My Tasks'; kind='tasks'; }
|
||
else if(/^\/workspaces/.test(p)){ icon='🗂️'; label='Espaces de travail'; kind='workspaces'; }
|
||
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='🗄️'; 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'; }
|
||
else if(/^\/board\/trash/.test(p)){ icon='🗑️'; label='Corbeille'; kind='trash'; }
|
||
else { label='Board'; t = labelFromTitle(); if(t) label = t; }
|
||
}
|
||
else if(/^\/notes\//.test(p)){ icon='📝'; label='Notes'; kind='notes'; }
|
||
else if(/^\/settings/.test(p) || /^\/accounts\/settings/.test(p)){ icon='⚙️'; label='Paramètres'; kind='settings'; }
|
||
else if(/^\/accounts/.test(p)){ icon='👤'; label='Compte'; kind='account'; }
|
||
else if(/^\/help/.test(p)){ icon='❓'; label='Aide'; kind='help'; }
|
||
else {
|
||
label = labelFromTitle() || 'FlowDeck';
|
||
kind='section';
|
||
}
|
||
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(){
|
||
var m = marker();
|
||
if(m) return m;
|
||
var d = docContext();
|
||
if(d) return d;
|
||
return byPath();
|
||
}
|
||
};
|
||
})();
|
||
window.FlowDeckCtx = FlowDeckCtx;
|
||
|
||
// Apply an AI-produced document change to the open page editor.
|
||
// mode 'replace' → remplace tout le contenu de la page ;
|
||
// mode 'insert' → insère la proposition en fin de page (comme Ask AI).
|
||
window.fdApplyDocument = function(text, mode){
|
||
var E = window.E;
|
||
if(!E || !E.blocks || typeof E.mkB !== 'function') return false;
|
||
text = (text || '').trim();
|
||
if(!text) return false;
|
||
try{
|
||
E.sync();
|
||
if(mode === 'replace'){
|
||
var nb = (typeof E.md2b === 'function') ? E.md2b(text) : null;
|
||
if(!nb || !nb.length){ E.blocks = [E.mkB('paragraph', text)]; }
|
||
else { E.blocks = nb; }
|
||
} else {
|
||
if(typeof E.applyAIBlocks === 'function'){ E.applyAIBlocks(text); }
|
||
else {
|
||
var lines = text.split(/\r?\n/).map(function(s){ return s.trim(); }).filter(Boolean);
|
||
var startEmpty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph' && !(E.blocks[0].content || '').trim();
|
||
var nb2 = lines.map(function(l){ return E.mkB('paragraph', l); });
|
||
E.blocks = startEmpty ? nb2 : E.blocks.concat(nb2);
|
||
}
|
||
}
|
||
E.dirty = true;
|
||
if(E.autoSave) E.autoSave();
|
||
if(E.render) E.render();
|
||
return true;
|
||
}catch{ return false; }
|
||
};
|
||
|
||
// Backward-compatible detection hooks (banner + Ask AI)
|
||
window.fdPageContextProvider = function(){
|
||
var c = FlowDeckCtx.current();
|
||
return c && c.kind === 'page' ? c.text || '' : '';
|
||
};
|
||
})();
|