1671 lines
86 KiB
HTML
1671 lines
86 KiB
HTML
{# ════════════ FlowDeck Agent panel v4.13.0 — Notion-AI-style sidebar ════════════ #}
|
||
<style>
|
||
#fd-agent-fab{position:fixed;right:22px;bottom:22px;width:54px;height:54px;border-radius:50%;background:var(--accent,#2383E2);color:#fff;border:none;font-size:24px;cursor:pointer;box-shadow:0 8px 26px rgba(35,131,226,.5);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
|
||
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 34px rgba(35,131,226,.62)}
|
||
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:470px;max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
|
||
#fd-agent-panel.open{transform:translateX(0)}
|
||
#fd-agent-panel ::-webkit-scrollbar{width:8px;height:8px}
|
||
#fd-agent-panel ::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}
|
||
#fd-agent-panel ::-webkit-scrollbar-track{background:transparent}
|
||
|
||
/* Header */
|
||
.fd-ap-head{display:flex;align-items:center;gap:12px;padding:16px 18px 12px}
|
||
.fd-ap-logo{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--accent,#2383E2),#5b8def);color:#fff;display:flex;align-items:center;justify-content:center;font-size:19px;flex-shrink:0;box-shadow:0 4px 12px rgba(35,131,226,.35)}
|
||
.fd-ap-head-main{flex:1;min-width:0}
|
||
.fd-ap-title{font-size:15px;font-weight:600;color:var(--text,#f0f0f0);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.fd-ap-sub{font-size:11.5px;color:var(--text-dim,#9aa);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.fd-ap-headbtn{background:none;border:none;color:var(--text-dim,#999);width:30px;height:30px;border-radius:8px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||
.fd-ap-headbtn:hover{background:var(--bg-hover,#2b2b2b);color:var(--text,#fff)}
|
||
|
||
/* Context bar */
|
||
.fd-ap-ctx{display:flex;align-items:center;gap:8px;margin:0 16px 8px;padding:7px 11px;border-radius:9px;background:rgba(35,131,226,.09);border:1px solid rgba(35,131,226,.16);font-size:12px;color:var(--text-secondary,#ddd);min-height:18px}
|
||
.fd-ap-ctx .fd-ctx-ico{font-size:13px}
|
||
.fd-ap-ctx .fd-ctx-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
|
||
/* No-config banner */
|
||
.fd-ap-noconfig{padding:8px 14px;font-size:12px;color:#e6b4b4;background:rgba(227,98,98,.1);border:1px solid rgba(227,98,98,.18);margin:0 16px 10px;border-radius:9px;display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
||
.fd-ap-noconfig a{color:#ff9d9d;font-weight:600;text-decoration:none}
|
||
.fd-ap-noconfig .fd-k-sub{width:100%;color:var(--text-dim,#aaa)}
|
||
|
||
/* Tabs */
|
||
.fd-ap-tabs{display:flex;gap:2px;padding:0 16px 8px;border-bottom:1px solid var(--border,rgba(255,255,255,.08))}
|
||
.fd-ap-tab{flex:1;text-align:center;padding:7px;background:none;border:none;border-radius:8px;color:var(--text-dim,#999);font-size:12.5px;cursor:pointer}
|
||
.fd-ap-tab.active{background:var(--bg-tertiary,#242424);color:var(--text,#fff);font-weight:600}
|
||
|
||
/* Body */
|
||
.fd-agent-body{flex:1;overflow-y:auto;padding:12px 16px}
|
||
.fd-ap-hist-item{padding:8px 11px;border-radius:9px;cursor:pointer;color:var(--text,#e0e0e0);font-size:13px;border:1px solid transparent;margin-bottom:2px}
|
||
.fd-ap-hist-item:hover,.fd-ap-hist-item.active{background:var(--bg-hover,#262626);border-color:var(--border,rgba(255,255,255,.08))}
|
||
|
||
/* Empty / welcome state */
|
||
.fd-ap-empty{padding:6px 2px 4px}
|
||
.fd-ap-empty-title{font-size:14px;font-weight:600;color:var(--text,#eee);margin-bottom:2px}
|
||
.fd-ap-empty-desc{font-size:12px;color:var(--text-dim,#999);margin-bottom:12px}
|
||
.fd-ap-qa{display:flex;flex-direction:column;gap:6px}
|
||
.fd-ap-qa-item{display:flex;align-items:center;gap:10px;text-align:left;background:var(--bg-tertiary,#202020);border:1px solid var(--border,rgba(255,255,255,.07));border-radius:10px;padding:9px 12px;color:var(--text,#e6e6e6);font-size:13px;cursor:pointer;transition:background .12s,border-color .12s}
|
||
.fd-ap-qa-item:hover{background:var(--bg-hover,#2a2a2a);border-color:rgba(35,131,226,.4)}
|
||
.fd-ap-qa-item .fd-qa-ico{font-size:16px;flex-shrink:0}
|
||
.fd-ap-qa-item .fd-qa-new{margin-left:auto;font-size:9px;font-weight:700;color:#fff;background:var(--accent);border-radius:99px;padding:2px 8px;letter-spacing:.3px}
|
||
|
||
/* Messages */
|
||
.fd-agent-msg{display:flex;gap:9px;margin-bottom:14px}
|
||
.fd-agent-msg-avatar{width:28px;height:28px;border-radius:50%;background:var(--bg-tertiary,#2b2b2b);display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
|
||
.fd-agent-msg-assistant .fd-agent-msg-avatar{background:rgba(35,131,226,.18)}
|
||
.fd-agent-msg-body{flex:1;min-width:0}
|
||
.fd-agent-msg-meta{font-size:10px;color:var(--text-dim,#888);margin-bottom:2px;text-transform:uppercase;letter-spacing:.5px}
|
||
.fd-agent-msg-content{font-size:13.5px;color:var(--text,#ececec);white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
||
.fd-agent-msg-content a{color:var(--accent)}
|
||
.fd-agent-msg-proposal .fd-agent-msg-content{background:rgba(35,131,226,.07);border:1px solid rgba(35,131,226,.22);border-radius:10px;padding:10px 12px}
|
||
.fd-proposal-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:8px}
|
||
.fd-proposal-btn{background:var(--bg-tertiary,#242424);border:1px solid var(--border,rgba(255,255,255,.12));color:var(--text,#e6e6e6);border-radius:8px;padding:6px 12px;font-size:12.5px;cursor:pointer}
|
||
.fd-proposal-btn:hover{background:var(--bg-hover,#2e2e2e)}
|
||
.fd-proposal-btn.primary{background:var(--accent,#2383E2);border-color:transparent;color:#fff;font-weight:600}
|
||
.fd-proposal-btn:disabled{opacity:.5;cursor:not-allowed}
|
||
.fd-proposal-note{font-size:11px;color:var(--text-dim,#999)}
|
||
.fd-agent-thinking{font-size:12.5px;color:var(--text-dim,#999);padding:10px 12px;border-radius:9px;background:rgba(255,255,255,.04);margin:4px 0 8px;font-style:italic}
|
||
.fd-ap-err{color:#ef8a8a;font-size:12px;padding:4px 2px}
|
||
|
||
/* Composer */
|
||
.fd-ap-composer{padding:10px 14px 14px;border-top:1px solid var(--border,rgba(255,255,255,.09));position:relative}
|
||
.fd-ap-box{background:var(--bg-tertiary,#1f1f1f);border:1px solid var(--border,rgba(255,255,255,.13));border-radius:16px;padding:10px 12px 8px;transition:border-color .12s,box-shadow .12s}
|
||
.fd-ap-box:focus-within{border-color:rgba(35,131,226,.65);box-shadow:0 0 0 3px rgba(35,131,226,.12)}
|
||
.fd-ap-input{width:100%;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:14px;resize:none;font-family:inherit;line-height:1.6;max-height:220px;box-sizing:border-box;overflow-y:auto;white-space:pre-wrap;word-break:break-word;min-height:26px;position:relative}
|
||
.fd-ap-input.empty::before{content:attr(data-placeholder);color:var(--text-dim,#8a8a8a);position:absolute;left:0;top:0;pointer-events:none;white-space:pre-wrap;max-width:100%}
|
||
.ment-token{display:inline-flex;align-items:center;gap:4px;vertical-align:middle;background:rgba(35,131,226,.14);border:1px solid rgba(35,131,226,.35);color:var(--text,#eaf2ff);border-radius:6px;padding:0 3px 0 4px;font-size:13px;line-height:1.5;margin:0 1px;max-width:200px;overflow:hidden;white-space:nowrap}
|
||
.ment-token .tk-lbl{overflow:hidden;text-overflow:ellipsis}
|
||
.ment-token.skill{background:rgba(168,85,247,.16);border-color:rgba(168,85,247,.5);color:#f3e8ff}
|
||
.ment-token .tk-ico{flex-shrink:0;opacity:.9}
|
||
.fd-ap-toolbar{display:flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap}
|
||
.fd-ap-tip{font-size:11px;color:var(--text-dim,#8f8f8f);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.fd-ap-tools{display:flex;align-items:center;gap:6px;margin-left:auto}
|
||
.fd-ap-model{display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.05);border:1px solid var(--border);border-radius:99px;padding:4px 10px;font-size:11.5px;color:var(--text-secondary,#ddd);cursor:pointer;white-space:nowrap;max-width:220px;overflow:hidden;text-overflow:ellipsis}
|
||
.fd-ap-model:hover{background:var(--bg-hover,#2c2c2c)}
|
||
.fd-ap-model.off{opacity:.75}
|
||
.fd-ap-send{width:34px;height:34px;border-radius:50%;border:none;background:var(--accent,#2383E2);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:15px;transition:background .12s,transform .12s;flex-shrink:0}
|
||
.fd-ap-send:hover{background:#2c8ceb}
|
||
.fd-ap-send:disabled{opacity:.4;cursor:not-allowed;transform:none}
|
||
.fd-ap-send:not(:disabled):active{transform:scale(.94)}
|
||
|
||
/* Model picker popover */
|
||
.fd-ap-pop{position:absolute;right:14px;bottom:calc(100% + 6px);width:270px;background:var(--bg-modal,#1c1c1c);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.5);padding:12px;z-index:60}
|
||
.fd-ap-pop label{display:block;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:8px 0 4px}
|
||
.fd-ap-pop select{width:100%;background:var(--bg-tertiary,#232323);border:1px solid var(--border);border-radius:8px;color:var(--text,#fff);font-size:12.5px;padding:7px 8px;outline:none}
|
||
.fd-ap-pop a{display:inline-block;margin-top:10px;font-size:12px;color:var(--accent);text-decoration:none}
|
||
|
||
/* Slash command menu */
|
||
.fd-agent-cmd-menu{position:absolute;bottom:calc(100% + 4px);left:0;right:0;background:var(--bg-modal,#1e1e1e);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:11px;max-height:190px;overflow-y:auto;box-shadow:0 -8px 26px rgba(0,0,0,.4);z-index:70;display:none}
|
||
.fd-agent-cmd-menu.show{display:block}
|
||
.fd-agent-cmd-item{padding:8px 12px;font-size:12px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:3px;display:flex;gap:8px;align-items:baseline}
|
||
.fd-agent-cmd-item:hover{background:var(--bg-hover,#2b2b2b)}
|
||
.fd-agent-cmd-item b{color:var(--accent,#2383E2);font-family:monospace;white-space:nowrap}
|
||
.fd-agent-cmd-item b.k-cmd{color:var(--accent,#2383E2)}
|
||
.fd-agent-cmd-item b.k-skill{color:#c084fc}
|
||
.fd-agent-cmd-item span{color:var(--text-dim,#999);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.fd-agent-cmd-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px}
|
||
|
||
/* Action bar under each assistant post */
|
||
.fd-agent-msg-actions{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px;align-items:center}
|
||
.fd-act-btn{display:inline-flex;align-items:center;justify-content:center;gap:0;background:transparent;border:1px solid var(--border,rgba(255,255,255,.09));color:var(--text-dim,#9aa);border-radius:7px;width:30px;height:26px;font-size:13px;cursor:pointer;transition:background .12s,color .12s,border-color .12s;line-height:1}
|
||
.fd-act-btn:hover{background:var(--bg-hover,#262626);color:var(--text,#fff);border-color:rgba(255,255,255,.25)}
|
||
.fd-act-btn.on{background:rgba(35,131,226,.18);border-color:rgba(35,131,226,.55);color:#fff}
|
||
.fd-act-btn:disabled,.fd-act-btn.disabled{opacity:.4;cursor:not-allowed}
|
||
|
||
/* Chips (contexte épinglé @ / + / skills) */
|
||
.fd-ap-chips{display:flex;flex-direction:column;gap:5px;padding:8px 4px 4px;max-height:92px;overflow-y:auto}
|
||
.fd-chip-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||
.fd-chip{display:inline-flex;align-items:center;gap:6px;background:rgba(35,131,226,.10);border:1px solid rgba(35,131,226,.30);color:var(--text,#e8e8e8);border-radius:99px;padding:3px 5px 3px 9px;font-size:11.5px;max-width:220px}
|
||
.fd-chip.skill{background:rgba(168,85,247,.12);border-color:rgba(168,85,247,.38)}
|
||
.fd-chip-ico{flex-shrink:0;font-size:12px}
|
||
.fd-chip-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.fd-chip-spacer{flex-basis:100%;height:0}
|
||
.fd-chip-x{background:none;border:none;color:var(--text-dim,#aaa);cursor:pointer;font-size:13px;line-height:1;padding:0 4px;border-radius:50%;flex-shrink:0}
|
||
.fd-chip-x:hover{color:#fff;background:rgba(255,255,255,.14)}
|
||
|
||
/* Add button (+ contexte) */
|
||
.fd-ap-add{width:30px;height:30px;border-radius:50%;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.14));color:var(--text,#ddd);cursor:pointer;font-size:17px;display:flex;align-items:center;justify-content:center;flex-shrink:0;line-height:1}
|
||
.fd-ap-add:hover{background:var(--bg-hover,#303030);color:#fff}
|
||
|
||
/* Mention menu (@ / +) */
|
||
.fd-ap-mention-menu{position:absolute;bottom:calc(100% + 4px);left:0;right:0;background:var(--bg-modal,#1e1e1e);border:1px solid var(--border,rgba(255,255,255,.12));border-radius:11px;max-height:220px;overflow-y:auto;box-shadow:0 -8px 26px rgba(0,0,0,.4);z-index:70;display:none}
|
||
.fd-ap-mention-menu.show{display:block}
|
||
.fd-mention-item{display:flex;align-items:center;gap:9px;padding:7px 12px;font-size:12.5px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:2px 4px}
|
||
.fd-mention-item:hover,.fd-mention-item.focus{background:var(--bg-hover,#2b2b2b)}
|
||
.fd-mention-ico{width:24px;text-align:center;flex-shrink:0;font-size:14px}
|
||
.fd-mention-main{flex:1;min-width:0;display:flex;flex-direction:column}
|
||
.fd-mention-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.fd-mention-sub{font-size:10.5px;color:var(--text-dim,#999);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.fd-mention-empty,.fd-mention-load{padding:11px 12px;font-size:12px;color:var(--text-dim,#aaa);text-align:center}
|
||
|
||
/* Toast */
|
||
.fd-ap-toast{position:absolute;left:14px;right:14px;bottom:calc(100% + 10px);background:var(--bg-modal,#1c1c1c);border:1px solid rgba(35,131,226,.5);color:var(--text,#fff);font-size:12px;border-radius:9px;padding:8px 12px;box-shadow:0 10px 30px rgba(0,0,0,.45);z-index:90;display:flex;gap:8px;align-items:center;justify-content:space-between}
|
||
.fd-ap-toast.err{border-color:rgba(227,98,98,.55)}
|
||
.fd-ap-toast button{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:13px;padding:0}
|
||
|
||
[x-cloak]{display:none!important}
|
||
</style>
|
||
|
||
{# ── Global Agent API (available before Alpine for the FAB / shortcut) ── #}
|
||
<script>
|
||
(function(){
|
||
var defaultAPI = {
|
||
open: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle')); },
|
||
close: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle', { detail: { close: true } })); },
|
||
toggle: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle')); },
|
||
ask: function(){}, generate: function(){ return Promise.resolve(''); }
|
||
};
|
||
window.fdAgent = window.fdAgent || defaultAPI;
|
||
// Ctrl/Cmd+J — open / close the Agent panel from anywhere.
|
||
document.addEventListener('keydown', function(e){
|
||
if((e.ctrlKey || e.metaKey) && (e.key === 'j' || e.key === 'J')){
|
||
e.preventDefault();
|
||
if(window.fdAgent && window.fdAgent.toggle) window.fdAgent.toggle();
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
{# ── Floating button ── #}
|
||
<button id="fd-agent-fab" title="FlowDeck Agent (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.toggle()">🤖</button>
|
||
|
||
{# ── Panel ── #}
|
||
<div id="fd-agent-panel" class="fd-ap" :class="{open: open}" x-data="agentPanel()" @keydown.escape.window="close()" role="dialog" aria-label="Agent FlowDeck">
|
||
<div class="fd-ap-head">
|
||
<span class="fd-ap-logo">🤖</span>
|
||
<div class="fd-ap-head-main">
|
||
<div class="fd-ap-title">How can I help you today?</div>
|
||
<div class="fd-ap-sub" x-text="providerModelLabel"></div>
|
||
</div>
|
||
<button class="fd-ap-headbtn" title="Nouvelle conversation" @click="newConversation()">+</button>
|
||
<button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button>
|
||
</div>
|
||
|
||
<div class="fd-ap-noconfig" x-show="!hasActiveProvider">
|
||
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
|
||
<a href="/settings" target="_blank" @click.stop>Configurer → Agent & IA</a>
|
||
<template x-if="unverifiedCount">
|
||
<span class="fd-k-sub">🔑 <span x-text="unverifiedCount"></span> clé(s) à tester : Paramètres puis « Tester ».</span>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="fd-ap-tabs">
|
||
<button class="fd-ap-tab" :class="{active: tab==='chat'}" @click="tab='chat'">Conversation</button>
|
||
<button class="fd-ap-tab" :class="{active: tab==='history'}" @click="tab='history'; loadConversations()">Historique</button>
|
||
</div>
|
||
|
||
<div class="fd-agent-body">
|
||
<template x-if="tab==='history'">
|
||
<div>
|
||
<template x-for="c in conversations" :key="c.id">
|
||
<div class="fd-ap-hist-item" :class="{active: currentConv && currentConv.id===c.id}" @click="loadConversation(c.id)">
|
||
<div x-text="c.title || 'New conversation'"></div>
|
||
<div style="font-size:11px;color:var(--text-dim)" x-text="c.updated_at"></div>
|
||
</div>
|
||
</template>
|
||
<div x-show="!conversations.length" style="font-size:13px;color:var(--text-dim);padding:10px">Aucune conversation. Démarrez-en une !</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template x-if="tab==='chat'">
|
||
<div>
|
||
<!-- Welcome / quick actions -->
|
||
<div class="fd-ap-empty" x-show="!messages.length && !streaming" x-cloak>
|
||
<div class="fd-ap-empty-desc" x-text="quickIntro"></div>
|
||
<div class="fd-ap-qa">
|
||
<template x-for="q in quickActions" :key="q.label">
|
||
<button class="fd-ap-qa-item" type="button" @click="runQuick(q)">
|
||
<span class="fd-qa-ico" x-text="q.icon"></span>
|
||
<span x-text="q.label"></span>
|
||
<span class="fd-qa-new" x-show="q.new" x-cloak>New</span>
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<template x-for="m in messages" :key="m.id">
|
||
<div class="fd-agent-msg" :class="'fd-agent-msg-'+(m.role||'user') + (m.proposal ? ' fd-agent-msg-proposal' : '')">
|
||
<div class="fd-agent-msg-avatar" x-text="m.role==='assistant'?'🤖':'👤'"></div>
|
||
<div class="fd-agent-msg-body">
|
||
<div class="fd-agent-msg-meta" x-text="(m.role==='assistant'?'Agent':'Vous')+(m.model?' · '+m.model:'')"></div>
|
||
<div class="fd-agent-msg-content" x-text="m.generating ? 'Génération…' : m.content"></div>
|
||
<template x-if="m.proposal">
|
||
<div class="fd-proposal-bar">
|
||
<button class="fd-proposal-btn primary" type="button" @click="approveProposal(m)" :disabled="m.applied || m.offline">
|
||
<span x-text="m.applied ? (m.rejected ? 'Rejetée' : '✓ Appliquée') : '✓ Appliquer au document'"></span>
|
||
</button>
|
||
<button class="fd-proposal-btn" type="button" @click="rejectProposal(m)" :disabled="m.applied">✕ Rejeter</button>
|
||
<span class="fd-proposal-note" x-show="m.offline" x-cloak>Hors-ligne — configurez une clé API pour appliquer la modification.</span>
|
||
</div>
|
||
</template>
|
||
<template x-if="m.role==='assistant' && !m.generating && m.content && !m.act">
|
||
<div class="fd-agent-msg-actions">
|
||
<button class="fd-act-btn" type="button" @click="copyResponse(m)" title="Copier la réponse">⧉</button>
|
||
<button class="fd-act-btn" type="button" @click="insertIntoPage(m)" :disabled="!canInsert" title="Insérer dans la page ouverte">➕</button>
|
||
<button class="fd-act-btn" :class="{on: m.fb==='up'}" type="button" @click="feedback(m,'up')" :disabled="!!m.fb && m.fb!=='up'" title="Feedback positif">👍</button>
|
||
<button class="fd-act-btn" :class="{on: m.fb==='down'}" type="button" @click="feedback(m,'down')" :disabled="!!m.fb && m.fb!=='down'" title="Feedback négatif">👎</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<div class="fd-agent-thinking" x-show="streaming">L'agent réfléchit et agit… <span x-text="liveReasoning"></span></div>
|
||
<div class="fd-ap-err" x-show="errorMsg" x-text="errorMsg"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="fd-ap-composer">
|
||
<div style="position:relative;">
|
||
<div class="fd-ap-toast" x-show="toastMsg" x-cloak :class="{err: toastErr}">
|
||
<span x-text="toastMsg"></span>
|
||
<button type="button" @click="toastMsg=''">✕</button>
|
||
</div>
|
||
<div class="fd-agent-cmd-menu" :class="{show: commandOpen && commandSuggestions.length && !slashDismiss}">
|
||
<template x-for="c in commandSuggestions" :key="c.key">
|
||
<div>
|
||
<div class="fd-agent-cmd-sep" x-show="c._sep" x-text="c._sep"></div>
|
||
<div class="fd-agent-cmd-item" x-show="!c._sep" :class="{focus: c._i===cmdFocus}" @click="useSuggestion(c)" @mouseenter="cmdFocus=c._i">
|
||
<b :class="c._admin ? 'k-cmd' : 'k-skill'" x-text="(c._admin ? c.cmd : c.cmd)"></b>
|
||
<span x-text="c.desc"></span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<div class="fd-ap-mention-menu" :class="{show: mentionOpen}">
|
||
<div class="fd-mention-load" x-show="mentionLoad" x-cloak>Recherche…</div>
|
||
<template x-if="!mentionLoad && !mentionItems.length">
|
||
<div class="fd-mention-empty">Aucun élément. Tapez un nom de document, page ou base.</div>
|
||
</template>
|
||
<template x-if="!mentionLoad" x-for="it in mentionItems" :key="it.token">
|
||
<div class="fd-mention-item" :class="{focus: it._i===mentionFocus}" @click="pickMention(it)" @mouseenter="mentionFocus=it._i">
|
||
<span class="fd-mention-ico" x-text="it.icon"></span>
|
||
<div class="fd-mention-main">
|
||
<div class="fd-mention-lbl" x-text="it.label"></div>
|
||
<div class="fd-mention-sub" x-text="it.sub"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="fd-ap-chips" x-show="contextChips.length" x-cloak>
|
||
<div class="fd-chip-row" x-show="mentionChips.length">
|
||
<template x-for="chip in mentionChips" :key="chip.key">
|
||
<span class="fd-chip">
|
||
<span class="fd-chip-ico" x-text="chip.icon"></span>
|
||
<span class="fd-chip-lbl" x-text="chip.label"></span>
|
||
<button class="fd-chip-x" type="button" @click="removeChip(chip.token)" title="Retirer du contexte">✕</button>
|
||
</span>
|
||
</template>
|
||
</div>
|
||
<div class="fd-chip-row" x-show="skillChips.length">
|
||
<template x-for="chip in skillChips" :key="chip.key">
|
||
<span class="fd-chip skill">
|
||
<span class="fd-chip-ico" x-text="chip.icon"></span>
|
||
<span class="fd-chip-lbl" x-text="chip.label"></span>
|
||
<button class="fd-chip-x" type="button" @click="removeChip(chip.token)" title="Retirer le skill">✕</button>
|
||
</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="fd-ap-box">
|
||
<div class="fd-ap-input empty" x-ref="cmdInput" contenteditable="true" spellcheck="false" role="textbox" aria-multiline="true"
|
||
data-placeholder="Demandez à l'agent… Tapez @ pour référencer une page/base, / pour un skill. Entrée = envoyer, Maj+Entrée = ligne"
|
||
@input="onInput()" @keydown="onComposerKeydown($event)" @paste="onPaste($event)"></div>
|
||
<div class="fd-ap-toolbar">
|
||
<button class="fd-ap-add" type="button" title="Ajouter un élément au contexte (@ / +)" @click="toggleAddPicker()">+</button>
|
||
<span class="fd-ap-tip" x-text="composerHint"></span>
|
||
<div class="fd-ap-tools">
|
||
<button class="fd-ap-model" :class="{off: !hasActiveProvider}" type="button" @click="modelOpen=!modelOpen" title="Choisir fournisseur et modèle">
|
||
<span x-text="autoLabel"></span> <span style="opacity:.6">▾</span>
|
||
</button>
|
||
<button class="fd-ap-send" @click="send()" :disabled="streaming || !canSend" title="Envoyer">▲</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="fd-ap-pop" x-show="modelOpen" @click.outside="modelOpen=false" x-cloak>
|
||
<label>Fournisseur</label>
|
||
<select x-model="llmProvider" @change="onProviderChange()">
|
||
<template x-for="p in providers" :key="p.id">
|
||
<option :value="p.id" x-text="p.id==='offline' ? 'Hors-ligne (mock)' : p.name"></option>
|
||
</template>
|
||
</select>
|
||
<label>Modèle</label>
|
||
<select x-model="llmModel" @change="patchConversation()" x-show="llmProvider!=='offline'">
|
||
<template x-for="m in providerModels" :key="m">
|
||
<option :value="m" x-text="m"></option>
|
||
</template>
|
||
</select>
|
||
<a href="/settings" target="_blank" @click="modelOpen=false">⚙️ Configurer (clés API) →</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function(){
|
||
// ── Skills intégrés (workflows « / ») et commandes admin du panneau ──
|
||
var FD_ADMIN_CMDS = [
|
||
{cmd:'/help', desc:'liste des commandes'},
|
||
{cmd:'/providers', desc:'liste les fournisseurs actifs'},
|
||
{cmd:'/provider <nom>', desc:'changer de fournisseur LLM'},
|
||
{cmd:'/model <nom>', desc:'changer de modèle LLM'},
|
||
{cmd:'/keys', desc:'fournisseurs avec clé API enregistrée'}
|
||
];
|
||
var FD_SKILLS = [
|
||
{cmd:'/research', icon:'🔎', desc:'recherche structurée + recommandation',
|
||
template:'Fais un état des lieux de [sujet] : contexte, avantages/inconvénients, options, puis une recommandation finale. Indique tes sources si tu en utilises.'},
|
||
{cmd:'/create-new-skill', icon:'🛠️', desc:'crée un workflow réutilisable (skill)',
|
||
template:'Je veux créer un skill réutilisable qui [décris le workflow]. Produis : nom, description courte, étapes et format de sortie attendu.'},
|
||
{cmd:'/resume', icon:'📄', desc:'résumé / synthèse structurée',
|
||
template:'Résume ce contenu de façon structurée et concise : points clés, décisions, TL;DR.'},
|
||
{cmd:'/actions', icon:'✅', desc:'extraire les actions & to-dos',
|
||
template:'À partir de ce contenu, liste les actions à mener avec responsable et échéance quand c\u2019est indiqué.'},
|
||
{cmd:'/reformuler', icon:'✍️', desc:'réécriture clarté / ton',
|
||
template:'Reformule ce contenu pour le rendre plus clair et plus concis en conservant le sens. Précise le ton souhaité (interne, client, formel…).'},
|
||
{cmd:'/correction', icon:'🔤', desc:'correction grammaire / orthographe / style',
|
||
template:'Corrige la grammaire, l\u2019orthographe et le style de ce contenu sans en changer le sens.'},
|
||
{cmd:'/brainstorm', icon:'💡', desc:'générer des idées, angles, variantes',
|
||
template:'Propose des idées variées sur [sujet] : angles, exemples concrets, variantes de messages (annonce, email, post).'},
|
||
{cmd:'/plan', icon:'🧭', desc:'planifier / structurer un document',
|
||
template:'Construis un plan structuré pour [sujet] : sections logiques, sous-points et ordre de rédaction.'},
|
||
{cmd:'/ask', icon:'💬', desc:'Q&A sur un contenu référencé',
|
||
template:'À partir du contenu référencé, réponds précisément à [ta question].'},
|
||
{cmd:'/meeting-note', icon:'📝', desc:'compte-rendu / note de réunion',
|
||
template:'Rédige une AI Meeting Note : Résumé, Décisions, Action items (avec responsables et échéances), Prochaines étapes.'},
|
||
{cmd:'/livrable', icon:'📨', desc:'email / compte-rendu / message Slack',
|
||
template:'Rédige un livrable professionnel (email, compte-rendu ou message Slack) sur [sujet / destinataire / contexte].'}
|
||
];
|
||
|
||
function agentPanel(){
|
||
return {
|
||
open: false, modelOpen: false, tab: 'chat',
|
||
agents: [], conversations: [], currentConv: null,
|
||
messages: [], input: '', streaming: false, liveReasoning: '',
|
||
provider: 'offline', providerAvailable: false, errorMsg: '',
|
||
allProviders: [], providers: [], providerModels: [], llmProvider: 'offline',
|
||
llmModel: '', llmKeys: [], defaultProvider: 'offline',
|
||
activeContext: null, ctxPinnedKey: null,
|
||
contextChips: [], skills: [],
|
||
mentionOpen: false, mentionItems: [], mentionQuery: '', mentionLoad: false,
|
||
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
|
||
cmdFocus: -1, slashDismiss: false,
|
||
toastMsg: '', toastErr: false, _toastTimer: null,
|
||
_slashSeq: 0,
|
||
|
||
init(){
|
||
var self = this;
|
||
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);
|
||
};
|
||
window.fdAgent.generate = function(message, context){
|
||
message = (message || '').trim();
|
||
if(!message) return Promise.resolve('');
|
||
var payload = {message: message};
|
||
if(context && context.trim()) payload.context = String(context).slice(0, 20000);
|
||
return fetch('/api/agent/conversations', {
|
||
method:'POST', headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({title:'Agent'})
|
||
}).then(function(r){ return r.json(); }).then(function(conv){
|
||
var cid = conv && conv.id;
|
||
if(!cid) throw new Error('Impossible de créer une conversation agent');
|
||
return fetch('/api/agent/conversations/' + cid + '/run', {
|
||
method:'POST', headers:{'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 buf = '', finalText = '', lastError = '';
|
||
function pump(){
|
||
return reader.read().then(function(res){
|
||
if(res.done){ if(lastError) throw new Error(lastError); return finalText; }
|
||
buf += dec.decode(res.value, {stream:true});
|
||
var parts = buf.split('\n\n');
|
||
buf = parts.pop();
|
||
for(var i=0;i<parts.length;i++){
|
||
var line = parts[i].replace(/^data:\s*/, '').trim();
|
||
if(!line) continue;
|
||
try{
|
||
var ev = JSON.parse(line);
|
||
if(ev.type === 'final') finalText = ev.content || '';
|
||
else if(ev.type === 'error') lastError = ev.message || 'Erreur agent';
|
||
}catch(e){}
|
||
}
|
||
return pump();
|
||
});
|
||
}
|
||
return pump();
|
||
});
|
||
});
|
||
};
|
||
},
|
||
|
||
// ── 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;
|
||
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. épinglée(s)' : '';
|
||
return base + extra + ' · Entrée ↵ = envoyer · Maj+Entrée = ligne';
|
||
},
|
||
get canInsert(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
|
||
|
||
// ── Quick actions (contextual) ──
|
||
get quickIntro(){
|
||
var c = this.activeContext;
|
||
if(c && c.kind === 'page') return 'Comment puis-je vous aider sur ce document ?';
|
||
return 'Que voulez-vous faire ?';
|
||
},
|
||
get quickActions(){
|
||
var c = this.activeContext || {};
|
||
var out = [];
|
||
if(c.kind === 'page'){
|
||
out.push({icon:'📄', label:'Résumer ce document', prompt:'Résume ce document de façon structurée et concise.', docRun:true, mode:'insert'});
|
||
out.push({icon:'🌐', label:'Traduire cette page', prompt:'Traduis le contenu de ce document en anglais, en conservant sa structure (titres, listes, paragraphes).', docRun:true, mode:'replace'});
|
||
out.push({icon:'✍️', label:'Proposer des améliorations', prompt:'Analyse ce document et propose une version améliorée (structure, clarté, complétude) sans en perdre le sens.', docRun:true, mode:'insert'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Rédige une « AI Meeting Note » structurée à partir de ce document : en-tête avec date, sections Résumé, Décisions, Action items et Prochaines étapes.', docRun:true, mode:'insert'});
|
||
} else if(c.kind === 'db'){
|
||
out.push({icon:'🗄️', label:'Analyser cette base', prompt:'Analyse cette base de données et résume sa structure, ses propriétés et ses données.'});
|
||
out.push({icon:'📊', label:'Créer une vue', prompt:'Propose une vue adaptée (board, tableau, calendrier…) pour cette base et explique pourquoi.'});
|
||
out.push({icon:'📈', label:'Synthèse des données', prompt:'Produis une synthèse des données présentes dans cette base.'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré.'});
|
||
} else if(c.kind === 'tasks'){
|
||
out.push({icon:'✅', label:'Mes tâches', prompt:'Liste mes tâches et suggère les priorités de la journée.'});
|
||
out.push({icon:'🗓️', label:'Planifier ma journée', prompt:'Aide-moi à organiser ma journée à partir de mes tâches.'});
|
||
out.push({icon:'🤖', label:'Question générale', prompt:'Explique-moi comment fonctionne My Tasks dans FlowDeck.'});
|
||
} else {
|
||
out.push({icon:'🗄️', label:'Créer une collection', prompt:'Crée une collection « Projets » avec les propriétés Status et Priorité, puis une vue board.', new:true});
|
||
out.push({icon:'📋', label:'Créer un board', prompt:'Crée un board pour suivre le travail de l\u2019équipe.'});
|
||
out.push({icon:'❓', label:'Comment utiliser FlowDeck ?', prompt:'Guide-moi sur les fonctionnalités principales de FlowDeck et comment les utiliser.'});
|
||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré.'});
|
||
}
|
||
return out;
|
||
},
|
||
runQuick(q){
|
||
if(!q || !q.prompt) return;
|
||
if(q.docRun){ this.runDocAction(q); return; }
|
||
var self = this;
|
||
this.ensureConversation().then(function(){
|
||
self.setComposerText(q.prompt);
|
||
self.send();
|
||
}).catch(function(){});
|
||
},
|
||
|
||
// ── Document actions (no tools) → proposition à approuver / rejeter ──
|
||
_isContentRequest(t){
|
||
var low = (t || '').toLowerCase();
|
||
var kws = ['écris','écrire','écrit un','rédige','rédiger','génère','générer','produis',
|
||
'développe','texte sur','paragraphe','article sur','rédaction','résume','résumer',
|
||
'résumé de','traduis','traduire','améliore','améliorer','note de réunion',
|
||
'série tv','série','contenu de la page','poème','histoire sur','sujet de','courriel','email sur'];
|
||
for(var i=0;i<kws.length;i++){ if(low.indexOf(kws[i]) !== -1) return true; }
|
||
return false;
|
||
},
|
||
_docModeFor(t){
|
||
var low = (t || '').toLowerCase();
|
||
if(low.indexOf('traduis') !== -1 || low.indexOf('traduire') !== -1 || low.indexOf('translate') !== -1) return 'replace';
|
||
return 'insert';
|
||
},
|
||
runDocAction(q){
|
||
var self = this;
|
||
this.ensureConversation().then(function(){
|
||
self.errorMsg = '';
|
||
self.messages.push({id:'udoc-'+Date.now(), role:'user', content: q.label});
|
||
self.refreshContext();
|
||
var ctxText = (self.activeContext && self.activeContext.kind === 'page' && self.activeContext.text) ? self.activeContext.text : '';
|
||
var thinkId = 'g-'+Date.now()+Math.random();
|
||
self.messages.push({id: thinkId, role:'assistant', generating: true});
|
||
self.scrollBottom();
|
||
|
||
var payload = {prompt: q.prompt};
|
||
if(ctxText) payload.context = ctxText;
|
||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||
if(self.llmModel) payload.model = self.llmModel;
|
||
|
||
fetch('/api/agent/generate', {
|
||
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload)
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||
if(d && d.ok && d.text){
|
||
self.messages.push({
|
||
id:'prop-'+Date.now()+Math.random(), role:'assistant',
|
||
content: d.text, proposal: true, mode: q.mode || 'insert',
|
||
offline: !!d.offline, model: d.model || '', applied: false, rejected: false
|
||
});
|
||
} else {
|
||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||
content: '⚠️ ' + ((d && d.error) || 'Échec de la génération'), model:''});
|
||
}
|
||
self.scrollBottom();
|
||
}).catch(function(err){
|
||
self.messages = self.messages.filter(function(m){ return m.id !== thinkId; });
|
||
self.messages.push({id:'err-'+Date.now()+Math.random(), role:'assistant',
|
||
content: '⚠️ ' + ((err && err.message) || 'Erreur réseau'), model:''});
|
||
self.scrollBottom();
|
||
});
|
||
}).catch(function(){});
|
||
},
|
||
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 = []; });
|
||
},
|
||
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 = '';
|
||
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; }
|
||
}).catch(function(){});
|
||
},
|
||
newConversation(){
|
||
var self = this;
|
||
var body = {title:'Nouvelle conversation'};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
fetch('/api/agent/conversations', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)})
|
||
.then(function(r){return r.json()}).then(function(d){
|
||
var conv = {id:d.id, title:d.title, updated_at:new Date().toISOString()};
|
||
self.conversations.unshift(conv); self.currentConv = conv;
|
||
self.messages = []; self.errorMsg=''; self.tab='chat';
|
||
self.contextChips = []; self.mentionOpen = false;
|
||
self.$nextTick(function(){ self.resizeInput(); });
|
||
}).catch(function(){});
|
||
},
|
||
ensureConversation(){
|
||
var self = this;
|
||
if(self.currentConv && self.currentConv.id) return Promise.resolve(self.currentConv);
|
||
if(self._ensuring) return self._ensuring;
|
||
var body = {title:'Nouvelle conversation'};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
self._ensuring = fetch('/api/agent/conversations', {
|
||
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
var conv = {id:d.id, title:d.title, updated_at:new Date().toISOString()};
|
||
self.conversations.unshift(conv); self.currentConv = conv;
|
||
self.messages = []; self.errorMsg=''; self.tab='chat';
|
||
self.contextChips = []; self.mentionOpen = false;
|
||
self._ensuring = null;
|
||
return conv;
|
||
}).catch(function(err){
|
||
self._ensuring = null;
|
||
self.errorMsg = (err && err.message) ? err.message : 'Impossible de créer la conversation';
|
||
throw err;
|
||
});
|
||
return self._ensuring;
|
||
},
|
||
patchConversation(){
|
||
var self = this;
|
||
if(!self.currentConv) return;
|
||
var body = {};
|
||
if(self.llmProvider) body.provider = self.llmProvider;
|
||
if(self.llmModel) body.model = self.llmModel;
|
||
fetch('/api/agent/conversations/'+self.currentConv.id, {
|
||
method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
|
||
}).catch(function(){});
|
||
},
|
||
|
||
// ── Composer (contenteditable) helpers ──
|
||
_composerEl(){ return this.$refs && this.$refs.cmdInput; },
|
||
composerText(){
|
||
var el = this._composerEl();
|
||
if(!el) return (this.input || '');
|
||
var parts = [];
|
||
var nodes = el.childNodes;
|
||
for(var i=0;i<nodes.length;i++){
|
||
var n = nodes[i];
|
||
if(n.nodeType===3) parts.push(n.nodeValue);
|
||
else if(n.nodeType===1){
|
||
if(n.tagName === 'BR') parts.push('\n');
|
||
else if(n.hasAttribute && n.hasAttribute('data-ctoken')) parts.push(n.getAttribute('data-label')||'');
|
||
else parts.push(n.textContent||'');
|
||
}
|
||
}
|
||
return parts.join('').replace(/\u00a0/g, ' ').replace(/[ \t]+\n/g, '\n').trim();
|
||
},
|
||
composerMentions(){
|
||
var el = this._composerEl();
|
||
var out = [], seen = {};
|
||
if(el){
|
||
var spans = el.querySelectorAll('[data-ctoken]');
|
||
for(var i=0;i<spans.length;i++){
|
||
if(spans[i].getAttribute('data-kind') === 'skill') continue;
|
||
var t = spans[i].getAttribute('data-token');
|
||
if(t && !seen[t]){ seen[t]=1; out.push(t); }
|
||
}
|
||
}
|
||
for(i=0;i<this.contextChips.length;i++){
|
||
var ch = this.contextChips[i];
|
||
if(ch.kind==='mention' && !seen[ch.token]){ seen[ch.token]=1; out.push(ch.token); }
|
||
}
|
||
return out;
|
||
},
|
||
resizeInput(){
|
||
var t = this._composerEl();
|
||
if(!t) return;
|
||
t.style.height = 'auto';
|
||
var h = Math.min(t.scrollHeight, 220);
|
||
t.style.height = h + 'px';
|
||
t.style.overflowY = (t.scrollHeight > 220) ? 'auto' : 'hidden';
|
||
},
|
||
_emptyCheck(){
|
||
var el = this._composerEl();
|
||
if(!el) return;
|
||
var empty = !el.textContent.trim() && !el.querySelector('[data-ctoken]');
|
||
el.classList.toggle('empty', !!empty);
|
||
},
|
||
setComposerText(str){
|
||
var el = this._composerEl();
|
||
if(!el){ this.input = str || ''; return; }
|
||
el.textContent = str || '';
|
||
this.input = str || '';
|
||
this.resizeInput();
|
||
this._emptyCheck();
|
||
},
|
||
clearComposer(){
|
||
var el = this._composerEl();
|
||
if(el){ el.textContent = ''; }
|
||
this.input = '';
|
||
this.resizeInput();
|
||
this._emptyCheck();
|
||
},
|
||
_focusComposer(){
|
||
var t = this._composerEl();
|
||
if(t) t.focus();
|
||
},
|
||
insertNewline(){
|
||
var el = this._composerEl();
|
||
if(!el) return;
|
||
try{
|
||
var sel = window.getSelection();
|
||
if(!sel.rangeCount) return;
|
||
var r = sel.getRangeAt(0);
|
||
r.deleteContents();
|
||
var br = document.createElement('br');
|
||
r.insertNode(br);
|
||
r.setStartAfter(br); r.collapse(true);
|
||
this.input = this.composerText();
|
||
}catch(e){}
|
||
this.resizeInput();
|
||
},
|
||
_caretQuery(){
|
||
try{
|
||
var sel = window.getSelection();
|
||
if(!sel || !sel.rangeCount || !sel.isCollapsed) return null;
|
||
var r = sel.getRangeAt(0);
|
||
var node = r.startContainer;
|
||
if(node.nodeType !== 3) return null;
|
||
var s = node.nodeValue, off = r.startOffset, i = off;
|
||
while(i > 0 && s[i-1] !== '@' && !/\s/.test(s[i-1])) i--;
|
||
if(i > 0 && s[i-1] === '@' && (i === 1 || /\s/.test(s[i-2]))){
|
||
return {node: node, at: i-1, raw: s.slice(i, off)};
|
||
}
|
||
}catch(e){}
|
||
return null;
|
||
},
|
||
_expandToAt(range){
|
||
var node = range.startContainer;
|
||
if(node.nodeType !== 3) return;
|
||
var s = node.nodeValue, off = range.startOffset, i = off;
|
||
while(i > 0 && s[i-1] !== '@' && !/\s/.test(s[i-1])) i--;
|
||
if(i > 0 && s[i-1] === '@') range.setStart(node, i-1);
|
||
},
|
||
_insertToken(item, range){
|
||
var el = this._composerEl();
|
||
if(!el) return;
|
||
try{
|
||
var span = document.createElement('span');
|
||
span.setAttribute('contenteditable','false');
|
||
span.setAttribute('data-ctoken','1');
|
||
span.setAttribute('data-token', item.token);
|
||
span.setAttribute('data-label', item.label);
|
||
span.setAttribute('data-kind', item.kind || 'mention');
|
||
span.className = 'ment-token' + (item.kind === 'skill' ? ' skill' : '');
|
||
var ico = document.createElement('span'); ico.className='tk-ico'; ico.textContent = ' ' + (item.icon || '');
|
||
var lbl = document.createElement('span'); lbl.className='tk-lbl'; lbl.textContent = ' ' + item.label + ' ';
|
||
span.appendChild(ico); span.appendChild(lbl);
|
||
var useRange = (range && range.startContainer) ? range : null;
|
||
if(!useRange){
|
||
var sel = window.getSelection();
|
||
if(sel && sel.rangeCount){
|
||
var cand = sel.getRangeAt(0);
|
||
if(cand && el.contains(cand.startContainer)) useRange = cand;
|
||
}
|
||
}
|
||
if(useRange){
|
||
if(this._mentionFrom !== '+' && !range) this._expandToAt(useRange);
|
||
useRange.deleteContents();
|
||
useRange.insertNode(span);
|
||
var sp = document.createTextNode(' ');
|
||
useRange.setStartAfter(span); useRange.collapse(true);
|
||
useRange.insertNode(sp);
|
||
useRange.setStart(sp, 1); useRange.setEnd(sp, 1);
|
||
try{ var s2 = window.getSelection(); if(s2){ s2.removeAllRanges(); s2.addRange(useRange); } }catch(e2){}
|
||
} else {
|
||
if(el.textContent) el.appendChild(document.createTextNode(' '));
|
||
el.appendChild(span);
|
||
el.appendChild(document.createTextNode(' '));
|
||
try{ var r2 = document.createRange(); r2.selectNodeContents(el); r2.collapse(false); var s3 = window.getSelection(); if(s3){ s3.removeAllRanges(); s3.addRange(r2); } }catch(e3){}
|
||
}
|
||
}catch(e){}
|
||
this.input = this.composerText();
|
||
this.resizeInput();
|
||
this._emptyCheck();
|
||
},
|
||
_insertMentionAt(item){
|
||
var node = this._atNode;
|
||
if(node && node.nodeType === 3 && node.parentNode){
|
||
try{
|
||
var s = node.nodeValue;
|
||
var idx = -1;
|
||
for(var k=0;k<s.length;k++){ if(s[k] === '@'){ idx = k; break; } }
|
||
if(idx >= 0){
|
||
var r = document.createRange();
|
||
r.setStart(node, idx);
|
||
r.setEnd(node, s.length);
|
||
this._insertToken(item, r);
|
||
return;
|
||
}
|
||
}catch(e){}
|
||
}
|
||
this._insertToken(item);
|
||
},
|
||
_removeTokenSpan(token){
|
||
var el = this._composerEl();
|
||
if(!el) return;
|
||
var spans = el.querySelectorAll('[data-ctoken][data-token="'+token+'"]');
|
||
for(var i=0;i<spans.length;i++){
|
||
var p = spans[i];
|
||
if(p.parentNode){
|
||
if(p.nextSibling && p.nextSibling.nodeType===3 && /^\s*$/.test(p.nextSibling.nodeValue)) p.parentNode.removeChild(p.nextSibling);
|
||
p.parentNode.removeChild(p);
|
||
}
|
||
}
|
||
this.input = this.composerText();
|
||
this.resizeInput();
|
||
this._emptyCheck();
|
||
},
|
||
_prevToken(node, off){
|
||
if(node.nodeType === 3 && off === 0){
|
||
var p = node.previousSibling;
|
||
while(p && p.nodeType===3 && !p.nodeValue.trim()) p = p.previousSibling;
|
||
if(p && p.nodeType===1 && p.hasAttribute && p.hasAttribute('data-ctoken')) return p;
|
||
}
|
||
return null;
|
||
},
|
||
_nextToken(node, off){
|
||
if(node.nodeType === 3 && off === node.nodeValue.length){
|
||
var p = node.nextSibling;
|
||
while(p && p.nodeType===3 && !p.nodeValue.trim()) p = p.nextSibling;
|
||
if(p && p.nodeType===1 && p.hasAttribute && p.hasAttribute('data-ctoken')) return p;
|
||
}
|
||
return null;
|
||
},
|
||
_removeTokenDom(span){
|
||
var token = span.getAttribute('data-token');
|
||
if(span.parentNode){
|
||
if(span.previousSibling && span.previousSibling.nodeType===3 && /^\s*$/.test(span.previousSibling.nodeValue)) span.parentNode.removeChild(span.previousSibling);
|
||
if(span.nextSibling && span.nextSibling.nodeType===3 && /^\s*$/.test(span.nextSibling.nodeValue)) span.parentNode.removeChild(span.nextSibling);
|
||
span.parentNode.removeChild(span);
|
||
}
|
||
for(var i=0;i<this.contextChips.length;i++){ if(this.contextChips[i].token === token){ this.contextChips.splice(i,1); break; } }
|
||
this.input = this.composerText();
|
||
this.resizeInput();
|
||
this._emptyCheck();
|
||
},
|
||
onPaste(e){
|
||
e.preventDefault();
|
||
var text = '';
|
||
try{ if(e.clipboardData) text = e.clipboardData.getData('text/plain') || ''; }catch(err){ }
|
||
if(!text) return;
|
||
try{
|
||
var sel = window.getSelection();
|
||
if(sel && sel.rangeCount){ sel.getRangeAt(0).deleteContents(); document.execCommand('insertText', false, text); }
|
||
}catch(err){}
|
||
this.input = this.composerText();
|
||
this.resizeInput();
|
||
},
|
||
get canSend(){ return !!((this.input || '').trim() || this.contextChips.length); },
|
||
get mentionChips(){ return (this.contextChips || []).filter(function(c){ return c.kind === 'mention'; }); },
|
||
get skillChips(){ return (this.contextChips || []).filter(function(c){ return c.kind === 'skill'; }); },
|
||
get commandOpen(){ return /^\s*\//.test(this.input) && !this.streaming; },
|
||
get commandSuggestions(){
|
||
var raw = (this.input||'').trim();
|
||
if(!/^\//.test(raw)) return [];
|
||
var q = raw.replace(/^\/+/, '').toLowerCase();
|
||
var first = (q.split(/\s+/).filter(Boolean)[0] || '').replace(/[^a-z0-9-]/g, '');
|
||
function qmatch(name){
|
||
var slug = String(name).toLowerCase().split(/[\s<]/)[0].replace(/^\/+/, '');
|
||
if(!first) return true;
|
||
return slug.indexOf(first) === 0 || slug.replace(/-/g, '').indexOf(first) === 0;
|
||
}
|
||
var out = [];
|
||
var seq = 0;
|
||
function pushItems(items){
|
||
items.forEach(function(o){ o._i = seq++; out.push(o); });
|
||
}
|
||
var skillsPresent = false, admPresent = false;
|
||
var built = [], dbs = [], adm = [];
|
||
for(var i=0;i<FD_SKILLS.length;i++){
|
||
if(qmatch(FD_SKILLS[i].cmd)){
|
||
var s = FD_SKILLS[i];
|
||
built.push({key:'bi-'+s.cmd, cmd:s.cmd, desc:s.desc, icon:s.icon, template:s.template, _type:'builtin'});
|
||
}
|
||
}
|
||
for(i=0;i<(this.skills||[]).length;i++){
|
||
var sk = this.skills[i];
|
||
if(qmatch(sk.name)){
|
||
dbs.push({key:'db-'+sk.id, cmd:'/'+sk.name, desc:(sk.description || 'Skill enregistré'), icon:'✨', _type:'db', skillId:sk.id});
|
||
}
|
||
}
|
||
for(i=0;i<FD_ADMIN_CMDS.length;i++){
|
||
if(qmatch(FD_ADMIN_CMDS[i].cmd)) adm.push({key:'adm-'+FD_ADMIN_CMDS[i].cmd, cmd:FD_ADMIN_CMDS[i].cmd, desc:FD_ADMIN_CMDS[i].desc});
|
||
}
|
||
skillsPresent = built.length || dbs.length;
|
||
admPresent = adm.length;
|
||
if(skillsPresent){
|
||
out.push({_sep:'Skills', key:'sep-skills'});
|
||
pushItems(built.concat(dbs));
|
||
}
|
||
if(skillsPresent && admPresent) out.push({_sep:'Commandes', key:'sep-adm'});
|
||
if(admPresent) pushItems(adm);
|
||
this._slashSeq = seq;
|
||
return out;
|
||
},
|
||
// Items slash « exécutables » (skills) — non séparateurs
|
||
_slashItems(){
|
||
return this.commandSuggestions.filter(function(o){ return !o._sep; });
|
||
},
|
||
_slugSkill(slug){
|
||
slug = String(slug || '').toLowerCase().replace(/^\/+/, '');
|
||
for(var i=0;i<FD_SKILLS.length;i++){
|
||
var c = FD_SKILLS[i].cmd.replace(/^\/+/, '').toLowerCase();
|
||
if(c === slug || c.replace(/-/g, '') === slug) return FD_SKILLS[i];
|
||
}
|
||
return null;
|
||
},
|
||
useSuggestion(c){
|
||
var self = this;
|
||
if(!c || c._sep) return;
|
||
if(c._admin){
|
||
this.setComposerText(c.cmd.replace(/\s*<[^>]*>\s*$/, ' '));
|
||
this.cmdFocus = -1;
|
||
this._focusComposer();
|
||
return;
|
||
}
|
||
if(c._type === 'db'){
|
||
var exists = this.contextChips.some(function(ch){ return ch.kind === 'skill' && ch.skillId === c.skillId; });
|
||
if(exists){ this.toast('Ce skill est déjà épinglé au contexte.'); }
|
||
else {
|
||
var hasSkill = this.contextChips.some(function(ch){ return ch.kind === 'skill'; });
|
||
this.contextChips.push({key:'skill-'+c.skillId, kind:'skill', icon:c.icon||'✨',
|
||
label:String(c.cmd).replace(/^\//, ''), token:'skill:'+c.skillId, skillId:c.skillId});
|
||
this.clearComposer();
|
||
this._insertToken({token:'skill:'+c.skillId, label:String(c.cmd).replace(/^\//, ''), icon:c.icon||'✨', kind:'skill'});
|
||
this.toast('Skill épinglé — décrivez votre demande, il sera appliqué à l\u2019envoi.');
|
||
}
|
||
} else {
|
||
this.setComposerText(c.template || '');
|
||
this.toast('Complétez la demande puis envoyez (Entrée).');
|
||
}
|
||
this.cmdFocus = -1; this.slashDismiss = false;
|
||
this._focusComposer();
|
||
},
|
||
echo(msg){
|
||
this.messages.push({id:'cmd-'+Date.now()+Math.random(), role:'assistant', content: msg, model:''});
|
||
this.scrollBottom();
|
||
},
|
||
handleCommand(text){
|
||
var self = this;
|
||
var parts = text.replace(/^\/+/, '').trim().split(/\s+/);
|
||
var cmd = (parts.shift() || '').toLowerCase();
|
||
var arg = parts.join(' ').trim();
|
||
if(cmd === 'help' || cmd === 'aide' || cmd === '?'){
|
||
this.echo('Tapez / pour choisir un skill (research, resume, brainstorm, plan, meeting-note…). Commandes : /providers · /keys · /provider <nom> · /model <nom>');
|
||
return true;
|
||
}
|
||
if(cmd === 'providers'){
|
||
var list = this.providers.filter(function(x){ return x.id !== 'offline'; })
|
||
.map(function(x){ return x.id + (x.verified ? ' ✓' : ''); }).join(', ');
|
||
this.echo('Fournisseurs actifs : ' + (list || 'aucun — configurez-en un dans Paramètres → Agent & IA'));
|
||
return true;
|
||
}
|
||
if(cmd === 'keys'){
|
||
var saved = this.llmKeys.map(function(k){ return k.provider + (k.verified ? ' ✓' : ' (à tester)'); }).join(', ');
|
||
this.echo('Clés enregistrées : ' + (saved || 'aucune — configurez-les dans Paramètres → Agent & IA'));
|
||
return true;
|
||
}
|
||
if(cmd === 'provider'){
|
||
var avail = this.providers.map(function(x){ return x.id; });
|
||
if(!arg){
|
||
this.echo('Utilisez : /provider <nom>. Fournisseurs : ' + avail.join(', '));
|
||
return true;
|
||
}
|
||
var p = this.providers.find(function(x){ return x.id === arg.toLowerCase(); });
|
||
if(!p){
|
||
this.echo('Fournisseur inconnu : ' + arg + ' (disponibles : ' + avail.join(', ') + ')');
|
||
return true;
|
||
}
|
||
this.llmProvider = p.id;
|
||
this.onProviderChange();
|
||
if(p.models && p.models.length && this.providerModels.indexOf(this.llmModel) === -1){
|
||
this.llmModel = (p.default_model) || p.models[0];
|
||
}
|
||
this.patchConversation();
|
||
this.echo('Provider activé : ' + p.id + ' · modèle : ' + (this.llmModel || 'défaut'));
|
||
return true;
|
||
}
|
||
if(cmd === 'model'){
|
||
if(!arg){
|
||
this.echo('Utilisez : /model <nom>. Modèles pour ' + this.llmProvider + ' : ' + this.providerModels.join(', '));
|
||
return true;
|
||
}
|
||
this.llmModel = arg;
|
||
this.patchConversation();
|
||
this.echo('Modèle activé : ' + arg + ' (' + this.llmProvider + ')');
|
||
return true;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// ── Toast ──
|
||
toast(msg, err){
|
||
var self = this;
|
||
this.toastMsg = msg || '';
|
||
this.toastErr = !!err;
|
||
if(this._toastTimer) clearTimeout(this._toastTimer);
|
||
if(!msg){ return; }
|
||
this._toastTimer = setTimeout(function(){ self.toastMsg = ''; self.toastErr = false; }, 3200);
|
||
},
|
||
|
||
// ── Mentions & contexte épinglé (@ / +) ──
|
||
toggleAddPicker(){
|
||
if(this.mentionOpen && this._mentionFrom === '+'){ this.mentionOpen = false; this.mentionQuery = ''; return; }
|
||
this.openMentionPicker('+');
|
||
},
|
||
openMentionPicker(from){
|
||
this.mentionOpen = true;
|
||
this._mentionFrom = from || '+';
|
||
this.mentionQuery = '';
|
||
this.mentionFocus = -1;
|
||
this.searchMentions('');
|
||
},
|
||
searchMentions(q){
|
||
var self = this;
|
||
q = (q || '').trim();
|
||
this.mentionQuery = q;
|
||
this.mentionLoad = true;
|
||
if(this._mentionTimer) clearTimeout(this._mentionTimer);
|
||
this._mentionTimer = setTimeout(function(){
|
||
fetch('/api/agent/mentions?q=' + encodeURIComponent(q))
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){
|
||
self.mentionItems = (d.mentions || []).map(function(it, idx){
|
||
it._i = idx;
|
||
return it;
|
||
});
|
||
self.mentionLoad = false;
|
||
if(self.mentionItems.length && self.mentionFocus < 0) self.mentionFocus = 0;
|
||
})
|
||
.catch(function(){ self.mentionItems = []; self.mentionLoad = false; });
|
||
}, q ? 180 : 0);
|
||
},
|
||
addChip(item){
|
||
if(!item || !item.token) return;
|
||
var dup = this.contextChips.some(function(ch){ return ch.token === item.token; });
|
||
if(dup){ this.toast('Déjà dans le contexte'); return; }
|
||
this.contextChips.push({key: item.token, kind: 'mention', icon: item.icon || '📎',
|
||
label: item.label, token: item.token, type: item.type, refId: item.id});
|
||
},
|
||
pickMention(item){
|
||
if(!item) return;
|
||
this.addChip(item);
|
||
if(this._mentionFrom === '@'){
|
||
this._insertMentionAt(item);
|
||
}
|
||
this.mentionOpen = false;
|
||
this.mentionQuery = '';
|
||
this._mentionFrom = null;
|
||
this._atNode = null;
|
||
this._focusComposer();
|
||
},
|
||
removeChip(token){
|
||
var idx = -1;
|
||
for(var i=0;i<this.contextChips.length;i++){
|
||
if(this.contextChips[i].token === token){ idx = i; break; }
|
||
}
|
||
if(idx < 0) return;
|
||
var chip = this.contextChips[idx];
|
||
this.contextChips.splice(idx, 1);
|
||
this._removeTokenSpan(chip.token);
|
||
this._emptyCheck();
|
||
},
|
||
onInput(){
|
||
this.resizeInput();
|
||
this.slashDismiss = false;
|
||
var text = this.composerText();
|
||
this.input = text;
|
||
var cmdNow = /^\s*\//.test(text);
|
||
if(cmdNow){
|
||
if(!this._cmdWasOpen){
|
||
var firstSkill = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
|
||
this.cmdFocus = firstSkill ? firstSkill._i : -1;
|
||
} else if(this.cmdFocus !== -1){
|
||
var present = (this.commandSuggestions || []).some(function(o){ return !o._sep && o._i === this.cmdFocus; }.bind(this));
|
||
if(!present){
|
||
var fs = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
|
||
this.cmdFocus = fs ? fs._i : -1;
|
||
}
|
||
}
|
||
} else {
|
||
this.cmdFocus = -1;
|
||
}
|
||
this._cmdWasOpen = cmdNow;
|
||
// @ mention inline
|
||
var cq = this._caretQuery();
|
||
if(cq){
|
||
if(this._mentionFrom !== '@'){ this._mentionFrom = '@'; this._atNode = cq.node; }
|
||
this.mentionOpen = true;
|
||
if(this._mentionTimer) clearTimeout(this._mentionTimer);
|
||
this.mentionLoad = true;
|
||
this.searchMentions(cq.raw);
|
||
} else if(this._mentionFrom === '@'){
|
||
this.mentionOpen = false;
|
||
this.mentionQuery = '';
|
||
}
|
||
this._emptyCheck();
|
||
},
|
||
moveMentionFocus(delta){
|
||
if(!this.mentionItems.length) return;
|
||
var n = this.mentionItems.length;
|
||
var idx = this.mentionItems.findIndex(function(it){ return it._i === this.mentionFocus; }.bind(this));
|
||
if(idx < 0) idx = delta > 0 ? -1 : 0;
|
||
idx = (idx + delta + n) % n;
|
||
this.mentionFocus = this.mentionItems[idx]._i;
|
||
},
|
||
moveSlashFocus(delta){
|
||
var items = this._slashItems().filter(function(o){ return !o._admin; });
|
||
if(!items.length) return;
|
||
var idx = items.findIndex(function(o){ return o._i === this.cmdFocus; }.bind(this));
|
||
if(idx < 0) idx = delta > 0 ? -1 : 0;
|
||
idx = (idx + delta + items.length) % items.length;
|
||
this.cmdFocus = items[idx]._i;
|
||
},
|
||
onComposerKeydown(e){
|
||
var key = e.key;
|
||
if(key === 'Backspace' || key === 'Delete'){
|
||
try{
|
||
var sel = window.getSelection();
|
||
if(sel && sel.rangeCount){
|
||
var r = sel.getRangeAt(0);
|
||
if(r.collapsed){
|
||
var tok = key === 'Backspace'
|
||
? this._prevToken(r.startContainer, r.startOffset)
|
||
: this._nextToken(r.startContainer, r.startOffset);
|
||
if(tok){ e.preventDefault(); this._removeTokenDom(tok); return; }
|
||
}
|
||
}
|
||
}catch(err){}
|
||
}
|
||
if(key === 'Escape'){
|
||
if(this.mentionOpen){ e.preventDefault(); e.stopPropagation(); this.mentionOpen = false; this._mentionFrom = null; this._atNode = null; return; }
|
||
if(this.commandOpen){ e.preventDefault(); e.stopPropagation(); this.slashDismiss = true; this.cmdFocus = -1; return; }
|
||
return; // laisse fermer le panneau
|
||
}
|
||
if(key === 'ArrowDown' || key === 'ArrowUp'){
|
||
var delta = key === 'ArrowDown' ? 1 : -1;
|
||
if(this.mentionOpen && this.mentionItems.length){ e.preventDefault(); this.moveMentionFocus(delta); return; }
|
||
if(this.commandOpen && !this.slashDismiss && this._slashItems().length){ e.preventDefault(); this.moveSlashFocus(delta); return; }
|
||
return;
|
||
}
|
||
// Entrée / Tab avec un menu ouvert (skills / mentions)
|
||
if(this.mentionOpen && this.mentionItems.length){
|
||
if(key === 'Enter' || key === 'Tab'){
|
||
e.preventDefault();
|
||
var mi = this.mentionItems.find(function(it){ return it._i === this.mentionFocus; }.bind(this)) || this.mentionItems[0];
|
||
this.pickMention(mi);
|
||
return;
|
||
}
|
||
}
|
||
if(this.commandOpen && !this.slashDismiss && this._slashItems().length){
|
||
var items = this._slashItems();
|
||
if(key === 'Enter' || key === 'Tab'){
|
||
var focusItem = items.find(function(o){ return o._i === this.cmdFocus; }.bind(this));
|
||
if(focusItem){
|
||
if(!focusItem._admin){ e.preventDefault(); this.useSuggestion(focusItem); return; }
|
||
e.preventDefault(); this.send(); return;
|
||
}
|
||
var queryTyped = (this.composerText() || '').replace(/^\/+/, '').trim().length > 0;
|
||
var onlyAdmin = items.every(function(o){ return o._admin; });
|
||
var onlySkill = !onlyAdmin && items.every(function(o){ return !o._admin; });
|
||
if(onlySkill && queryTyped){
|
||
e.preventDefault(); this.useSuggestion(items[0]); return;
|
||
}
|
||
if(onlyAdmin){
|
||
e.preventDefault(); this.send(); return;
|
||
}
|
||
e.preventDefault(); return;
|
||
}
|
||
}
|
||
// Entrée : envoyer (Maj+Entrée = nouvelle ligne)
|
||
if(key === 'Enter'){
|
||
if(e.shiftKey){ e.preventDefault(); this.insertNewline(); }
|
||
else { e.preventDefault(); this.send(); }
|
||
return;
|
||
}
|
||
},
|
||
|
||
// ── Actions sous chaque réponse agent ──
|
||
copyResponse(m){
|
||
var self = this;
|
||
var text = (m && m.content) || '';
|
||
if(!text) return;
|
||
function fallback(){
|
||
try{
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
var ok = document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
self.toast(ok ? 'Réponse copiée.' : 'Copie impossible.', !ok);
|
||
}catch(err){ self.toast('Copie impossible.', true); }
|
||
}
|
||
if(navigator.clipboard && navigator.clipboard.writeText){
|
||
navigator.clipboard.writeText(text).then(function(){ self.toast('Réponse copiée.'); }).catch(function(){ fallback(); });
|
||
} else { fallback(); }
|
||
},
|
||
insertIntoPage(m){
|
||
var c = FlowDeckCtx.current();
|
||
if(!c || c.kind !== 'page'){ this.toast('Aucune page ouverte pour insérer cette réponse.', true); return; }
|
||
if(!window.fdApplyDocument){ this.toast('Insertion indisponible ici.', true); return; }
|
||
var text = (m && m.content) || '';
|
||
if(!text){ return; }
|
||
var ok = window.fdApplyDocument(text, m && m.mode && !m.applied ? m.mode : 'insert');
|
||
this.toast(ok ? 'Réponse insérée dans la page.' : 'Insertion impossible (page non éditable).', !ok);
|
||
},
|
||
feedback(m, rating){
|
||
var self = this;
|
||
if(!m || m.fb) return;
|
||
var payload = {rating: rating, content: ((m.content) || '').slice(0, 1000)};
|
||
if(this.currentConv && this.currentConv.id) payload.conversation_id = this.currentConv.id;
|
||
if(m.id && /^\d+$/.test(String(m.id))) payload.message_id = Number(m.id);
|
||
fetch('/api/agent/feedback', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
}).then(function(r){ return r.json(); }).then(function(d){
|
||
if(d && d.status === 'recorded'){ m.fb = rating; }
|
||
self.toast(rating === 'up' ? 'Merci pour votre retour 👍' : 'Retour enregistré 👎', false);
|
||
}).catch(function(){ self.toast('Retour non envoyé.', true); });
|
||
},
|
||
|
||
// ── Send / SSE ──
|
||
async send(){
|
||
var self = this;
|
||
if(self.streaming) return;
|
||
var text = (self.input || '').trim();
|
||
if(!text && !self.contextChips.length) return;
|
||
if(self.commandOpen && self.handleCommand(text)){ self.clearComposer(); return; }
|
||
// skill intégré tapé « en dur » (« /research », « /meeting-note … ») → applique son modèle
|
||
if(self.commandOpen){
|
||
var firstTok = (text.split(/\s+/)[0] || '');
|
||
var sk = self._slugSkill(firstTok);
|
||
if(sk){
|
||
var rest = text.slice(String(firstTok).length).trim();
|
||
text = sk.template + (rest ? '\n\n' + rest : '');
|
||
}
|
||
}
|
||
// barre oblique seule (après Escape du menu /) → on ne l'envoie pas
|
||
if(/^\/+$/.test(text)){ self.clearComposer(); return; }
|
||
var ment = self.composerMentions();
|
||
if(!text){
|
||
if(ment.length){ text = 'Analyse le contexte référencé.'; }
|
||
else { return; }
|
||
}
|
||
try { await self.ensureConversation(); } catch(e){ return; }
|
||
var rawText = text;
|
||
self.clearComposer();
|
||
self.errorMsg = '';
|
||
self.mentionOpen = false;
|
||
|
||
// A writing/content request typed while a document is open → route it to
|
||
// headless text generation and offer the result with Apply / Reject,
|
||
// instead of sending the agent into tool calls (search/create).
|
||
if(rawText && self.activeContext && self.activeContext.kind === 'page' && self._isContentRequest(rawText)){
|
||
self.refreshContext();
|
||
return self.runDocAction({prompt: rawText, label: rawText, mode: self._docModeFor(rawText)});
|
||
}
|
||
|
||
self.streaming = true;
|
||
self.liveReasoning = '…';
|
||
if(rawText) self.messages.push({id:'u-'+Date.now(), role:'user', content: rawText});
|
||
self.scrollBottom();
|
||
|
||
var payload = {message: rawText};
|
||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||
if(self.llmModel) payload.model = self.llmModel;
|
||
var ctxText = self.contextPayload();
|
||
if(ctxText) payload.context = ctxText;
|
||
if(ment && ment.length) payload.mentions = ment;
|
||
var skillChip = (self.contextChips || []).find(function(ch){ return ch.kind === 'skill'; });
|
||
if(skillChip && skillChip.skillId) payload.skill_id = skillChip.skillId;
|
||
|
||
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
|
||
method:'POST',
|
||
headers:{'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'){ self.liveReasoning = ev.content; }
|
||
else if(ev.type==='action'){
|
||
var msg = ev.status==='success'
|
||
? (ev.tool+' ✓ '+(ev.message||''))
|
||
: (ev.tool+' ✗ '+(ev.detail||'erreur'));
|
||
self.messages.push({id:'a-'+Date.now()+Math.random(), role:'assistant', content: msg, act:true});
|
||
}
|
||
else if(ev.type==='final'){
|
||
self.messages.push({id:'f-'+Date.now(), role:'assistant', content: ev.content});
|
||
if(window.__fdInsertAI){ try{ var h=window.__fdInsertAI; window.__fdInsertAI=null; h(ev.content||''); }catch(e){} }
|
||
}
|
||
else if(ev.type==='error'){ self.errorMsg = ev.message; }
|
||
}catch(e){}
|
||
});
|
||
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.fetchConversationsRefresh();
|
||
});
|
||
},
|
||
fetchConversationsRefresh(){
|
||
var self = this;
|
||
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){ self.conversations = d.conversations || []; }).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;
|
||
|
||
// ═══════════ Context reader — document (page editor) ═══════════
|
||
window.fdReadPageContext = function(){
|
||
try{
|
||
var tEl = document.getElementById('_titleEl');
|
||
if(!tEl) return '';
|
||
var title = (tEl.textContent || '').trim();
|
||
var ct = document.getElementById('_blocksCt');
|
||
var text = '';
|
||
if(ct){
|
||
var kids = ct.children || [];
|
||
for(var i = 0; i < kids.length; i++){
|
||
var el = kids[i];
|
||
if(!el || !el.textContent) continue;
|
||
var t = el.textContent.replace(/\s+/g, ' ').trim();
|
||
if(t) text += t + '\n';
|
||
}
|
||
text = text.trim();
|
||
}
|
||
if(!title && !text) return '';
|
||
var out = title ? ('Titre du document : ' + title) : 'Document ouvert';
|
||
if(text) out += '\n\n' + text;
|
||
return out.slice(0, 9000);
|
||
}catch(e){ return ''; }
|
||
};
|
||
if(!window.fdGetPageContext) window.fdGetPageContext = window.fdReadPageContext;
|
||
|
||
// ═══════════ FlowDeckCtx — universal context detection ═══════════
|
||
// Priority: (1) an explicit marker set by a screen (window.fdViewContext /
|
||
// data-fd-context), (2) the currently open document (page editor),
|
||
// (3) the route the user is on. Every screen of the app yields a context.
|
||
var FlowDeckCtx = (function(){
|
||
function idFromPath(prefix){
|
||
try{
|
||
var m = (window.location.pathname || '').match(new RegExp('^\\/?' + prefix + '\\/(\\d+)'));
|
||
return m ? m[1] : null;
|
||
}catch(e){ return null; }
|
||
}
|
||
function labelFromTitle(){
|
||
try{
|
||
var t = document.title || '';
|
||
t = t.replace(/^\s*FlowDeck\s*-\s*/i, '');
|
||
t = t.replace(/\s*[-|]\s*FlowDeck\s*$/i, '');
|
||
if(t && t.toLowerCase() !== 'home' && t.toLowerCase() !== 'flowdeck') return t;
|
||
}catch(e){}
|
||
return '';
|
||
}
|
||
function docContext(){
|
||
try{
|
||
if(window.fdGetPageContext){
|
||
var raw = String(window.fdGetPageContext() || '').trim();
|
||
if(!raw) return null;
|
||
var firstLine = raw.split('\n')[0] || 'Document';
|
||
var title = firstLine.replace(/^Titre du document\s*:\s*/i, '').trim() || 'Document en cours';
|
||
var pid = idFromPath('pages');
|
||
return {
|
||
kind:'page', icon:'📄', label:'Document « ' + title + ' »', text: raw, key:'page',
|
||
title: title,
|
||
mentionToken: pid ? ('document:' + pid) : null,
|
||
refId: pid ? Number(pid) : null,
|
||
refType: pid ? 'document' : null
|
||
};
|
||
}
|
||
}catch(e){}
|
||
return null;
|
||
}
|
||
function marker(){
|
||
try{
|
||
var m = window.fdViewContext;
|
||
if(m && (m.label || m.detail)){
|
||
var label = m.label || m.detail;
|
||
return {kind: m.kind || 'section', icon: m.icon || '📍', label: String(label), text: m.text || '', key: 'marker'};
|
||
}
|
||
var b = document.body;
|
||
if(b && b.getAttribute && b.getAttribute('data-fd-context')){
|
||
var lbl = b.getAttribute('data-fd-context');
|
||
return {kind:'section', icon:'📍', label:lbl, text:'', key:'marker'};
|
||
}
|
||
}catch(e){}
|
||
return null;
|
||
}
|
||
function byPath(){
|
||
var p = window.location.pathname || '/';
|
||
var icon = '📍';
|
||
var label = '';
|
||
var kind = 'section';
|
||
var t;
|
||
if(p === '/' || p === '/dashboard'){ icon='🏠'; label='Accueil'; kind='home'; }
|
||
else if(/^\/pages\/\d+/.test(p)){
|
||
icon='📄'; kind='page'; label=labelFromTitle() || 'Page en cours';
|
||
var pid = idFromPath('pages');
|
||
t = pid ? ('document:' + pid) : null;
|
||
}
|
||
else if(/^\/library/.test(p)){
|
||
icon='📚'; kind='library';
|
||
var tab = (window.location.search.match(/tab=([^&]+)/)||[])[1] || '';
|
||
label = tab ? ('Bibliothèque — ' + tab.charAt(0).toUpperCase() + tab.slice(1)) : 'Bibliothèque';
|
||
}
|
||
else if(/^\/trash/.test(p)){ icon='🗑️'; label='Corbeille'; kind='trash'; }
|
||
else if(/^\/my-tasks/.test(p)){ icon='✅'; label='My Tasks'; kind='tasks'; }
|
||
else if(/^\/workspaces/.test(p)){ icon='🗂️'; label='Espaces de travail'; kind='workspaces'; }
|
||
else if(/^\/local-workspace/.test(p)){ icon='🗂️'; label='Espace local'; kind='workspace'; }
|
||
else if(/^\/workspace($|\?)/.test(p)){ icon='🗂️'; label='Espace de travail'; kind='workspace'; }
|
||
else if(/^\/gitea-workspace/.test(p)){ icon='🔗'; label='Workspace Gitea'; kind='workspace'; }
|
||
else if(/^\/db\//.test(p)){
|
||
icon='🗄️'; kind='db';
|
||
label = labelFromTitle() || 'Base de données';
|
||
var cid = idFromPath('db');
|
||
t = cid ? ('collection:' + cid) : null;
|
||
}
|
||
else if(/^\/board\//.test(p)){
|
||
icon='📋'; kind='board';
|
||
if(/^\/board\/library/.test(p)){ icon='📚'; label='Bibliothèque'; kind='library'; }
|
||
else if(/^\/board\/trash/.test(p)){ icon='🗑️'; label='Corbeille'; kind='trash'; }
|
||
else { label='Board'; t = labelFromTitle(); if(t) label = t; }
|
||
}
|
||
else if(/^\/notes\//.test(p)){ icon='📝'; label='Notes'; kind='notes'; }
|
||
else if(/^\/settings/.test(p) || /^\/accounts\/settings/.test(p)){ icon='⚙️'; label='Paramètres'; kind='settings'; }
|
||
else if(/^\/accounts/.test(p)){ icon='👤'; label='Compte'; kind='account'; }
|
||
else if(/^\/help/.test(p)){ icon='❓'; label='Aide'; kind='help'; }
|
||
else {
|
||
label = labelFromTitle() || 'FlowDeck';
|
||
kind='section';
|
||
}
|
||
var mentionToken = (kind === 'page' && pid) ? ('document:' + pid) : ((kind === 'db' && cid) ? ('collection:' + cid) : null);
|
||
return {kind: kind, icon: icon, label: label, text: '', key: p,
|
||
title: label, mentionToken: mentionToken,
|
||
refId: mentionToken ? Number(String(mentionToken).split(':')[1]) : null,
|
||
refType: mentionToken ? String(mentionToken).split(':')[0] : null};
|
||
}
|
||
return {
|
||
current: function(){
|
||
var m = marker();
|
||
if(m) return m;
|
||
var d = docContext();
|
||
if(d) return d;
|
||
return byPath();
|
||
}
|
||
};
|
||
})();
|
||
window.FlowDeckCtx = FlowDeckCtx;
|
||
|
||
// Apply an AI-produced document change to the open page editor.
|
||
// mode 'replace' → remplace tout le contenu de la page ;
|
||
// mode 'insert' → insère la proposition en fin de page (comme Ask AI).
|
||
window.fdApplyDocument = function(text, mode){
|
||
var E = window.E;
|
||
if(!E || !E.blocks || typeof E.mkB !== 'function') return false;
|
||
text = (text || '').trim();
|
||
if(!text) return false;
|
||
try{
|
||
E.sync();
|
||
if(mode === 'replace'){
|
||
var nb = (typeof E.md2b === 'function') ? E.md2b(text) : null;
|
||
if(!nb || !nb.length){ E.blocks = [E.mkB('paragraph', text)]; }
|
||
else { E.blocks = nb; }
|
||
} else {
|
||
if(typeof E.applyAIBlocks === 'function'){ E.applyAIBlocks(text); }
|
||
else {
|
||
var lines = text.split(/\r?\n/).map(function(s){ return s.trim(); }).filter(Boolean);
|
||
var startEmpty = E.blocks.length === 1 && E.blocks[0].type === 'paragraph' && !(E.blocks[0].content || '').trim();
|
||
var nb2 = lines.map(function(l){ return E.mkB('paragraph', l); });
|
||
E.blocks = startEmpty ? nb2 : E.blocks.concat(nb2);
|
||
}
|
||
}
|
||
E.dirty = true;
|
||
if(E.autoSave) E.autoSave();
|
||
if(E.render) E.render();
|
||
return true;
|
||
}catch(e){ return false; }
|
||
};
|
||
|
||
// Backward-compatible detection hooks (banner + Ask AI)
|
||
window.fdPageContextProvider = function(){
|
||
var c = FlowDeckCtx.current();
|
||
return c && c.kind === 'page' ? c.text || '' : '';
|
||
};
|
||
})();
|
||
</script>
|