(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].'} ]; 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, 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.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); }; // 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': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', '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'); }); }; }, // ── 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=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); }, // ── Mentions & contexte épinglé (@ / +) ── toggleAddPicker(){ if(this.mentionOpen && this._mentionFrom === '+'){ this.mentionOpen = false; this.mentionQuery = ''; return; } this.openMentionPicker('+'); }, openMentionPicker(from){ this.mentionOpen = true; this._mentionFrom = from || '+'; this.mentionQuery = ''; this.mentionFocus = -1; this.searchMentions(''); }, searchMentions(q){ var self = this; q = (q || '').trim(); this.mentionQuery = q; this.mentionLoad = true; if(this._mentionTimer) clearTimeout(this._mentionTimer); this._mentionTimer = setTimeout(function(){ 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.mentionOpen){ e.preventDefault(); e.stopPropagation(); this.mentionOpen = false; this._mentionFrom = null; this._atNode = null; return; } if(this.commandOpen){ e.preventDefault(); e.stopPropagation(); this.slashDismiss = true; this.cmdFocus = -1; return; } return; // laisse fermer le panneau } if(key === 'ArrowDown' || key === 'ArrowUp'){ var delta = key === 'ArrowDown' ? 1 : -1; if(this.mentionOpen && this.mentionItems.length){ e.preventDefault(); this.moveMentionFocus(delta); return; } if(this.commandOpen && !this.slashDismiss && this._slashItems().length){ e.preventDefault(); this.moveSlashFocus(delta); return; } return; } // Entrée / Tab avec un menu ouvert (skills / mentions) if(this.mentionOpen){ 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': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', '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': (document.cookie.match(/csrf_token=([^;]+)/)||[])[1]||'', '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