415 lines
22 KiB
HTML
415 lines
22 KiB
HTML
{# ════════ FlowDeck Agent panel (v4.10.0) — sidebar-triggered chat + floating button ════════ #}
|
|
<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-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-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-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>
|
|
|
|
<button id="fd-agent-fab" title="FlowDeck Agent" onclick="document.dispatchEvent(new CustomEvent('fd-agent-toggle'))">🤖</button>
|
|
|
|
<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>
|
|
</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és API, Settings → Agent & IA)" @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>
|
|
</div>
|
|
|
|
<div class="fd-agent-body">
|
|
<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 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>
|
|
</template>
|
|
|
|
<template x-if="tab=='chat'">
|
|
<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-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-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>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="fd-agent-input-area">
|
|
<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>
|
|
|
|
<script>
|
|
(function(){
|
|
function fmt(ev){ return ev; }
|
|
function agentPanel(){
|
|
return {
|
|
open: false, tab: 'chat',
|
|
agents: [], conversations: [], currentConv: null,
|
|
messages: [], input: '', streaming: false, liveReasoning: '',
|
|
provider: 'offline', providerAvailable: false, errorMsg: '',
|
|
providers: [], providerModels: [], llmProvider: 'offline', llmModel: '', llmKeys: [],
|
|
|
|
init(){
|
|
var self = this;
|
|
// The panel lives outside the root appState; toggle via the FAB + Agents sidebar.
|
|
window.addEventListener('fd-agent-toggle', function(e){
|
|
self.open = !self.open;
|
|
if(e && e.detail && e.detail.agentId){ self.agentId = e.detail.agentId; }
|
|
self.loadConversations();
|
|
});
|
|
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 || {};
|
|
window.fdAgent.ask = function(text){
|
|
var t = (text || '').trim();
|
|
if(!t) return;
|
|
self.openAgent();
|
|
self.input = t;
|
|
setTimeout(function(){ self.send(); }, 60);
|
|
};
|
|
},
|
|
|
|
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.llmKeys = d.keys || [];
|
|
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){
|
|
self.agents = d.agents || [];
|
|
}).catch(function(){});
|
|
},
|
|
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 = '';
|
|
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;
|
|
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;
|
|
self.messages = []; self.errorMsg=''; self.tab='chat';
|
|
}).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'};
|
|
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.tab='chat';
|
|
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;
|
|
},
|
|
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;
|
|
},
|
|
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; }
|
|
try { await self.ensureConversation(); } catch(e){ return; }
|
|
self.input = '';
|
|
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;
|
|
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();
|
|
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'){ self.liveReasoning = ev.content; }
|
|
else if(ev.type==='action'){
|
|
var msg = ev.status==='success'
|
|
? (ev.tool+' ✓ '+(ev.message||''))
|
|
: (ev.tool+' ✗ '+(ev.detail||'erreur'));
|
|
self.messages.push({id:'a-'+Date.now()+Math.random(), role:'assistant', content: msg});
|
|
}
|
|
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; }
|
|
}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.fetchConversationsRefresh();
|
|
});
|
|
},
|
|
fetchConversationsRefresh(){
|
|
var self = this;
|
|
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){ self.conversations = d.conversations || []; }).catch(function(){});
|
|
},
|
|
scrollBottom(){
|
|
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';
|
|
}
|
|
};
|
|
}
|
|
document.addEventListener('alpine:init', function(){
|
|
Alpine.data('agentPanel', agentPanel);
|
|
});
|
|
window.agentPanel = agentPanel;
|
|
})();
|
|
</script> |