feat(agent): v4.12.0 - refonte config 'Agent & IA', fournisseurs actifs/testes dans le panneau, contexte document + guide d'utilisation
This commit is contained in:
+105
-12
@@ -39,6 +39,10 @@
|
||||
.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)}
|
||||
|
||||
</style>
|
||||
|
||||
<button id="fd-agent-fab" title="FlowDeck Agent" onclick="document.dispatchEvent(new CustomEvent('fd-agent-toggle'))">🤖</button>
|
||||
@@ -53,7 +57,7 @@
|
||||
<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.name"></option>
|
||||
<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">
|
||||
@@ -64,6 +68,17 @@
|
||||
<a href="/settings" target="_blank" class="fd-agent-gear" title="Configurer (clés API, Settings → Agent & IA)" @click.stop>⚙️</a>
|
||||
</div>
|
||||
|
||||
<div class="fd-agent-noconfig" x-show="!hasActiveProvider">
|
||||
<span>⚠️ Aucun fournisseur activé et fonctionnel.</span>
|
||||
<a href="/settings" target="_blank">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>
|
||||
</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>
|
||||
@@ -123,7 +138,8 @@
|
||||
agents: [], conversations: [], currentConv: null,
|
||||
messages: [], input: '', streaming: false, liveReasoning: '',
|
||||
provider: 'offline', providerAvailable: false, errorMsg: '',
|
||||
providers: [], providerModels: [], llmProvider: 'offline', llmModel: '', llmKeys: [],
|
||||
allProviders: [], providers: [], providerModels: [], llmProvider: 'offline',
|
||||
llmModel: '', llmKeys: [], defaultProvider: 'offline', ctxDetected: false,
|
||||
|
||||
init(){
|
||||
var self = this;
|
||||
@@ -131,6 +147,7 @@
|
||||
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();
|
||||
});
|
||||
this.fetchProviders();
|
||||
@@ -148,14 +165,25 @@
|
||||
};
|
||||
},
|
||||
|
||||
// Only providers *configured and functional* (verified) are selectable,
|
||||
// plus the built-in "offline" mock so the chat always stays usable.
|
||||
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.providers = d.providers || [];
|
||||
self.allProviders = d.providers || [];
|
||||
self.llmKeys = d.keys || [];
|
||||
if(!self.llmProvider) self.llmProvider = d.provider || 'offline';
|
||||
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(!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(){});
|
||||
@@ -174,6 +202,32 @@
|
||||
self.providerModels = (p && p.models) || [];
|
||||
self.llmModel = (p && p.default_model) || ((p && p.models && p.models[0]) || '');
|
||||
},
|
||||
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;
|
||||
},
|
||||
get chatContextActive(){ return this.ctxDetected; },
|
||||
detectCtx(){
|
||||
var has = false;
|
||||
try{ has = !!(window.fdGetPageContext && String(window.fdGetPageContext()).trim()); }catch(e){ has = false; }
|
||||
this.ctxDetected = has;
|
||||
},
|
||||
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 '';
|
||||
},
|
||||
fetchAgents(){
|
||||
var self = this;
|
||||
fetch('/api/agent').then(function(r){return r.json()}).then(function(d){
|
||||
@@ -197,8 +251,13 @@
|
||||
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}; });
|
||||
if(d.conversation && d.conversation.provider){ self.llmProvider = d.conversation.provider; self.applyModels(); }
|
||||
if(d.conversation && d.conversation.model){ self.llmModel = d.conversation.model; }
|
||||
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;
|
||||
}
|
||||
self.applyModels();
|
||||
if(d.conversation && d.conversation.model && self.providerModels.indexOf(d.conversation.model) !== -1){ self.llmModel = d.conversation.model; }
|
||||
}).catch(function(){});
|
||||
},
|
||||
newConversation(){
|
||||
@@ -237,7 +296,7 @@
|
||||
});
|
||||
return self._ensuring;
|
||||
},
|
||||
openAgent(){ this.open=true; this.loadConversations(); },
|
||||
openAgent(){ this.open=true; this.detectCtx(); this.loadConversations(); },
|
||||
close(){ this.open=false; },
|
||||
get commandOpen(){ return /^\s*\//.test(this.input) && !this.streaming; },
|
||||
get commandSuggestions(){
|
||||
@@ -286,12 +345,13 @@
|
||||
return true;
|
||||
}
|
||||
if(cmd === 'providers'){
|
||||
var list = this.providers.map(function(x){ return x.id + (x.has_key ? ' ✓' : ''); }).join(', ');
|
||||
this.echo('Fournisseurs : ' + (list || 'aucun'));
|
||||
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 + ' ✓'; }).join(', ');
|
||||
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;
|
||||
}
|
||||
@@ -346,6 +406,10 @@
|
||||
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;
|
||||
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
@@ -402,14 +466,43 @@
|
||||
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
|
||||
},
|
||||
get providerLabel(){
|
||||
if(this.providerAvailable) return (this.llmProvider || this.provider);
|
||||
return 'offline mock';
|
||||
var p = this.llmProvider || this.provider || 'offline';
|
||||
return p === 'offline' ? 'offline · mock' : p;
|
||||
}
|
||||
};
|
||||
}
|
||||
document.addEventListener('alpine:init', function(){
|
||||
Alpine.data('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.
|
||||
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;
|
||||
window.agentPanel = agentPanel;
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user