feat(agent): v4.10.1 - config LLM dans l'UI (sélecteur provider/modèle, config runtime, admin + test connexion)
FlowDeck CI / test (push) Failing after 7s
FlowDeck CI / docker (push) Skipped

- Panneau agent : selects provider/modèles connus persistés par conversation
  (colonnes agent_conversations.provider/model, migration idempotente)
- GET /api/agent/providers enrichi (liste providers + modèles + requires_key)
- PATCH /api/agent/providers (admin) : config runtime DB-backed (table llm_config)
- POST /api/agent/providers/test : test connexion via LLMClient.ping() sans fallback mock
- Settings > Admin > Agent & IA : provider, modèle, clé API, URL API, save + test
- VERSION -> 4.10.1, CHANGELOG + ROADMAP mis à jour, 232 tests verts
This commit is contained in:
2026-09-05 10:58:09 -04:00
parent e752e46583
commit 3cb9edc1f3
11 changed files with 546 additions and 19 deletions
+48 -2
View File
@@ -28,6 +28,11 @@
.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-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>
<button id="fd-agent-fab" title="FlowDeck Agent" onclick="document.dispatchEvent(new CustomEvent('fd-agent-toggle'))">🤖</button>
@@ -39,6 +44,20 @@
<button class="fd-agent-close" @click="close()">&times;</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.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é API, admin)" @click.stop>⚙️</a>
</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>
@@ -89,6 +108,7 @@
agents: [], conversations: [], currentConv: null,
messages: [], input: '', streaming: false, liveReasoning: '',
provider: 'offline', providerAvailable: false, errorMsg: '',
providers: [], providerModels: [], llmProvider: 'offline', llmModel: '',
init(){
var self = this;
@@ -107,8 +127,26 @@
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 || [];
if(!self.llmProvider) self.llmProvider = d.provider || 'offline';
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]) || '');
},
fetchAgents(){
var self = this;
fetch('/api/agent').then(function(r){return r.json()}).then(function(d){
@@ -132,11 +170,16 @@
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; }
}).catch(function(){});
},
newConversation(){
var self = this;
fetch('/api/agent/conversations', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({title:'New conversation'})})
var body = {title:'New 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;
@@ -158,10 +201,13 @@
self.scrollBottom();
var respBuf = '';
var payload = {message: text};
if(self.llmProvider) payload.provider = self.llmProvider;
if(self.llmModel) payload.model = self.llmModel;
return fetch('/api/agent/conversations/'+self.currentConv.id+'/run', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({message: text})
body: JSON.stringify(payload)
}).then(function(resp){
if(!resp.ok){
return resp.json().then(function(j){ throw new Error(j.detail || ('HTTP '+resp.status)); });