(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 ', desc:'changer de fournisseur LLM'}, {cmd:'/model ', 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 — effet réel : routes, UI, outils', action:'plugins', chev: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, plugins: [], connectorForm: {name: '', url: '', secret: '', kind: 'custom', auth: 'bearer', hint: ''}, 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'); // Retour du flux OAuth (Google / M365) : signaler le résultat puis nettoyer l'URL. try{ var qs = new URLSearchParams(window.location.search); if(qs.get('oauth') || qs.get('oauth_error')){ this.toast(qs.get('oauth_error') ? 'Connexion du connecteur refusée : ' + qs.get('oauth_error') : 'Connecteur connecté.', !!qs.get('oauth_error')); qs.delete('oauth'); qs.delete('oauth_error'); window.history.replaceState(null, '', window.location.pathname + (qs.toString() ? '?' + qs.toString() : '') + window.location.hash); } }catch{ /* volontaire */ } 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 { 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 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= 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]*>\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 · /model '); 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 . 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 . 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'; if(s === 'plugins') return 'Add plugins'; 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 === 'plugins') return this._pluginItems(); 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' ? '⚠ ' : '? ')); var tools = (c.kind === 'mcp' && c.tools_count) ? ' · ' + c.tools_count + ' outil(s)' : ''; return mark + (c.detail || c.status) + tools + (c.enabled ? '' : ' · désactivé'); }, // ── Plugins de l'instance (v7.58.0) ── fetchPlugins(){ var self = this; fetch('/api/agent/plugins').then(function(r){ return r.json(); }) .then(function(d){ self.plugins = (d && d.plugins) || []; }) .catch(function(){ self.plugins = []; }); }, _pluginItems(){ var out = (this.plugins || []).map(function(pl){ return {key:'pl-' + pl.slug, icon: pl.enabled ? '✅' : '⛔', label: pl.name, sub: (pl.enabled ? 'Activé' : 'Désactivé') + ' — ' + pl.description, action:'plugin-toggle', slug: pl.slug}; }); if(!out.length) out.push({key:'pl-none', _sep:'Aucun plugin'}); out.push({key:'pl-hint', _sep: 'OFF = routes refusées, UI masquée, outils retirés'}); return out; }, pluginToggle(slug){ var self = this; var pl = (this.plugins || []).filter(function(x){ return x.slug === slug; })[0]; if(!pl) return; fetch('/api/agent/plugins/' + encodeURIComponent(slug), { method:'PATCH', headers:{'X-CSRF-Token': getCsrf(), 'Content-Type':'application/json'}, body: JSON.stringify({enabled: !pl.enabled}) }).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; } self.toast(res.d.enabled ? 'Plugin activé.' : 'Plugin désactivé.'); self.fetchPlugins(); }).catch(function(){ self.toast('Bascule impossible (réseau).', true); }); }, _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.oauth){ var connected = c.status === 'ok'; items.push({key:'cn-oauth', icon: connected ? '🔓' : '🔑', label: connected ? 'Déconnecter' : 'Se connecter', sub: connected ? 'Retire les autorisations de ce compte' : 'Ouvre la page de connexion du fournisseur', action: connected ? 'connector-disconnect' : 'connector-connect'}); } 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); }); }, // ── Connexion OAuth Google / M365 (v7.56.0) ── connectorConnect(){ var self = this, c = this.connector; if(!c || !c.oauth) return; fetch('/api/agent/connectors/oauth/' + c.kind + '/authorize', { method:'POST', 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) || 'Connexion impossible.', true); return; } window.location.href = res.d.url; // → fournisseur → callback → retour ici }).catch(function(){ self.toast('Connexion impossible (réseau).', true); }); }, connectorDisconnect(){ var self = this, c = this.connector; if(!c || !c.oauth) return; fetch('/api/agent/connectors/oauth/' + c.kind + '/disconnect', { method:'POST', 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('Déconnexion impossible.', true); return; } self.toast('Connecteur déconnecté.'); self.fetchConnectors(); }).catch(function(){ self.toast('Déconnexion 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); }); }, // Type choisi dans le formulaire → URL / schéma d'auth / aide pré-remplis. connectorPreset(){ var f = this.connectorForm; var presets = { custom: {url:'', auth:'bearer', hint:'URL publique de votre API (hôtes privés refusés)'}, discord: {url:'https://discord.com/api/v10', auth:'bot', hint:'Discord Developers → Bot Token (jeton en Authorization)'}, telegram:{url:'https://api.telegram.org/bot{secret}', auth:'none', hint:'BotFather → Bot Token ({secret} complète l\u2019URL)'}, mcp: {url:'', auth:'bearer', hint:'URL du endpoint MCP (streamable HTTP)'} }; var p = presets[f.kind] || presets.custom; f.url = p.url; f.auth = p.auth; f.hint = p.hint; }, 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 || '', kind: f.kind || 'custom', auth: f.auth || 'bearer'}) }).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:'', kind:'custom', auth:'bearer', hint:''}; this.plusSection = 'connector-new'; this.plusFocus = -1; return; } if(it.action === 'plugins'){ this.fetchPlugins(); this.plusSection = 'plugins'; this.plusFocus = 0; return; } if(it.action === 'plugin-toggle'){ this.pluginToggle(it.slug); 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; } if(it.action === 'connector-connect'){ this.connectorConnect(); return; } if(it.action === 'connector-disconnect'){ this.connectorDisconnect(); 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 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,'>'); } function inline(t){ t = escHtml(t); return t .replace(/`([^`]+)`/g,'$1') .replace(/\*\*([^*]+)\*\*/g,'$1') .replace(/\*([^*]+)\*/g,'$1') .replace(/~~([^~]+)~~/g,'$1') .replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'$1'); } if(!md) return ''; var src = String(md); // Trim leading/trailing blank lines to avoid stray

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+'
  • '; } return '
  • '+li+'
  • '; }).join('') + ''; } if(block.type === 'quote'){ return '
    '+block.items.map(function(t){ return '

    '+inline(t)+'

    '; }).join('')+'
    '; } if(block.type === 'p'){ return block.text ? '

    '+inline(block.text)+'

    ' : ''; } 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 += '
    ' + escHtml(code.join('\n')) + '
    '; 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 = ''+headerCells.map(function(c){return ''+inline(c)+'';}).join('')+''; var tb = rows.map(function(r){ var cells = []; for(var k=0;k'+cells.map(function(c){return ''+inline(c)+'';}).join('')+''; }).join(''); html += '
    '+thead+''+tb+'
    '; i = j; continue; } // Horizontal rule if(/^(-{3,}|\*{3,}|_{3,})$/.test(t)){ html += '
    '; 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 += ''+inline(hm[2])+''; 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 '

    '+inline(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