feat(agent): v4.11.0 - clés API par utilisateur, chargement dynamique modèles, commandes slash
FlowDeck CI / test (push) Failing after 7s
FlowDeck CI / docker (push) Skipped

This commit is contained in:
2026-09-05 11:47:58 -04:00
parent 1c11b9d351
commit 34368a4946
10 changed files with 772 additions and 24 deletions
+111 -5
View File
@@ -31,6 +31,12 @@
.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-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 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)}
</style>
@@ -55,7 +61,7 @@
<option :value="m" x-text="m"></option>
</template>
</select>
<a href="/settings" target="_blank" class="fd-agent-gear" title="Configurer (clé API, admin)" @click.stop>⚙️</a>
<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-tabs">
@@ -94,7 +100,16 @@
</div>
<div class="fd-agent-input-area">
<textarea class="fd-agent-input" rows="2" placeholder="Demandez à l'agent (ex: crée une collection Projets)…" x-model="input" @keydown.enter.exact.prevent="send()"></textarea>
<div style="position:relative;flex:1;">
<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)">
<b x-text="c.cmd"></b><span x-text="c.desc"></span>
</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>
</div>
@@ -108,7 +123,7 @@
agents: [], conversations: [], currentConv: null,
messages: [], input: '', streaming: false, liveReasoning: '',
provider: 'offline', providerAvailable: false, errorMsg: '',
providers: [], providerModels: [], llmProvider: 'offline', llmModel: '',
providers: [], providerModels: [], llmProvider: 'offline', llmModel: '', llmKeys: [],
init(){
var self = this;
@@ -128,6 +143,7 @@
self.provider = d.provider || 'offline';
self.providerAvailable = !!d.available;
self.providers = d.providers || [];
self.llmKeys = d.keys || [];
if(!self.llmProvider) self.llmProvider = d.provider || 'offline';
if(!self.llmModel) self.llmModel = d.model || '';
self.applyModels();
@@ -188,10 +204,100 @@
},
openAgent(){ this.open=true; this.loadConversations(); },
close(){ this.open=false; },
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] || '');
var all = [
{cmd:'/help', desc:'liste des commandes'},
{cmd:'/providers', desc:'liste les fournisseurs et leurs clés'},
{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'}
];
if(!first) return all;
return all.filter(function(c){ return c.cmd.toLowerCase().replace(/^\/+/, '').indexOf(first) === 0; });
},
useSuggestion(c){
var v = c.cmd.replace(/\s*<[^>]*>\s*$/, ' ');
this.input = v;
if(this.$refs.cmdInput) this.$refs.cmdInput.focus();
},
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+/);
var cmd = (parts.shift() || '').toLowerCase();
var arg = parts.join(' ').trim();
if(cmd === 'help' || cmd === 'aide' || cmd === '?'){
this.echo('Commandes : /providers · /keys · /provider <nom> · /model <nom> · /help');
return true;
}
if(cmd === 'providers'){
var list = this.providers.map(function(x){ return x.id + (x.has_key ? ' ✓' : ''); }).join(', ');
this.echo('Fournisseurs : ' + (list || 'aucun'));
return true;
}
if(cmd === 'keys'){
var saved = this.llmKeys.map(function(k){ return k.provider + ' ✓'; }).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;
},
send(){
var self = this;
if(!self.currentConv || self.streaming || !self.input.trim()) return;
if(!self.currentConv || self.streaming) return;
var text = self.input.trim();
if(!text) return;
if(self.commandOpen && self.handleCommand(text)){ self.input=''; return; }
self.input = '';
self.errorMsg = '';
self.streaming = true;
@@ -257,7 +363,7 @@
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
},
get providerLabel(){
if(this.providerAvailable) return this.provider;
if(this.providerAvailable) return (this.llmProvider || this.provider);
return 'offline mock';
}
};