Files
flowdeck/app/templates/agent_panel.html
T
bruno d986959927
FlowDeck CI / lint (push) Successful in 47s
FlowDeck CI / test (push) Successful in 4m17s
FlowDeck CI / docker (push) Successful in 45s
fix(cloudflare): protect agent panel, workspaces & shell scripts from Rocket Loader; trust proxy headers
2026-09-13 09:51:54 -04:00

2198 lines
114 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# ════════════ 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(--text-primary,#37352F);color:var(--bg-primary,#fff);border:none;cursor:pointer;box-shadow:0 8px 26px rgba(0,0,0,.35);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(0,0,0,.45)}
#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:var(--text-primary,#37352F);color:var(--bg-primary,#fff);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 2px 8px rgba(0,0,0,.25)}
.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))}
/* History hover / long-press preview */
.fd-ap-preview{position:fixed;width:290px;max-width:calc(100vw - 24px);background:var(--bg-modal,#1c1c1c);border:1px solid var(--border,rgba(255,255,255,.16));border-radius:12px;box-shadow:0 16px 44px rgba(0,0,0,.6);z-index:1990;padding:11px 13px;font-size:12px;color:var(--text,#e6e6e6);display:none;pointer-events:auto}
.fd-ap-preview.show{display:block}
.fd-ap-preview-title{font-weight:600;font-size:12.5px;margin-bottom:2px;color:var(--text,#fff);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-ap-preview-meta{font-size:10px;color:var(--text-dim,#999);margin-bottom:8px}
.fd-ap-preview-empty{font-size:11px;color:var(--text-dim,#aaa);padding:4px 0}
.fd-ap-preview-label{font-size:9.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:7px 0 3px}
.fd-ap-preview-txt{white-space:pre-wrap;word-break:break-word;line-height:1.55;max-height:72px;overflow:hidden;color:var(--text-secondary,#d0d0d0)}
/* 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}
.fd-ap-notice{color:var(--text-secondary,#ccc);font-size:12px;padding:7px 10px;margin:2px 0 8px;border-radius:9px;background:rgba(128,128,128,.12);border:1px solid var(--border,rgba(255,255,255,.12))}
/* Steps accordion (Notion-style reasoning + actions audit trail) */
.fd-steps{margin:0 0 8px;border:1px solid var(--border,rgba(255,255,255,.11));border-radius:10px;background:rgba(255,255,255,.03);overflow:hidden}
.fd-steps-head{display:flex;align-items:center;gap:7px;width:100%;padding:7px 11px;background:transparent;border:none;color:var(--text-secondary,#ccc);font-size:12px;cursor:pointer;text-align:left}
.fd-steps-head:hover{background:rgba(255,255,255,.05)}
.fd-steps-chev{display:inline-block;font-size:9px;color:var(--text-dim,#999);transition:transform .15s;width:12px;flex-shrink:0;text-align:center}
.fd-steps-chev.down{transform:rotate(90deg)}
.fd-steps-count{font-weight:600;color:var(--text,#eee)}
.fd-steps-elem{font-weight:400;color:var(--text-dim,#999);margin-left:6px}
.fd-steps-body{border-top:1px solid var(--border,rgba(255,255,255,.08));padding:6px 8px 8px;max-height:340px;overflow-y:auto}
.fd-step-thought{margin:3px 0}
.fd-step-thought-head{display:flex;align-items:center;gap:6px;width:100%;border:none;background:transparent;color:var(--text-dim,#b9b9b9);font-size:12px;text-align:left;cursor:pointer;padding:4px 5px;border-radius:7px}
.fd-step-thought-head:hover{background:rgba(128,128,128,.1);color:var(--text,#fff)}
.fd-step-thought-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.fd-step-thought-title .fd-t-ico{opacity:.85}
.fd-step-thought-body{margin:2px 0 4px 27px;padding:7px 10px;font-size:12px;color:var(--text-secondary,#cfcfcf);background:rgba(128,128,128,.1);border-left:2px solid rgba(35,131,226,.45);border-radius:0 8px 8px 0;white-space:pre-wrap;word-break:break-word;line-height:1.55}
.fd-step-action{display:flex;align-items:flex-start;gap:7px;padding:4px 7px;font-size:12px;color:var(--text-secondary,#c9c9c9)}
.fd-step-action .fd-step-ico{flex-shrink:0;width:14px;text-align:center;font-size:11px;margin-top:1px}
.fd-step-action.success{color:var(--text,#d9d9d9)}
.fd-step-action.success .fd-step-ico{color:#58c47a}
.fd-step-action.error{color:#e78c8c}
.fd-step-action.error .fd-step-ico{color:#ef6a6a}
.fd-step-action-detail{font-size:11px;color:#e0a3a3;display:block;margin-top:1px}
/* 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.focus{background:var(--bg-hover,#2b2b2b);box-shadow:inset 2px 0 0 var(--accent,#2383E2);color:var(--text,#fff)}
.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.chip-open{cursor:pointer}
.fd-chip.chip-open:hover{background:rgba(35,131,226,.20);border-color:rgba(35,131,226,.55)}
.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-sep{font-size:10px;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim,#888);padding:8px 14px 2px}
.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}
.fd-agent-msg-markdown{line-height:1.6}
.fd-agent-msg-markdown p{margin:0 0 8px}
.fd-agent-msg-markdown h1,.fd-agent-msg-markdown h2,.fd-agent-msg-markdown h3,.fd-agent-msg-markdown h4{margin:12px 0 6px;font-weight:700;line-height:1.25}
.fd-agent-msg-markdown h1{font-size:1.4em}
.fd-agent-msg-markdown h2{font-size:1.25em}
.fd-agent-msg-markdown h3{font-size:1.1em}
.fd-agent-msg-markdown h4{font-size:1em}
.fd-agent-msg-markdown ul,.fd-agent-msg-markdown ol{margin:0 0 8px;padding-left:20px}
.fd-agent-msg-markdown li{margin:2px 0}
.fd-agent-msg-markdown code{background:rgba(127,127,127,.18);border-radius:4px;padding:1px 5px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.9em}
.fd-agent-msg-markdown pre{background:var(--bg-code,#111);border:1px solid var(--border,#333);border-radius:8px;padding:12px;overflow-x:auto;margin:8px 0}
.fd-agent-msg-markdown pre code{background:none;padding:0;font-size:12px;line-height:1.5}
.fd-md-table-wrap{overflow-x:auto;margin:8px 0}
.fd-md-table{border-collapse:collapse;width:100%;font-size:13px}
.fd-md-table th,.fd-md-table td{border:1px solid var(--border,#333);padding:6px 10px;text-align:left}
.fd-md-table th{background:rgba(127,127,127,.14);font-weight:600}
.fd-md-table tr:nth-child(even) td{background:rgba(127,127,127,.06)}
.fd-md-quote{border-left:3px solid var(--accent,#2383E2);margin:8px 0;padding:2px 12px;color:var(--text-dim)}
.fd-md-quote p{margin:4px 0}
.fd-md-hr{border:none;border-top:1px solid var(--border,#333);margin:12px 0}
.fd-agent-msg-markdown a{color:var(--accent,#2383E2);text-decoration:none}
.fd-agent-msg-markdown a:hover{text-decoration:underline}
</style>
{# ── Global Agent API (available before Alpine for the FAB / shortcut) ── #}
<script data-cfasync="false">
(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()"><svg viewBox="0 0 24 24" width="26" height="26" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></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"><svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></span>
<div class="fd-ap-head-main">
<div class="fd-ap-title" x-text="currentConv && currentConv.title ? currentConv.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 &amp; 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="historyClick(c.id)"
@mouseenter="previewConversation(c, $event)" @mouseleave="schedulePreviewHide()"
@touchstart="startLongPress(c, $event)" @touchend="onTouchEnd()"
@touchmove="onTouchCancel()" @touchcancel="onTouchCancel()">
<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>
<!-- Steps accordion (reasoning thoughts + tool actions) -->
<div class="fd-steps" x-show="m.steps && m.steps.length" x-cloak>
<button class="fd-steps-head" type="button" @click="toggleSteps(m)">
<span class="fd-steps-chev" :class="{down: m.stepsOpen}">▶</span>
<span class="fd-steps-count" x-text="!m.steps ? '' : (m.steps.length === 1 ? '1 étape' : m.steps.length + ' étapes')"></span>
<span class="fd-steps-elem" x-text="m.generating ? 'en cours…' : (m.stepsOpen ? 'réduire' : 'voir le déroulé')"></span>
</button>
<div class="fd-steps-body" x-show="m.stepsOpen" x-cloak>
<template x-for="(s, si) in m.steps" :key="si">
<div>
<template x-if="s.type==='thought'">
<div class="fd-step-thought">
<button class="fd-step-thought-head" type="button" @click="toggleStep(s)">
<span class="fd-steps-chev" :class="{down: s.open}">▶</span>
<span class="fd-step-thought-title"><span class="fd-t-ico">💭</span> <span x-text="s.title || 'Réflexion'"></span></span>
</button>
<div class="fd-step-thought-body" x-show="s.open" x-text="s.content"></div>
</div>
</template>
<template x-if="s.type==='action'">
<div class="fd-step-action" :class="s.status || 'success'" :title="(s.detail && s.status==='error') ? s.detail : s.tool || ''">
<span class="fd-step-ico" x-text="(s.status==='success') ? '✓' : '✗'"></span>
<div>
<div x-text="s.label"></div>
<div class="fd-step-action-detail" x-show="s.status==='error' && s.detail" x-text="s.detail" x-cloak></div>
</div>
</div>
</template>
</div>
</template>
</div>
</div>
<div class="fd-agent-msg-content" x-show="m.generating" x-cloak>Génération…</div>
<div class="fd-agent-msg-content fd-agent-msg-markdown" x-show="!m.generating && m.role==='assistant' && m.content" x-html="renderMarkdown(m.content)"></div>
<div class="fd-agent-msg-content" x-show="!m.generating && m.role!=='assistant' && m.content" x-text="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 && !(_livePh && _livePh.steps && _livePh.steps.length)">L'agent réfléchit et agit… <span x-text="liveReasoning"></span></div>
<div class="fd-ap-notice" x-show="notice" x-cloak x-text="notice"></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" x-ref="cmdMenu" :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}" x-ref="mentionMenu">
<div class="fd-mention-load" x-show="mentionLoad" x-cloak>Recherche…</div>
<template x-if="!mentionLoad && !hasMentionItems">
<div class="fd-mention-empty">Aucun élément. Tapez un nom de document, page ou base.</div>
</template>
<template x-if="!mentionLoad">
<template x-for="it in mentionItems" :key="it.token || it.key">
<div>
<div class="fd-mention-sep" x-show="it._sep" x-text="it._sep"></div>
<div class="fd-mention-item" x-show="!it._sep" :class="{focus: it._i===mentionFocus}"
@click="pickMention(it)" @mouseenter="setMentionFocus(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>
</div>
</template>
</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 chip-open" :title="chipAction(chip)" @click="openChip(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.stop="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>
{# Historique : aperçu (hover / long-press) #}
<div class="fd-ap-preview" x-ref="convPreview" :class="{show: previewVisible}"
@mouseenter="cancelPreviewHide()" @mouseleave="schedulePreviewHide()" x-cloak>
<div class="fd-ap-preview-title" x-text="previewTitle"></div>
<div class="fd-ap-preview-meta" x-text="previewMeta"></div>
<div class="fd-ap-preview-empty" x-show="!previewStart && !previewEnd" x-text="previewEmptyHint"></div>
<div class="fd-ap-preview-label" x-show="previewStart" x-cloak>Début</div>
<div class="fd-ap-preview-txt" x-show="previewStart" x-text="previewStart"></div>
<div class="fd-ap-preview-label" x-show="previewEnd" x-cloak>Réponse</div>
<div class="fd-ap-preview-txt" x-show="previewEnd" x-text="previewEnd"></div>
</div>
</div>
<script data-cfasync="false">
(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: '', notice: '',
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,
_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:{'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<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 = '';
this.hidePreview();
fetch('/api/agent/conversations/'+id).then(function(r){return r.json()}).then(function(d){
self.currentConv = d.conversation;
self.messages = (d.messages||[]).map(function(m){ return {id:m.id, role:m.role, content:m.content, model:m.model, tool_calls:m.tool_calls_json}; });
var prov = d.conversation && d.conversation.provider;
if(prov && (!self.providers.length || self.providers.some(function(x){ return x.id === prov; }))){ self.llmProvider = prov; }
self.applyModels();
if(d.conversation && d.conversation.model && self.providerModels.indexOf(d.conversation.model) !== -1){ self.llmModel = d.conversation.model; }
// Rebuild the steps accordion from the audit trail (actions only —
// the reasoning thoughts are not persisted, only shown live).
self.attachPersistedSteps(id);
}).catch(function(){});
},
attachPersistedSteps(id){
var self = this;
fetch('/api/agent/conversations/'+id+'/actions').then(function(r){return r.json()}).then(function(d){
var acts = d.actions || [];
if(!acts.length) return;
var lastAsst = null;
for(var i=self.messages.length-1;i>=0;i--){
var m = self.messages[i];
if(m.role==='assistant' && !m.proposal){ lastAsst = m; break; }
}
if(!lastAsst) return;
var steps = acts.map(function(a){
var res = null;
try{ res = a.result_json ? JSON.parse(a.result_json) : {}; }catch(e){ res = {}; }
var msg = (res && res.message) ? res.message : '';
return agentPanel._actionStep({
tool: a.tool_name, status: a.status, target_id: a.target_id,
message: msg, detail: msg || ''
});
});
lastAsst.steps = steps;
lastAsst.stepsOpen = false;
}).catch(function(){});
},
get previewEmptyHint(){ return 'Aucun échange enregistré pour cette conversation.'; },
historyClick(id){
if(this._suppressNextClick){ this._suppressNextClick = false; return; }
this.loadConversation(id);
},
_previewSummary(msgs){
var first = '', last = '';
for(var i=0;i<msgs.length;i++){
var r = msgs[i].role;
var t = (msgs[i].content || '');
if(r==='user'){ if(!first && t.trim()) first = agentPanel._strip(t); }
else if(r==='assistant'){ if(t.trim()) last = agentPanel._strip(t); }
}
return { first: first, last: last, total: msgs.length };
},
previewConversation(c, ev){
var self = this;
// On touch: a quick tap triggers a synthetic mouseenter AFTER touchend —
// skip it so we don't flash the preview before navigating (the long-press
// timer handles the preview itself).
if(ev && ev.type === 'mouseenter' && self._suppressEnterUntil && Date.now() < self._suppressEnterUntil) return;
var rect = (ev && ev.currentTarget) ? ev.currentTarget.getBoundingClientRect() : null;
var cache = self._previewCache || (self._previewCache = {});
function show(sum){
if(!sum) return;
self.previewTitle = c.title || 'New conversation';
var dateStr = (c.updated_at || '').replace('T', ' ').slice(0, 16);
self.previewMeta = sum.total + ' message(s)' + (dateStr ? ' · ' + dateStr : '');
self.previewStart = sum.first || '';
self.previewEnd = sum.last || '';
self.previewVisible = true;
self.cancelLongPress();
self.$nextTick(function(){
var el = self.$refs && self.$refs.convPreview;
if(!el) return;
var pw = el.offsetWidth || 290;
var m = 10;
var x = rect ? (rect.left - pw - m) : m;
if(x < m && rect) x = rect.right + m;
var y = rect ? Math.min(rect.top, window.innerHeight - el.offsetHeight - m) : m;
if(y < m) y = m;
el.style.left = Math.round(x) + 'px';
el.style.top = Math.round(y) + 'px';
});
}
if(cache[c.id]){ show(cache[c.id]); return; }
fetch('/api/agent/conversations/'+c.id).then(function(r){ return r.json(); }).then(function(d){
var sum = self._previewSummary(d.messages || []);
cache[c.id] = sum;
show(sum);
}).catch(function(){ self.schedulePreviewHide(); });
},
schedulePreviewHide(){
var self = this;
if(this._previewTimer) clearTimeout(this._previewTimer);
this._previewTimer = setTimeout(function(){ self.previewVisible = false; self._previewTimer = null; }, 220);
},
cancelPreviewHide(){
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
},
startLongPress(c, ev){
var self = this;
this.cancelLongPress();
this._longPressTimer = setTimeout(function(){
self._suppressNextClick = true;
self.previewConversation(c, ev);
}, 550);
},
cancelLongPress(){
if(this._longPressTimer){ clearTimeout(this._longPressTimer); this._longPressTimer = null; }
},
onTouchEnd(){
// A quick tap: suppress the synthetic mouseenter preview flash right
// before the click navigates into the conversation.
this.cancelLongPress();
this._suppressEnterUntil = Date.now() + 400;
},
onTouchCancel(){
this.cancelLongPress();
},
hidePreview(){
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
this.previewVisible = false;
},
newConversation(){
var self = this;
var body = {title:'Nouvelle conversation'};
if(self.llmProvider) body.provider = self.llmProvider;
if(self.llmModel) body.model = self.llmModel;
fetch('/api/agent/conversations', {method:'POST', headers:{'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.notice=''; self.tab='chat';
self.contextChips = []; self.mentionOpen = false;
self.$nextTick(function(){ self.resizeInput(); });
}).catch(function(){});
},
ensureConversation(){
var self = this;
if(self.currentConv && self.currentConv.id) return Promise.resolve(self.currentConv);
if(self._ensuring) return self._ensuring;
var body = {title:'Nouvelle conversation'};
if(self.llmProvider) body.provider = self.llmProvider;
if(self.llmModel) body.model = self.llmModel;
self._ensuring = fetch('/api/agent/conversations', {
method:'POST', headers:{'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.notice=''; self.tab='chat';
self.contextChips = []; self.mentionOpen = false;
self._ensuring = null;
return conv;
}).catch(function(err){
self._ensuring = null;
self.errorMsg = (err && err.message) ? err.message : 'Impossible de créer la conversation';
throw err;
});
return self._ensuring;
},
patchConversation(){
var self = this;
if(!self.currentConv) return;
var body = {};
if(self.llmProvider) body.provider = self.llmProvider;
if(self.llmModel) body.model = self.llmModel;
fetch('/api/agent/conversations/'+self.currentConv.id, {
method:'PATCH', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body)
}).catch(function(){});
},
// ── Composer (contenteditable) helpers ──
_composerEl(){ return this.$refs && this.$refs.cmdInput; },
composerText(){
var el = this._composerEl();
if(!el) return (this.input || '');
var parts = [];
var nodes = el.childNodes;
for(var i=0;i<nodes.length;i++){
var n = nodes[i];
if(n.nodeType===3) parts.push(n.nodeValue);
else if(n.nodeType===1){
if(n.tagName === 'BR') parts.push('\n');
else if(n.hasAttribute && n.hasAttribute('data-ctoken')) parts.push(n.getAttribute('data-label')||'');
else parts.push(n.textContent||'');
}
}
return parts.join('').replace(/\u00a0/g, ' ').replace(/[ \t]+\n/g, '\n').trim();
},
composerMentions(){
var el = this._composerEl();
var out = [], seen = {};
if(el){
var spans = el.querySelectorAll('[data-ctoken]');
for(var i=0;i<spans.length;i++){
if(spans[i].getAttribute('data-kind') === 'skill') continue;
var t = spans[i].getAttribute('data-token');
if(t && !seen[t]){ seen[t]=1; out.push(t); }
}
}
for(i=0;i<this.contextChips.length;i++){
var ch = this.contextChips[i];
if(ch.kind==='mention' && !seen[ch.token]){ seen[ch.token]=1; out.push(ch.token); }
}
return out;
},
_composePrompt(text){
var chips = this.contextChips || [];
var segs = [];
// contextes épinglés (@ documents / bases / pages)
chips.forEach(function(ch){
if(ch.kind === 'mention'){ segs.push('Contexte « ' + (ch.label || '') + ' »'); }
});
// skills épinglés : template intégré, ou libellé pour un skill enregistré
// (son prompt complet est injecté via skill_ids côté serveur)
chips.forEach(function(ch){
if(ch.kind === 'skill'){
if(ch.builtin && ch.template){ segs.push(ch.template); }
else { segs.push('Skill « ' + (ch.label || '') + ' »'); }
}
});
var t = (text || '').trim();
if(t){ segs.push(t); }
return segs.join('\n\n');
},
resizeInput(){
var t = this._composerEl();
if(!t) return;
t.style.height = 'auto';
var h = Math.min(t.scrollHeight, 220);
t.style.height = h + 'px';
t.style.overflowY = (t.scrollHeight > 220) ? 'auto' : 'hidden';
},
_emptyCheck(){
var el = this._composerEl();
if(!el) return;
var empty = !el.textContent.trim() && !el.querySelector('[data-ctoken]');
el.classList.toggle('empty', !!empty);
},
setComposerText(str){
var el = this._composerEl();
if(!el){ this.input = str || ''; return; }
el.textContent = str || '';
this.input = str || '';
this.resizeInput();
this._emptyCheck();
},
clearComposer(){
var el = this._composerEl();
if(el){ el.textContent = ''; }
this.input = '';
this.resizeInput();
this._emptyCheck();
},
_focusComposer(){
var t = this._composerEl();
if(t) t.focus();
},
insertNewline(){
var el = this._composerEl();
if(!el) return;
try{
var sel = window.getSelection();
if(!sel.rangeCount) return;
var r = sel.getRangeAt(0);
r.deleteContents();
var br = document.createElement('br');
r.insertNode(br);
r.setStartAfter(br); r.collapse(true);
this.input = this.composerText();
}catch(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 pin = {key:'skill-'+c.skillId, kind:'skill', icon:c.icon||'✨',
label:String(c.cmd).replace(/^\//, ''), token:'skill:'+c.skillId, skillId:c.skillId};
this.contextChips.push(pin);
this.clearComposer();
this._insertToken({token:pin.token, label:pin.label, icon:pin.icon, kind:'skill'});
this.toast('Skill épinglé — décrivez votre demande, il sera appliqué à l\u2019envoi.');
}
} 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 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(){
var ws = (window.appState && window.appState.localWsId) || 0;
fetch('/api/agent/mentions?q=' + encodeURIComponent(q) + '&workspace_id=' + ws)
.then(function(r){ return r.json(); })
.then(function(d){
var flat = [], gi = 0;
(d.sections || []).forEach(function(grp){
var items = grp.items || [];
if(!items.length) return;
flat.push({_sep: grp.label || '', key: 'sep-' + (grp.key || gi), _kind: 'sep'});
items.forEach(function(it){ it._i = gi++; flat.push(it); });
});
self.mentionItems = flat;
self.mentionLoad = false;
var sel = self._selectableMentionItems();
if(sel.length) self.mentionFocus = sel[0]._i;
self.$nextTick(function(){ self._scrollMentionItem(); });
})
.catch(function(){ self.mentionItems = []; self.mentionLoad = false; });
}, q ? 180 : 0);
},
get hasMentionItems(){ return (this.mentionItems || []).some(function(it){ return !it._sep; }); },
_selectableMentionItems(){
return (this.mentionItems || []).filter(function(it){ return it && it._sep !== true && it._kind !== 'sep'; });
},
setMentionFocus(i){
this.mentionFocus = i;
this._scrollMentionItem();
},
_scrollMentionItem(){
var menu = this.$refs && this.$refs.mentionMenu;
if(!menu) return;
var el = menu.querySelector('.fd-mention-item.focus');
if(el && el.scrollIntoView){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
},
addChip(item){
if(!item || !item.token) return;
var dup = this.contextChips.some(function(ch){ return ch.token === item.token; });
if(dup){ this.toast('Déjà dans le contexte'); return; }
this.contextChips.push({key: item.token, kind: 'mention', icon: item.icon || '📎',
label: item.label, token: item.token, type: item.type, refId: item.id});
},
pickMention(item){
if(!item) return;
this.addChip(item);
if(this._mentionFrom === '@'){
this._insertMentionAt(item);
}
this.mentionOpen = false;
this.mentionQuery = '';
this._mentionFrom = null;
this._atNode = null;
this._focusComposer();
},
removeChip(token){
var idx = -1;
for(var i=0;i<this.contextChips.length;i++){
if(this.contextChips[i].token === token){ idx = i; break; }
}
if(idx < 0) return;
var chip = this.contextChips[idx];
this.contextChips.splice(idx, 1);
this._removeTokenSpan(chip.token);
this._emptyCheck();
},
chipHref(chip){
if(!chip || chip.kind !== 'mention') return null;
var id;
if(chip.refId != null) id = Number(chip.refId);
else {
var tk = String(chip.token || '').split(':');
if(tk.length === 2 && /^\d+$/.test(tk[1])) id = Number(tk[1]);
}
if(id == null || isNaN(id)) return null;
if(chip.type === 'collection') return '/db/' + id;
return '/pages/' + id; // document & pages de collection s'ouvrent dans l'éditeur
},
chipAction(chip){
if(!chip || chip.kind !== 'mention') return '';
return chip.type === 'collection' ? 'Ouvrir la base de données' : 'Ouvrir le document';
},
openChip(chip){
var href = this.chipHref(chip);
if(!href){ this.toast('Impossible d\u2019ouvrir cet élément.', true); return; }
window.location.assign(href);
},
onInput(){
this.resizeInput();
this.slashDismiss = false;
var text = this.composerText();
this.input = text;
var cmdNow = /^\s*\//.test(text);
if(cmdNow){
if(!this._cmdWasOpen){
var firstSkill = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
this.cmdFocus = firstSkill ? firstSkill._i : -1;
} else if(this.cmdFocus !== -1){
var present = (this.commandSuggestions || []).some(function(o){ return !o._sep && o._i === this.cmdFocus; }.bind(this));
if(!present){
var fs = (this.commandSuggestions || []).filter(function(o){ return !o._sep && !o._admin; })[0];
this.cmdFocus = fs ? fs._i : -1;
}
}
} else {
this.cmdFocus = -1;
}
this._cmdWasOpen = cmdNow;
// @ mention inline
var cq = this._caretQuery();
if(cq){
if(this._mentionFrom !== '@'){ this._mentionFrom = '@'; this._atNode = cq.node; }
this.mentionOpen = true;
if(this._mentionTimer) clearTimeout(this._mentionTimer);
this.mentionLoad = true;
this.searchMentions(cq.raw);
} else if(this._mentionFrom === '@'){
this.mentionOpen = false;
this.mentionQuery = '';
}
this._emptyCheck();
},
moveMentionFocus(delta){
var sel = this._selectableMentionItems();
if(!sel.length) return;
var n = sel.length;
var idx = sel.findIndex(function(it){ return it._i === this.mentionFocus; }.bind(this));
if(idx < 0) idx = delta > 0 ? -1 : 0;
idx = (idx + delta + n) % n;
this.mentionFocus = sel[idx]._i;
this._scrollMentionItem();
},
moveSlashFocus(delta){
var items = this._slashItems().filter(function(o){ return !o._admin; });
if(!items.length) return;
var idx = items.findIndex(function(o){ return o._i === this.cmdFocus; }.bind(this));
if(idx < 0) idx = delta > 0 ? -1 : 0;
idx = (idx + delta + items.length) % items.length;
this.cmdFocus = items[idx]._i;
var menu = this.$refs && this.$refs.cmdMenu;
if(menu){
var el = menu.querySelector('.fd-agent-cmd-item.focus');
if(el && el.offsetTop != null){ menu.scrollTop = el.offsetTop - menu.clientHeight / 2 + el.clientHeight / 2; }
}
},
onComposerKeydown(e){
var key = e.key;
if(key === 'Backspace' || key === 'Delete'){
try{
var sel = window.getSelection();
if(sel && sel.rangeCount){
var r = sel.getRangeAt(0);
if(r.collapsed){
var tok = key === 'Backspace'
? this._prevToken(r.startContainer, r.startOffset)
: this._nextToken(r.startContainer, r.startOffset);
if(tok){ e.preventDefault(); this._removeTokenDom(tok); return; }
}
}
}catch(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){
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){
var self = this;
function escHtml(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function inline(t){
t = escHtml(t);
return t
.replace(/`([^`]+)`/g,'<code>$1</code>')
.replace(/\*\*([^*]+)\*\*/g,'<strong>$1</strong>')
.replace(/\*([^*]+)\*/g,'<em>$1</em>')
.replace(/~~([^~]+)~~/g,'<s>$1</s>')
.replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g,'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
}
if(!md) return '';
var src = String(md);
// Trim leading/trailing blank lines to avoid stray <p></p>
var lines = src.split(/\r?\n/);
var html = '';
var i = 0;
function flush(block){
if(!block) return '';
if(block.type === 'ul' || block.type === 'ol'){
var tag = block.type === 'ul' ? 'ul' : 'ol';
return '<'+tag+' class="fd-md-list">' + block.items.map(function(it){
var li = inline(it.text);
if(it.checked != null){
return '<li><input type="checkbox" disabled'+(it.checked?' checked':'')+'> '+li+'</li>';
}
return '<li>'+li+'</li>';
}).join('') + '</'+tag+'>';
}
if(block.type === 'quote'){
return '<blockquote class="fd-md-quote">'+block.items.map(function(t){ return '<p>'+inline(t)+'</p>'; }).join('')+'</blockquote>';
}
if(block.type === 'p'){
return block.text ? '<p>'+inline(block.text)+'</p>' : '';
}
return '';
}
while(i < lines.length){
var raw = lines[i];
var t = raw.trim();
// Fenced code block
if(t.startsWith('```')){
var lang = t.substring(3).trim();
var code = [];
i++;
while(i < lines.length && !lines[i].trim().startsWith('```')){
code.push(lines[i]); i++;
}
i++; // skip closing ```
html += '<pre class="fd-md-code"><code>' + escHtml(code.join('\n')) + '</code></pre>';
continue;
}
// Table: | a | b | followed by | --- | --- |
if(t.startsWith('|') && i+1 < lines.length && /^\|[\s\-:|]+\|?$/.test(lines[i+1].trim())){
var headerCells = t.replace(/^\||\|$/g,'').split('|').map(function(c){return c.trim();});
var rows = [];
var j = i+2;
while(j < lines.length && lines[j].trim().startsWith('|')){
rows.push(lines[j].trim().replace(/^\||\|$/g,'').split('|').map(function(c){return c.trim();}));
j++;
}
var thead = '<tr>'+headerCells.map(function(c){return '<th>'+inline(c)+'</th>';}).join('')+'</tr>';
var tb = rows.map(function(r){
var cells = [];
for(var k=0;k<headerCells.length;k++) cells.push(r[k]||'');
return '<tr>'+cells.map(function(c){return '<td>'+inline(c)+'</td>';}).join('')+'</tr>';
}).join('');
html += '<div class="fd-md-table-wrap"><table class="fd-md-table"><thead>'+thead+'</thead><tbody>'+tb+'</tbody></table></div>';
i = j;
continue;
}
// Horizontal rule
if(/^(-{3,}|\*{3,}|_{3,})$/.test(t)){ html += '<hr class="fd-md-hr">'; i++; continue; }
// Blockquote (accumulate consecutive)
if(t.startsWith('> ')){
var q = {type:'quote', items:[]};
while(i < lines.length && lines[i].trim().startsWith('> ')){
q.items.push(lines[i].trim().substring(2)); i++;
}
html += flush(q); continue;
}
// Lists (accumulate consecutive ul/ol, incl. checkboxes)
if(/^[-*+] /.test(t) || /^\d+\. /.test(t)){
var ul = /^[-*+] /.test(t) ? 'ul' : 'ol';
var b = {type: ul, items: []};
while(i < lines.length){
var lt = lines[i].trim();
if(ul === 'ul' && /^[-*+] /.test(lt)){
var mt = lt.substring(2);
var chk = mt.match(/^\[([ xX])\]\s*/);
if(chk){ b.items.push({text: mt.substring(3), checked: chk[1]!==' '}); }
else b.items.push({text: mt});
i++;
} else if(ul === 'ol' && /^\d+\. /.test(lt)){
b.items.push({text: lt.replace(/^\d+\. /,'')}); i++;
} else break;
}
html += flush(b); continue;
}
// Headings
var hm = t.match(/^(#{1,4})\s+(.*)$/);
if(hm){ html += '<h'+hm[1].length+'>'+inline(hm[2])+'</h'+hm[1].length+'>'; i++; continue; }
// Paragraph (accumulate consecutive non-empty non-special)
if(t){
var para = [];
while(i < lines.length){
var pt = lines[i].trim();
if(!pt) break;
if(/^(```|#{1,4}\s|> |[-*+] |\d+\. |\||-{3,}$)/.test(pt)) break;
para.push(pt); i++;
}
html += para.map(function(p){ return '<p>'+inline(p)+'</p>'; }).join('');
continue;
}
i++;
}
return html;
},
copyResponse(m){
var self = this;
var text = (m && m.content) || '';
if(!text) return;
function fallback(){
try{
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
var ok = document.execCommand('copy');
document.body.removeChild(ta);
self.toast(ok ? 'Réponse copiée.' : 'Copie impossible.', !ok);
}catch(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); });
},
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(e){ 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:{'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(e){} }
}
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(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._livePh = null;
self.fetchConversationsRefresh();
});
},
fetchConversationsRefresh(){
var self = this;
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){
self.conversations = d.conversations || [];
// keep the open conversation in sync so its auto-updated title shows
// immediately in the header and in a later History screen.
var cur = self.currentConv;
if(cur && cur.id){
var match = self.conversations.find(function(c){ return c.id === cur.id; });
if(match){ cur.title = match.title; cur.updated_at = match.updated_at; }
}
}).catch(function(){});
},
scrollBottom(){
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
}
};
}
document.addEventListener('alpine:init', function(){
Alpine.data('agentPanel', agentPanel);
});
window.agentPanel = agentPanel;
// Plain-text helper for the history preview (strip markdown noise).
agentPanel._strip = function(t){
if(!t) return '';
return String(t)
.replace(/```[\s\S]*?```/g, ' [code] ')
.replace(/[#*_>`~]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 280);
};
// Short title for a reasoning thought (first non-empty line, truncated).
agentPanel._thoughtTitle = function(text){
var s = (text || '').trim();
if(!s) return 'Réflexion';
// Skip leading blank lines and get first meaningful line
var lines = s.split(/\r?\n/);
var first = '';
for(var i=0;i<lines.length;i++){
var ln = lines[i].trim();
if(ln){ first = ln; break; }
}
if(!first) return 'Réflexion';
first = first.replace(/^#+\s*/, ''); // strip markdown heading prefix
return first.length <= 72 ? first : first.slice(0,72).trimEnd() + '…';
};
// Past-tense label for each known agent action.
agentPanel._actionLabel = {
read_workspaces:'Espaces de travail listés', search_workspace:'Recherche effectuée',
read_document:'Document lu', read_collection:'Collection lue', read_page:'Page lue',
create_document:'Document créé', create_collection:'Collection créée',
create_page:'Page créée', create_view:'Vue créée', add_property:'Propriété ajoutée',
add_relation:'Relation ajoutée', update_page:'Page mise à jour',
write_blocks:'Contenu écrit', delete_document:'Document supprimé',
delete_page:'Page supprimée', delete_collection:'Collection supprimée',
apply_template:'Template appliqué', create_sub_item:'Sous-élément créé',
add_dependency:'Dépendance ajoutée', read_gitea_issues:'Issues Gitea lues',
create_gitea_issue:'Issue Gitea créée', sync_gitea:'Issues Gitea synchronisées'
};
// Build an action step object from an SSE event or a persisted action row.
agentPanel._actionStep = function(ev){
var ok = ev.status !== 'error';
var tool = String(ev.tool || '');
var label = agentPanel._actionLabel[tool] || tool.replace(/_/g,' ');
var extra = '';
if(ok && ev.target_id != null && ev.target_id !== '' && ev.target_id !== 0){
extra = ' → #' + ev.target_id;
}
if(ok && ev.message && ev.message.length <= 46){
extra += (extra ? ' — ' : ' · ') + ev.message;
}
return {
type:'action', tool:tool,
status: ok ? 'success' : 'error',
label: label + extra,
detail: ok ? '' : (ev.detail || ev.message || '')
};
};
// ═══════════ Context reader — document (page editor) ═══════════
window.fdReadPageContext = function(){
try{
var tEl = document.getElementById('_titleEl');
if(!tEl) return '';
var title = (tEl.textContent || '').trim();
var ct = document.getElementById('_blocksCt');
var text = '';
if(ct){
var kids = ct.children || [];
for(var i = 0; i < kids.length; i++){
var el = kids[i];
if(!el || !el.textContent) continue;
var t = el.textContent.replace(/\s+/g, ' ').trim();
if(t) text += t + '\n';
}
text = text.trim();
}
if(!title && !text) return '';
var out = title ? ('Titre du document : ' + title) : 'Document ouvert';
if(text) out += '\n\n' + text;
return out.slice(0, 9000);
}catch(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>