feat(agent): v4.13.0 - panneau Agent style Notion AI, ouverture fiable (Ctrl+J), contexte universel epingle a l'ouverture, selecteur provider/modele
This commit is contained in:
@@ -1,5 +1,40 @@
|
||||
# Changelog — FlowDeck
|
||||
|
||||
## v4.13.0 (2026-09-06) — Agent : panneau « Notion AI », ouverture fiable (Ctrl+J) & contexte universel
|
||||
|
||||
> Refonte complète du panneau Agent (style Notion AI) : ouverture/fermeture **fiable** (API
|
||||
> `window.fdAgent.open/close/toggle`, bouton flottant + sidebar « Agents » + raccourci `Ctrl/Cmd+J`),
|
||||
> **contexte détecté et épinglé à l'ouverture** (document en cours, bibliothèque, My Tasks,
|
||||
> espaces, base de données, board, corbeille…) et sélecteur **fournisseur + modèle** intégré à la
|
||||
> zone de saisie.
|
||||
|
||||
### Added
|
||||
- **Ouverture robuste** : `window.fdAgent.open()/close()/toggle()` appliquent l'état directement
|
||||
(classe `.open` + attribut) ; FAB, entrée « Agents » du sidebar, icône Chat et `fdAgent.ask`
|
||||
ouvrent explicitement le panneau (fini le toggle silencieux). Raccourci **`Ctrl/Cmd+J`** global.
|
||||
- **Contexte universel `window.FlowDeckCtx`** : détection par priorité —
|
||||
(1) marqueur d'écran (`window.fdViewContext` / `data-fd-context`), (2) **document ouvert**
|
||||
(éditeur : titre + contenu des blocs), (3) **route** de l'application (Accueil, Bibliothèque,
|
||||
Corbeille, My Tasks, Espaces, Espace local/Gitea, Base `/db`, Board, Notes, Paramètres…).
|
||||
Le contexte est **épinglé à l'ouverture** du panneau, affiché en chip (« Contexte : … ») et
|
||||
**rafraîchi** automatiquement si l'utilisateur ouvre un autre document avant d'envoyer.
|
||||
- **Fournisseur & modèle affichés** : sous-titre du panneau `Provider · Modèle` (ex. `OpenAI ·
|
||||
gpt-4o`) dès qu'un modèle réel fonctionnel est sélectionné ; « hors-ligne (mock) » n'apparaît
|
||||
que lorsqu'aucun fournisseur actif n'existe. Sélecteurs provider → modèle regroupés dans une
|
||||
pastille « Auto · <model> » au-dessus de la zone de saisie.
|
||||
- **Actions rapides contextuelles** (état vide) en français selon la section : Résumer / Traduire /
|
||||
Améliorer un document, Créer une note de réunion, Analyser une base, Mes tâches / planifier,
|
||||
ou guides « Comment utiliser FlowDeck ? » et création de collection/board.
|
||||
|
||||
### Changed
|
||||
- **Panneau redessiné façon Notion AI** : largeur 470 px, en-tête « How can I help you today? »,
|
||||
icône avatar, ligne de contexte, historique, grande `textarea` arrondie auto-redimensionnable
|
||||
(jusqu'à ~220 px) avec toolbar (`+`-équivalent, contexte, modèle, flèche d'envoi) et sélection
|
||||
de modèle en popover.
|
||||
|
||||
### Infra
|
||||
- `VERSION` → 4.13.0 ; `app/main.py` version → 4.13.0.
|
||||
|
||||
## v4.12.1 (2026-09-05) — Corrections configuration Agent & IA + AI Meeting Note
|
||||
|
||||
> Correctifs UI sur la section **Settings → Agent & IA** et remplacement de l'ancien bouton
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="4.12.1",
|
||||
version="4.13.0",
|
||||
docs_url="/docs" if settings.log_level == "DEBUG" else None,
|
||||
redoc_url=None,
|
||||
lifespan=lifespan,
|
||||
|
||||
+459
-174
@@ -1,121 +1,196 @@
|
||||
{# ════════ FlowDeck Agent panel (v4.10.0) — sidebar-triggered chat + floating button ════════ #}
|
||||
{# ════════════ FlowDeck Agent panel v4.13.0 — Notion-AI-style sidebar ════════════ #}
|
||||
<style>
|
||||
#fd-agent-fab{position:fixed;right:24px;bottom:24px;width:52px;height:52px;border-radius:50%;background:var(--accent,#2383E2);color:#fff;border:none;font-size:22px;cursor:pointer;box-shadow:0 8px 24px rgba(35,131,226,.5);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
|
||||
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 32px rgba(35,131,226,.6)}
|
||||
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:400px;max-width:100vw;background:var(--bg-secondary,#1f1f1f);border-left:1px solid var(--border,rgba(255,255,255,.09));z-index:1800;display:flex;flex-direction:column;box-shadow:-12px 0 40px rgba(0,0,0,.5);transform:translateX(100%);transition:transform .25s ease}
|
||||
#fd-agent-fab{position:fixed;right:22px;bottom:22px;width:54px;height:54px;border-radius:50%;background:var(--accent,#2383E2);color:#fff;border:none;font-size:24px;cursor:pointer;box-shadow:0 8px 26px rgba(35,131,226,.5);display:flex;align-items:center;justify-content:center;z-index:1700;transition:transform .15s,box-shadow .15s}
|
||||
#fd-agent-fab:hover{transform:scale(1.06);box-shadow:0 12px 34px rgba(35,131,226,.62)}
|
||||
#fd-agent-panel{position:fixed;top:0;right:0;bottom:0;width:470px;max-width:100vw;background:var(--bg-primary,#191919);border-left:1px solid var(--border,rgba(255,255,255,.1));z-index:1850;display:flex;flex-direction:column;box-shadow:-16px 0 48px rgba(0,0,0,.55);transform:translateX(102%);transition:transform .24s cubic-bezier(.16,1,.3,1)}
|
||||
#fd-agent-panel.open{transform:translateX(0)}
|
||||
.fd-agent-head{padding:14px 16px;border-bottom:1px solid var(--border,rgba(255,255,255,.08));display:flex;align-items:center;gap:10px}
|
||||
.fd-agent-title{font-weight:600;font-size:15px;color:var(--text,#e0e0e0);flex:1;display:flex;align-items:center;gap:8px}
|
||||
.fd-agent-provider{font-size:11px;color:var(--text-dim,#999);background:rgba(255,255,255,.06);padding:2px 8px;border-radius:99px}
|
||||
.fd-agent-close{background:none;border:none;color:var(--text-dim,#999);font-size:20px;cursor:pointer;padding:2px 6px;border-radius:6px}
|
||||
.fd-agent-close:hover{background:var(--bg-hover,#333);color:var(--text,#fff)}
|
||||
.fd-agent-tabs{display:flex;gap:4px;padding:6px 10px;border-bottom:1px solid var(--border,rgba(255,255,255,.06))}
|
||||
.fd-agent-tab{flex:1;text-align:center;padding:7px;background:none;border:none;border-radius:7px;color:var(--text-dim,#999);font-size:12px;cursor:pointer}
|
||||
.fd-agent-tab.active{background:var(--bg-tertiary,#2a2a2a);color:var(--text,#fff);font-weight:600}
|
||||
.fd-agent-body{flex:1;overflow-y:auto;padding:14px}
|
||||
.fd-agent-hist-item{padding:8px 10px;border-radius:8px;cursor:pointer;color:var(--text,#e0e0e0);font-size:13px;border:1px solid transparent}
|
||||
.fd-agent-hist-item:hover,.fd-agent-hist-item.active{background:var(--bg-hover,#2c2c2c);border-color:var(--border,rgba(255,255,255,.08))}
|
||||
.fd-agent-msg{display:flex;gap:8px;margin-bottom:12px}
|
||||
.fd-agent-msg-avatar{width:26px;height:26px;border-radius:50%;background:var(--bg-tertiary,#2c2c2c);display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
|
||||
#fd-agent-panel ::-webkit-scrollbar{width:8px;height:8px}
|
||||
#fd-agent-panel ::-webkit-scrollbar-thumb{background:rgba(128,128,128,.35);border-radius:4px}
|
||||
#fd-agent-panel ::-webkit-scrollbar-track{background:transparent}
|
||||
|
||||
/* Header */
|
||||
.fd-ap-head{display:flex;align-items:center;gap:12px;padding:16px 18px 12px}
|
||||
.fd-ap-logo{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--accent,#2383E2),#5b8def);color:#fff;display:flex;align-items:center;justify-content:center;font-size:19px;flex-shrink:0;box-shadow:0 4px 12px rgba(35,131,226,.35)}
|
||||
.fd-ap-head-main{flex:1;min-width:0}
|
||||
.fd-ap-title{font-size:15px;font-weight:600;color:var(--text,#f0f0f0);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fd-ap-sub{font-size:11.5px;color:var(--text-dim,#9aa);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fd-ap-headbtn{background:none;border:none;color:var(--text-dim,#999);width:30px;height:30px;border-radius:8px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
||||
.fd-ap-headbtn:hover{background:var(--bg-hover,#2b2b2b);color:var(--text,#fff)}
|
||||
|
||||
/* Context bar */
|
||||
.fd-ap-ctx{display:flex;align-items:center;gap:8px;margin:0 16px 8px;padding:7px 11px;border-radius:9px;background:rgba(35,131,226,.09);border:1px solid rgba(35,131,226,.16);font-size:12px;color:var(--text-secondary,#ddd);min-height:18px}
|
||||
.fd-ap-ctx .fd-ctx-ico{font-size:13px}
|
||||
.fd-ap-ctx .fd-ctx-lbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
|
||||
/* No-config banner */
|
||||
.fd-ap-noconfig{padding:8px 14px;font-size:12px;color:#e6b4b4;background:rgba(227,98,98,.1);border:1px solid rgba(227,98,98,.18);margin:0 16px 10px;border-radius:9px;display:flex;gap:6px;flex-wrap:wrap;align-items:center}
|
||||
.fd-ap-noconfig a{color:#ff9d9d;font-weight:600;text-decoration:none}
|
||||
.fd-ap-noconfig .fd-k-sub{width:100%;color:var(--text-dim,#aaa)}
|
||||
|
||||
/* Tabs */
|
||||
.fd-ap-tabs{display:flex;gap:2px;padding:0 16px 8px;border-bottom:1px solid var(--border,rgba(255,255,255,.08))}
|
||||
.fd-ap-tab{flex:1;text-align:center;padding:7px;background:none;border:none;border-radius:8px;color:var(--text-dim,#999);font-size:12.5px;cursor:pointer}
|
||||
.fd-ap-tab.active{background:var(--bg-tertiary,#242424);color:var(--text,#fff);font-weight:600}
|
||||
|
||||
/* Body */
|
||||
.fd-agent-body{flex:1;overflow-y:auto;padding:12px 16px}
|
||||
.fd-ap-hist-item{padding:8px 11px;border-radius:9px;cursor:pointer;color:var(--text,#e0e0e0);font-size:13px;border:1px solid transparent;margin-bottom:2px}
|
||||
.fd-ap-hist-item:hover,.fd-ap-hist-item.active{background:var(--bg-hover,#262626);border-color:var(--border,rgba(255,255,255,.08))}
|
||||
|
||||
/* Empty / welcome state */
|
||||
.fd-ap-empty{padding:6px 2px 4px}
|
||||
.fd-ap-empty-title{font-size:14px;font-weight:600;color:var(--text,#eee);margin-bottom:2px}
|
||||
.fd-ap-empty-desc{font-size:12px;color:var(--text-dim,#999);margin-bottom:12px}
|
||||
.fd-ap-qa{display:flex;flex-direction:column;gap:6px}
|
||||
.fd-ap-qa-item{display:flex;align-items:center;gap:10px;text-align:left;background:var(--bg-tertiary,#202020);border:1px solid var(--border,rgba(255,255,255,.07));border-radius:10px;padding:9px 12px;color:var(--text,#e6e6e6);font-size:13px;cursor:pointer;transition:background .12s,border-color .12s}
|
||||
.fd-ap-qa-item:hover{background:var(--bg-hover,#2a2a2a);border-color:rgba(35,131,226,.4)}
|
||||
.fd-ap-qa-item .fd-qa-ico{font-size:16px;flex-shrink:0}
|
||||
.fd-ap-qa-item .fd-qa-new{margin-left:auto;font-size:9px;font-weight:700;color:#fff;background:var(--accent);border-radius:99px;padding:2px 8px;letter-spacing:.3px}
|
||||
|
||||
/* Messages */
|
||||
.fd-agent-msg{display:flex;gap:9px;margin-bottom:14px}
|
||||
.fd-agent-msg-avatar{width:28px;height:28px;border-radius:50%;background:var(--bg-tertiary,#2b2b2b);display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
|
||||
.fd-agent-msg-assistant .fd-agent-msg-avatar{background:rgba(35,131,226,.18)}
|
||||
.fd-agent-msg-body{flex:1;min-width:0}
|
||||
.fd-agent-msg-meta{font-size:10px;color:var(--text-dim,#888);margin-bottom:2px;text-transform:uppercase;letter-spacing:.5px}
|
||||
.fd-agent-msg-content{font-size:13px;color:var(--text,#e6e6e6);white-space:pre-wrap;word-break:break-word}
|
||||
.fd-agent-msg-tools{list-style:none;padding:6px 8px;margin:6px 0 0;background:rgba(35,131,226,.08);border-left:3px solid var(--accent,#2383E2);border-radius:4px;font-size:12px;color:var(--text-dim,#bbb)}
|
||||
.fd-agent-thinking{font-size:12px;color:var(--text-dim,#999);padding:10px;border-radius:8px;background:rgba(255,255,255,.04);margin-bottom:10px;font-style:italic}
|
||||
.fd-agent-input-area{padding:12px;border-top:1px solid var(--border,rgba(255,255,255,.08));display:flex;gap:8px;align-items:flex-end}
|
||||
.fd-agent-input{flex:1;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:10px;padding:10px 12px;color:var(--text,#fff);font-size:13px;resize:none;outline:none;font-family:inherit}
|
||||
.fd-agent-input:focus{border-color:var(--accent,#2383E2)}
|
||||
.fd-agent-send{background:var(--accent,#2383E2);border:none;color:#fff;border-radius:8px;padding:10px 14px;cursor:pointer;font-size:14px}
|
||||
.fd-agent-send:disabled{opacity:.5;cursor:not-allowed}
|
||||
.fd-agent-new{background:none;border:1px solid var(--border,rgba(255,255,255,.1));color:var(--text,#ddd);border-radius:8px;padding:4px 8px;font-size:12px;cursor:pointer}
|
||||
.fd-agent-config{display:flex;align-items:center;gap:6px;padding:6px 10px;border-bottom:1px solid var(--border,rgba(255,255,255,.06))}
|
||||
.fd-agent-select{flex:1;min-width:0;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:7px;color:var(--text,#fff);font-size:12px;padding:5px 8px;outline:none;font-family:inherit}
|
||||
.fd-agent-select:focus{border-color:var(--accent,#2383E2)}
|
||||
.fd-agent-cmd-menu{position:absolute;bottom:100%;left:0;right:0;margin-bottom:6px;background:var(--bg-tertiary,#262626);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:10px;max-height:180px;overflow-y:auto;box-shadow:0 -8px 24px rgba(0,0,0,.4);z-index:20;display:none}
|
||||
.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-thinking{font-size:12.5px;color:var(--text-dim,#999);padding:10px 12px;border-radius:9px;background:rgba(255,255,255,.04);margin:4px 0 8px;font-style:italic}
|
||||
.fd-ap-err{color:#ef8a8a;font-size:12px;padding:4px 2px}
|
||||
|
||||
/* Composer */
|
||||
.fd-ap-composer{padding:10px 14px 14px;border-top:1px solid var(--border,rgba(255,255,255,.09));position:relative}
|
||||
.fd-ap-box{background:var(--bg-tertiary,#1f1f1f);border:1px solid var(--border,rgba(255,255,255,.13));border-radius:16px;padding:10px 12px 8px;transition:border-color .12s,box-shadow .12s}
|
||||
.fd-ap-box:focus-within{border-color:rgba(35,131,226,.65);box-shadow:0 0 0 3px rgba(35,131,226,.12)}
|
||||
.fd-ap-input{width:100%;background:transparent;border:none;outline:none;color:var(--text,#fff);font-size:14px;resize:none;font-family:inherit;line-height:1.5;max-height:220px;box-sizing:border-box}
|
||||
.fd-ap-input::placeholder{color:var(--text-dim,#8a8a8a)}
|
||||
.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:7px 12px;font-size:12px;color:var(--text,#eee);cursor:pointer;border-radius:6px;margin:2px;display:flex;gap:8px;align-items:baseline}
|
||||
.fd-agent-cmd-item:hover{background:var(--bg-hover,#333)}
|
||||
.fd-agent-cmd-item{padding:8px 12px;font-size:12px;color:var(--text,#eee);cursor:pointer;border-radius:7px;margin:3px;display:flex;gap:8px;align-items:baseline}
|
||||
.fd-agent-cmd-item:hover{background:var(--bg-hover,#2b2b2b)}
|
||||
.fd-agent-cmd-item b{color:var(--accent,#2383E2);font-family:monospace;white-space:nowrap}
|
||||
.fd-agent-cmd-item span{color:var(--text-dim,#999);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.fd-agent-gear{background:none;border:none;color:var(--text-dim,#999);font-size:14px;cursor:pointer;padding:2px 4px;border-radius:6px;text-decoration:none;line-height:1}
|
||||
.fd-agent-gear:hover{background:var(--bg-hover,#333);color:var(--text,#fff)}
|
||||
.fd-agent-noconfig{padding:8px 10px;font-size:12px;color:var(--text-dim,#ccc);background:rgba(227,98,98,.10);border-bottom:1px solid var(--border,rgba(255,255,255,.06));display:flex;gap:6px;align-items:center;flex-wrap:wrap}
|
||||
.fd-agent-noconfig a{color:var(--accent,#2383E2);text-decoration:none;font-weight:600}
|
||||
.fd-agent-ctx{font-size:11px;color:var(--text-dim,#999);padding:5px 12px;border-bottom:1px solid var(--border,rgba(255,255,255,.06));display:flex;gap:6px;align-items:center;background:rgba(35,131,226,.08)}
|
||||
|
||||
[x-cloak]{display:none!important}
|
||||
</style>
|
||||
|
||||
<button id="fd-agent-fab" title="FlowDeck Agent" onclick="document.dispatchEvent(new CustomEvent('fd-agent-toggle'))">🤖</button>
|
||||
{# ── Global Agent API (available before Alpine for the FAB / shortcut) ── #}
|
||||
<script>
|
||||
(function(){
|
||||
var defaultAPI = {
|
||||
open: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle')); },
|
||||
close: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle', { detail: { close: true } })); },
|
||||
toggle: function(){ document.dispatchEvent(new CustomEvent('fd-agent-toggle')); },
|
||||
ask: function(){}, generate: function(){ return Promise.resolve(''); }
|
||||
};
|
||||
window.fdAgent = window.fdAgent || defaultAPI;
|
||||
// Ctrl/Cmd+J — open / close the Agent panel from anywhere.
|
||||
document.addEventListener('keydown', function(e){
|
||||
if((e.ctrlKey || e.metaKey) && (e.key === 'j' || e.key === 'J')){
|
||||
e.preventDefault();
|
||||
if(window.fdAgent && window.fdAgent.toggle) window.fdAgent.toggle();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<div id="fd-agent-panel" x-data="agentPanel()" :class="{open: open}" @keydown.escape.window="close()">
|
||||
<div class="fd-agent-head">
|
||||
<div class="fd-agent-title">🤖 Agent <span class="fd-agent-provider" x-text="providerLabel"></span></div>
|
||||
<button class="fd-agent-new" @click="newConversation()">+ New</button>
|
||||
<button class="fd-agent-close" @click="close()">×</button>
|
||||
{# ── Floating button ── #}
|
||||
<button id="fd-agent-fab" title="FlowDeck Agent (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.toggle()">🤖</button>
|
||||
|
||||
{# ── Panel ── #}
|
||||
<div id="fd-agent-panel" class="fd-ap" :class="{open: open}" x-data="agentPanel()" @keydown.escape.window="close()" role="dialog" aria-label="Agent FlowDeck">
|
||||
<div class="fd-ap-head">
|
||||
<span class="fd-ap-logo">🤖</span>
|
||||
<div class="fd-ap-head-main">
|
||||
<div class="fd-ap-title">How can I help you today?</div>
|
||||
<div class="fd-ap-sub" x-text="providerModelLabel"></div>
|
||||
</div>
|
||||
<button class="fd-ap-headbtn" title="Nouvelle conversation" @click="newConversation()">+</button>
|
||||
<button class="fd-ap-headbtn" title="Fermer (Ctrl+J)" @click="close()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="fd-agent-config">
|
||||
<select class="fd-agent-select" x-model="llmProvider" @change="onProviderChange()" title="Provider">
|
||||
<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>
|
||||
<select class="fd-agent-select" x-model="llmModel" x-show="llmProvider!=='offline'" title="Modèle">
|
||||
<template x-for="m in providerModels" :key="m">
|
||||
<option :value="m" x-text="m"></option>
|
||||
</template>
|
||||
</select>
|
||||
<a href="/settings" target="_blank" class="fd-agent-gear" title="Configurer (clés API, Settings → Agent & IA)" @click.stop>⚙️</a>
|
||||
<div class="fd-ap-ctx" x-show="activeContext" x-cloak>
|
||||
<span class="fd-ctx-ico" x-text="activeContext ? activeContext.icon : ''"></span>
|
||||
<span class="fd-ctx-lbl" x-text="activeContext ? ('Contexte : ' + activeContext.label) : ''"></span>
|
||||
</div>
|
||||
|
||||
<div class="fd-agent-noconfig" x-show="!hasActiveProvider">
|
||||
<div class="fd-ap-noconfig" x-show="!hasActiveProvider">
|
||||
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
|
||||
<a href="/settings" target="_blank">Configurer → Agent & IA</a>
|
||||
<a href="/settings" target="_blank" @click.stop>Configurer → Agent & IA</a>
|
||||
<template x-if="unverifiedCount">
|
||||
<span style="width:100%;color:var(--text-dim,#bbb)">🔑 <span x-text="unverifiedCount"></span> clé(s) enregistrée(s) à tester : ouvrez Paramètres puis « Tester ».</span>
|
||||
<span class="fd-k-sub">🔑 <span x-text="unverifiedCount"></span> clé(s) à tester : Paramètres puis « Tester ».</span>
|
||||
</template>
|
||||
</div>
|
||||
<div class="fd-agent-ctx" x-show="chatContextActive" x-cloak>
|
||||
<span>📄 Document ouvert : l'agent peut interagir avec cette page.</span>
|
||||
</div>
|
||||
|
||||
<div class="fd-agent-tabs">
|
||||
<button class="fd-agent-tab" :class="{active: tab=='chat'}" @click="tab='chat'">Conversation</button>
|
||||
<button class="fd-agent-tab" :class="{active: tab=='history'}" @click="tab='history'; loadConversations()">Historique</button>
|
||||
<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'">
|
||||
<template x-if="tab==='history'">
|
||||
<div>
|
||||
<template x-for="c in conversations" :key="c.id">
|
||||
<div class="fd-agent-hist-item" :class="{active: currentConv && currentConv.id===c.id}" @click="loadConversation(c.id)">
|
||||
<div class="fd-ap-hist-item" :class="{active: currentConv && currentConv.id===c.id}" @click="loadConversation(c.id)">
|
||||
<div x-text="c.title || 'New conversation'"></div>
|
||||
<div style="font-size:11px;color:var(--text-dim)" x-text="c.updated_at"></div>
|
||||
</div>
|
||||
</template>
|
||||
<div x-show="!conversations.length" style="font-size:13px;color:var(--text-dim);padding:8px">Aucune conversation. Démarrez-en une !</div>
|
||||
<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'">
|
||||
<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">
|
||||
<div class="fd-agent-msg" :class="'fd-agent-msg-'+(m.role||'user')">
|
||||
<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||'user')+(m.model?' · '+m.model:'')"></div>
|
||||
<div class="fd-agent-msg-meta" x-text="(m.role==='assistant'?'Agent':'Vous')+(m.model?' · '+m.model:'')"></div>
|
||||
<div class="fd-agent-msg-content" x-text="m.content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="fd-agent-thinking" x-show="streaming">L'agent réfléchit et agit… <span x-text="liveReasoning"></span></div>
|
||||
<div x-show="errorMsg" style="color:#e06b6b;font-size:12px;padding:6px" x-text="errorMsg"></div>
|
||||
<div class="fd-ap-err" x-show="errorMsg" x-text="errorMsg"></div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="fd-agent-input-area">
|
||||
<div style="position:relative;flex:1;">
|
||||
<div class="fd-ap-composer">
|
||||
<div style="position:relative;">
|
||||
<div class="fd-agent-cmd-menu" :class="{show: commandOpen && commandSuggestions.length}">
|
||||
<template x-for="c in commandSuggestions" :key="c.cmd">
|
||||
<div class="fd-agent-cmd-item" @click="useSuggestion(c)">
|
||||
@@ -123,39 +198,79 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<textarea class="fd-agent-input" x-ref="cmdInput" rows="2" placeholder="Demandez à l'agent (ex: crée une collection Projets)… tapez / pour les commandes" x-model="input" @keydown.enter.exact.prevent="send()"></textarea>
|
||||
</div>
|
||||
<button class="fd-agent-send" @click="send()" :disabled="streaming || !input.trim()">▶</button>
|
||||
<div class="fd-ap-box">
|
||||
<textarea class="fd-ap-input" x-ref="cmdInput" rows="1"
|
||||
placeholder="Demandez à l'agent (ex : crée une collection Projets)… Tapez / pour les commandes"
|
||||
x-model="input" @input="resizeInput()" @keydown.enter.exact.prevent="send()"
|
||||
@keydown.enter.shift.prevent="insertNewline()"></textarea>
|
||||
<div class="fd-ap-toolbar">
|
||||
<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 || !input.trim()" title="Envoyer">▲</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fd-ap-pop" x-show="modelOpen" @click.outside="modelOpen=false" x-cloak>
|
||||
<label>Fournisseur</label>
|
||||
<select x-model="llmProvider" @change="onProviderChange()">
|
||||
<template x-for="p in providers" :key="p.id">
|
||||
<option :value="p.id" x-text="p.id==='offline' ? 'Hors-ligne (mock)' : p.name"></option>
|
||||
</template>
|
||||
</select>
|
||||
<label>Modèle</label>
|
||||
<select x-model="llmModel" @change="patchConversation()" x-show="llmProvider!=='offline'">
|
||||
<template x-for="m in providerModels" :key="m">
|
||||
<option :value="m" x-text="m"></option>
|
||||
</template>
|
||||
</select>
|
||||
<a href="/settings" target="_blank" @click="modelOpen=false">⚙️ Configurer (clés API) →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
function fmt(ev){ return ev; }
|
||||
function agentPanel(){
|
||||
return {
|
||||
open: false, tab: 'chat',
|
||||
open: false, modelOpen: false, tab: 'chat',
|
||||
agents: [], conversations: [], currentConv: null,
|
||||
messages: [], input: '', streaming: false, liveReasoning: '',
|
||||
provider: 'offline', providerAvailable: false, errorMsg: '',
|
||||
allProviders: [], providers: [], providerModels: [], llmProvider: 'offline',
|
||||
llmModel: '', llmKeys: [], defaultProvider: 'offline', ctxDetected: false,
|
||||
llmModel: '', llmKeys: [], defaultProvider: 'offline',
|
||||
activeContext: null, ctxPinnedKey: null,
|
||||
|
||||
init(){
|
||||
var self = this;
|
||||
// The panel lives outside the root appState; toggle via the FAB + Agents sidebar.
|
||||
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){
|
||||
self.open = !self.open;
|
||||
if(e && e.detail && e.detail.agentId){ self.agentId = e.detail.agentId; }
|
||||
self.detectCtx();
|
||||
self.loadConversations();
|
||||
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();
|
||||
|
||||
// Programmatic entry point used by the page editor ("Ask AI",
|
||||
// "AI meeting note") and any global UI: open the panel and run a prompt.
|
||||
window.fdAgent = window.fdAgent || {};
|
||||
// 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;
|
||||
@@ -163,8 +278,6 @@
|
||||
self.input = t;
|
||||
setTimeout(function(){ self.send(); }, 60);
|
||||
};
|
||||
// Headless generation (no panel): used by the AI Meeting Note block to
|
||||
// build its Summary. Returns a Promise of the final assistant text.
|
||||
window.fdAgent.generate = function(message, context){
|
||||
message = (message || '').trim();
|
||||
if(!message) return Promise.resolve('');
|
||||
@@ -172,26 +285,20 @@
|
||||
if(context && context.trim()) payload.context = String(context).slice(0, 20000);
|
||||
return fetch('/api/agent/conversations', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({title:'AI Meeting Note'})
|
||||
body: JSON.stringify({title:'Agent'})
|
||||
}).then(function(r){ return r.json(); }).then(function(conv){
|
||||
var cid = conv && conv.id;
|
||||
if(!cid) throw new Error('Impossible de créer une conversation agent');
|
||||
return fetch('/api/agent/conversations/' + cid + '/run', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
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)); });
|
||||
}
|
||||
if(!resp.ok){ return resp.json().then(function(j){ throw new Error(j.detail || ('HTTP ' + resp.status)); }); }
|
||||
var reader = resp.body.getReader();
|
||||
var dec = new TextDecoder();
|
||||
var buf = '', finalText = '', lastError = '';
|
||||
function pump(){
|
||||
return reader.read().then(function(res){
|
||||
if(res.done){
|
||||
if(lastError) throw new Error(lastError);
|
||||
return finalText;
|
||||
}
|
||||
if(res.done){ if(lastError) throw new Error(lastError); return finalText; }
|
||||
buf += dec.decode(res.value, {stream:true});
|
||||
var parts = buf.split('\n\n');
|
||||
buf = parts.pop();
|
||||
@@ -213,8 +320,93 @@
|
||||
};
|
||||
},
|
||||
|
||||
// Only providers *configured and functional* (verified) are selectable,
|
||||
// plus the built-in "offline" mock so the chat always stays usable.
|
||||
// ── Open / close (robust) ──
|
||||
openAgent(){
|
||||
if(this.open) { this.refreshContext(); return; }
|
||||
this.open = true;
|
||||
this.refreshContext(true);
|
||||
if(this._applyState) this._applyState();
|
||||
this.loadConversations();
|
||||
var self = this;
|
||||
this.$nextTick(function(){
|
||||
var t = self.$refs && self.$refs.cmdInput;
|
||||
if(t){ t.focus(); self.resizeInput(); }
|
||||
});
|
||||
},
|
||||
close(){
|
||||
if(!this.open) return;
|
||||
this.open = false;
|
||||
this.modelOpen = false;
|
||||
if(this._applyState) this._applyState();
|
||||
},
|
||||
toggle(){ this.open ? this.close() : this.openAgent(); },
|
||||
|
||||
// ── Context (pinned at open, refreshed when a new document is open) ──
|
||||
refreshContext(force){
|
||||
var ctx = FlowDeckCtx.current();
|
||||
if(!ctx){ this.activeContext = null; return; }
|
||||
if(!this.activeContext) { this.activeContext = ctx; 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;
|
||||
}
|
||||
},
|
||||
get chatContextActive(){ return !!(this.activeContext && this.activeContext.kind === 'page'); },
|
||||
contextPayload(){
|
||||
this.refreshContext();
|
||||
var c = this.activeContext;
|
||||
if(!c) return '';
|
||||
if(c.text && c.text.trim()) return c.text.trim().slice(0, 9000);
|
||||
return c.label ? ('Contexte : ' + c.label + '.').slice(0, 2000) : '';
|
||||
},
|
||||
get composerHint(){
|
||||
var c = this.activeContext;
|
||||
if(!c) return 'Demandez ou tapez / pour les commandes';
|
||||
if(c.kind === 'page') return 'Contexte : le document est pris en compte par l\u2019agent.';
|
||||
return 'Contexte : ' + c.label;
|
||||
},
|
||||
|
||||
// ── 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.'});
|
||||
out.push({icon:'🌐', label:'Traduire cette page', prompt:'Traduis le contenu de ce document en anglais.', new:false});
|
||||
out.push({icon:'✍️', label:'Proposer des améliorations', prompt:'Analyse ce document et propose des améliorations concrètes de structure et de contenu.'});
|
||||
out.push({icon:'📝', label:'Créer une note de réunion', prompt:'Crée un squelette d\u2019AI meeting note structuré à partir de ce document.'});
|
||||
} 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;
|
||||
var self = this;
|
||||
this.ensureConversation().then(function(){
|
||||
self.input = q.prompt;
|
||||
self.send();
|
||||
}).catch(function(){});
|
||||
},
|
||||
|
||||
// ── Providers / models ──
|
||||
fetchProviders(){
|
||||
var self = this;
|
||||
fetch('/api/agent/providers').then(function(r){return r.json()}).then(function(d){
|
||||
@@ -225,13 +417,15 @@
|
||||
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});
|
||||
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.llmProvider) self.llmProvider = want;
|
||||
if(!self.llmModel) self.llmModel = d.model || '';
|
||||
self.applyModels();
|
||||
}).catch(function(){});
|
||||
@@ -249,38 +443,34 @@
|
||||
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 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;
|
||||
return (this.allProviders || []).filter(function(p){ return p.configured && !p.verified && p.id !== 'offline'; }).length;
|
||||
},
|
||||
get chatContextActive(){ return this.ctxDetected; },
|
||||
detectCtx(){
|
||||
var has = false;
|
||||
try{ has = !!(window.fdGetPageContext && String(window.fdGetPageContext()).trim()); }catch(e){ has = false; }
|
||||
this.ctxDetected = has;
|
||||
providerName(id){
|
||||
var p = (this.allProviders || []).find(function(x){ return x.id === id; });
|
||||
return (p && p.name) || id;
|
||||
},
|
||||
docContext(){
|
||||
try{
|
||||
if(window.fdGetPageContext && typeof window.fdGetPageContext === 'function'){
|
||||
var c = window.fdGetPageContext();
|
||||
if(c && c.trim()) return String(c).slice(0, 9000);
|
||||
}
|
||||
}catch(e){}
|
||||
try{
|
||||
if(window.fdPageContext && String(window.fdPageContext).trim()) return String(window.fdPageContext).slice(0, 9000);
|
||||
}catch(e){}
|
||||
return '';
|
||||
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(){});
|
||||
fetch('/api/agent').then(function(r){return r.json()}).then(function(d){ self.agents = d.agents || []; }).catch(function(){});
|
||||
},
|
||||
loadConversations(){
|
||||
var self = this;
|
||||
@@ -300,17 +490,14 @@
|
||||
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;
|
||||
// Reuse the conversation's provider only while it stays functional.
|
||||
if(prov && (!self.providers.length || self.providers.some(function(x){ return x.id === prov; }))){
|
||||
self.llmProvider = prov;
|
||||
}
|
||||
if(prov && (!self.providers.length || self.providers.some(function(x){ return x.id === prov; }))){ self.llmProvider = prov; }
|
||||
self.applyModels();
|
||||
if(d.conversation && d.conversation.model && self.providerModels.indexOf(d.conversation.model) !== -1){ self.llmModel = d.conversation.model; }
|
||||
}).catch(function(){});
|
||||
},
|
||||
newConversation(){
|
||||
var self = this;
|
||||
var body = {title:'New conversation'};
|
||||
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)})
|
||||
@@ -318,15 +505,14 @@
|
||||
var conv = {id:d.id, title:d.title, updated_at:new Date().toISOString()};
|
||||
self.conversations.unshift(conv); self.currentConv = conv;
|
||||
self.messages = []; self.errorMsg=''; self.tab='chat';
|
||||
self.$nextTick(function(){ self.resizeInput(); });
|
||||
}).catch(function(){});
|
||||
},
|
||||
ensureConversation(){
|
||||
// Make the chat usable immediately: reuse the open conversation or
|
||||
// create one lazily so sending never silently no-ops (empty state).
|
||||
var self = this;
|
||||
if(self.currentConv && self.currentConv.id) return Promise.resolve(self.currentConv);
|
||||
if(self._ensuring) return self._ensuring;
|
||||
var body = {title:'New conversation'};
|
||||
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', {
|
||||
@@ -344,8 +530,32 @@
|
||||
});
|
||||
return self._ensuring;
|
||||
},
|
||||
openAgent(){ this.open=true; this.detectCtx(); this.loadConversations(); },
|
||||
close(){ this.open=false; },
|
||||
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(){});
|
||||
},
|
||||
|
||||
// ── Input helpers ──
|
||||
resizeInput(){
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
if(!t) return;
|
||||
t.style.height = 'auto';
|
||||
t.style.height = Math.min(t.scrollHeight, 220) + 'px';
|
||||
},
|
||||
insertNewline(){
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
if(!t) return;
|
||||
var s = t.selectionStart, e = t.selectionEnd;
|
||||
var v = this.input || '';
|
||||
this.input = v.slice(0, s) + '\n' + v.slice(e);
|
||||
this.$nextTick(function(){ t.selectionStart = t.selectionEnd = s + 1; });
|
||||
},
|
||||
get commandOpen(){ return /^\s*\//.test(this.input) && !this.streaming; },
|
||||
get commandSuggestions(){
|
||||
var raw = (this.input||'').trim();
|
||||
@@ -354,7 +564,7 @@
|
||||
var first = (q.split(/\s+/).filter(Boolean)[0] || '');
|
||||
var all = [
|
||||
{cmd:'/help', desc:'liste des commandes'},
|
||||
{cmd:'/providers', desc:'liste les fournisseurs et leurs clés'},
|
||||
{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'}
|
||||
@@ -365,24 +575,13 @@
|
||||
useSuggestion(c){
|
||||
var v = c.cmd.replace(/\s*<[^>]*>\s*$/, ' ');
|
||||
this.input = v;
|
||||
if(this.$refs.cmdInput) this.$refs.cmdInput.focus();
|
||||
var t = this.$refs && this.$refs.cmdInput;
|
||||
if(t){ t.focus(); this.resizeInput(); }
|
||||
},
|
||||
echo(msg){
|
||||
this.messages.push({id:'cmd-'+Date.now()+Math.random(), role:'assistant', content: msg, model:''});
|
||||
this.scrollBottom();
|
||||
},
|
||||
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(){});
|
||||
},
|
||||
handleCommand(text){
|
||||
var self = this;
|
||||
var parts = text.replace(/^\/+/, '').trim().split(/\s+/);
|
||||
@@ -435,39 +634,38 @@
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// ── Send / SSE ──
|
||||
async send(){
|
||||
var self = this;
|
||||
if(self.streaming) return;
|
||||
var text = (self.input || '').trim();
|
||||
if(!text) return;
|
||||
if(self.commandOpen && self.handleCommand(text)){ self.input=''; return; }
|
||||
if(self.commandOpen && self.handleCommand(text)){ self.input=''; this.resizeInput(); return; }
|
||||
try { await self.ensureConversation(); } catch(e){ return; }
|
||||
self.input = '';
|
||||
this.resizeInput();
|
||||
self.errorMsg = '';
|
||||
self.streaming = true;
|
||||
self.liveReasoning = '…';
|
||||
|
||||
self.messages.push({id:'u-'+Date.now(), role:'user', content: text});
|
||||
self.scrollBottom();
|
||||
|
||||
var respBuf = '';
|
||||
var payload = {message: text};
|
||||
if(self.llmProvider) payload.provider = self.llmProvider;
|
||||
if(self.llmModel) payload.model = self.llmModel;
|
||||
// Attach the currently open document/page so the agent can interact
|
||||
// with it; empty on general screens (then the agent guides on the app).
|
||||
var docCtx = self.docContext();
|
||||
if(docCtx) payload.context = docCtx;
|
||||
var ctxText = self.contextPayload();
|
||||
if(ctxText) payload.context = ctxText;
|
||||
|
||||
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)); });
|
||||
}
|
||||
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; }
|
||||
@@ -488,8 +686,6 @@
|
||||
}
|
||||
else if(ev.type==='final'){
|
||||
self.messages.push({id:'f-'+Date.now(), role:'assistant', content: ev.content});
|
||||
// "Ask AI" / "AI meeting note" (page editor) register this hook
|
||||
// so the generated text is inserted directly into the page.
|
||||
if(window.__fdInsertAI){ try{ var h=window.__fdInsertAI; window.__fdInsertAI=null; h(ev.content||''); }catch(e){} }
|
||||
}
|
||||
else if(ev.type==='error'){ self.errorMsg = ev.message; }
|
||||
@@ -512,21 +708,16 @@
|
||||
},
|
||||
scrollBottom(){
|
||||
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
|
||||
},
|
||||
get providerLabel(){
|
||||
var p = this.llmProvider || this.provider || 'offline';
|
||||
return p === 'offline' ? 'offline · mock' : p;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('alpine:init', function(){
|
||||
Alpine.data('agentPanel', agentPanel);
|
||||
});
|
||||
window.agentPanel = agentPanel;
|
||||
|
||||
// ── Context reader ──
|
||||
// Reads the document currently open in the page editor so the Agent can
|
||||
// interact with it. Returns '' outside of a document (workspace, dashboard…):
|
||||
// the agent then answers general "how do I use FlowDeck ?" questions.
|
||||
// ═══════════ Context reader — document (page editor) ═══════════
|
||||
window.fdReadPageContext = function(){
|
||||
try{
|
||||
var tEl = document.getElementById('_titleEl');
|
||||
@@ -551,6 +742,100 @@
|
||||
}catch(e){ return ''; }
|
||||
};
|
||||
if(!window.fdGetPageContext) window.fdGetPageContext = window.fdReadPageContext;
|
||||
window.agentPanel = agentPanel;
|
||||
|
||||
// ═══════════ 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 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';
|
||||
return {kind:'page', icon:'📄', label:'Document « ' + title + ' »', text: raw, key:'page'};
|
||||
}
|
||||
}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='📄'; label=labelFromTitle() || 'Page en cours'; kind='page'; }
|
||||
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='🗄️'; label='Base de données'; kind='db'; t = labelFromTitle(); if(t) label = 'Base — ' + t; }
|
||||
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';
|
||||
}
|
||||
return {kind: kind, icon: icon, label: label, text: '', key: p};
|
||||
}
|
||||
return {
|
||||
current: function(){
|
||||
var m = marker();
|
||||
if(m) return m;
|
||||
var d = docContext();
|
||||
if(d) return d;
|
||||
return byPath();
|
||||
}
|
||||
};
|
||||
})();
|
||||
window.FlowDeckCtx = FlowDeckCtx;
|
||||
|
||||
// Backward-compatible detection hooks (banner + Ask AI)
|
||||
window.fdPageContextProvider = function(){
|
||||
var c = FlowDeckCtx.current();
|
||||
return c && c.kind === 'page' ? c.text || '' : '';
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -210,7 +210,7 @@
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"/></svg>
|
||||
Home
|
||||
</a>
|
||||
<button class="nav-icon-btn" title="Chat">
|
||||
<button class="nav-icon-btn" title="Chat (Ctrl+J)" onclick="window.fdAgent&&window.fdAgent.open()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||
</button>
|
||||
<button class="nav-icon-btn" title="Inbox">
|
||||
@@ -944,10 +944,12 @@
|
||||
}).catch(function(){});
|
||||
},
|
||||
agentOpen(id) {
|
||||
document.dispatchEvent(new CustomEvent('fd-agent-toggle', { detail: { agentId: id || null } }));
|
||||
if(window.fdAgent && window.fdAgent.open){ window.fdAgent.open(); }
|
||||
else { document.dispatchEvent(new CustomEvent('fd-agent-toggle', { detail: { agentId: id || null } })); }
|
||||
},
|
||||
agentNew() {
|
||||
document.dispatchEvent(new CustomEvent('fd-agent-toggle'));
|
||||
if(window.fdAgent && window.fdAgent.open){ window.fdAgent.open(); }
|
||||
else { document.dispatchEvent(new CustomEvent('fd-agent-toggle')); }
|
||||
},
|
||||
sidebarCollapsed: false,
|
||||
mobileSidebarOpen: false,
|
||||
|
||||
Reference in New Issue
Block a user