feat(agent): v5.11.0 — auto-title per request, history hover preview, write_blocks normalization, Notion-style steps accordion
This commit is contained in:
+313
-12
@@ -37,6 +37,15 @@
|
||||
.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))}
|
||||
|
||||
/* History hover / long-press preview */
|
||||
.fd-ap-preview{position:fixed;width:290px;max-width:calc(100vw - 24px);background:var(--bg-modal,#1c1c1c);border:1px solid var(--border,rgba(255,255,255,.16));border-radius:12px;box-shadow:0 16px 44px rgba(0,0,0,.6);z-index:1990;padding:11px 13px;font-size:12px;color:var(--text,#e6e6e6);display:none;pointer-events:auto}
|
||||
.fd-ap-preview.show{display:block}
|
||||
.fd-ap-preview-title{font-weight:600;font-size:12.5px;margin-bottom:2px;color:var(--text,#fff);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-ap-preview-meta{font-size:10px;color:var(--text-dim,#999);margin-bottom:8px}
|
||||
.fd-ap-preview-empty{font-size:11px;color:var(--text-dim,#aaa);padding:4px 0}
|
||||
.fd-ap-preview-label{font-size:9.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim,#999);margin:7px 0 3px}
|
||||
.fd-ap-preview-txt{white-space:pre-wrap;word-break:break-word;line-height:1.55;max-height:72px;overflow:hidden;color:var(--text-secondary,#d0d0d0)}
|
||||
|
||||
/* 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}
|
||||
@@ -66,6 +75,29 @@
|
||||
.fd-ap-err{color:#ef8a8a;font-size:12px;padding:4px 2px}
|
||||
.fd-ap-notice{color:var(--text-secondary,#ccc);font-size:12px;padding:7px 10px;margin:2px 0 8px;border-radius:9px;background:rgba(128,128,128,.12);border:1px solid var(--border,rgba(255,255,255,.12))}
|
||||
|
||||
/* Steps accordion (Notion-style reasoning + actions audit trail) */
|
||||
.fd-steps{margin:0 0 8px;border:1px solid var(--border,rgba(255,255,255,.11));border-radius:10px;background:rgba(255,255,255,.03);overflow:hidden}
|
||||
.fd-steps-head{display:flex;align-items:center;gap:7px;width:100%;padding:7px 11px;background:transparent;border:none;color:var(--text-secondary,#ccc);font-size:12px;cursor:pointer;text-align:left}
|
||||
.fd-steps-head:hover{background:rgba(255,255,255,.05)}
|
||||
.fd-steps-chev{display:inline-block;font-size:9px;color:var(--text-dim,#999);transition:transform .15s;width:12px;flex-shrink:0;text-align:center}
|
||||
.fd-steps-chev.down{transform:rotate(90deg)}
|
||||
.fd-steps-count{font-weight:600;color:var(--text,#eee)}
|
||||
.fd-steps-elem{font-weight:400;color:var(--text-dim,#999);margin-left:6px}
|
||||
.fd-steps-body{border-top:1px solid var(--border,rgba(255,255,255,.08));padding:6px 8px 8px;max-height:340px;overflow-y:auto}
|
||||
.fd-step-thought{margin:3px 0}
|
||||
.fd-step-thought-head{display:flex;align-items:center;gap:6px;width:100%;border:none;background:transparent;color:var(--text-dim,#b9b9b9);font-size:12px;text-align:left;cursor:pointer;padding:4px 5px;border-radius:7px}
|
||||
.fd-step-thought-head:hover{background:rgba(128,128,128,.1);color:var(--text,#fff)}
|
||||
.fd-step-thought-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.fd-step-thought-title .fd-t-ico{opacity:.85}
|
||||
.fd-step-thought-body{margin:2px 0 4px 27px;padding:7px 10px;font-size:12px;color:var(--text-secondary,#cfcfcf);background:rgba(128,128,128,.1);border-left:2px solid rgba(35,131,226,.45);border-radius:0 8px 8px 0;white-space:pre-wrap;word-break:break-word;line-height:1.55}
|
||||
.fd-step-action{display:flex;align-items:flex-start;gap:7px;padding:4px 7px;font-size:12px;color:var(--text-secondary,#c9c9c9)}
|
||||
.fd-step-action .fd-step-ico{flex-shrink:0;width:14px;text-align:center;font-size:11px;margin-top:1px}
|
||||
.fd-step-action.success{color:var(--text,#d9d9d9)}
|
||||
.fd-step-action.success .fd-step-ico{color:#58c47a}
|
||||
.fd-step-action.error{color:#e78c8c}
|
||||
.fd-step-action.error .fd-step-ico{color:#ef6a6a}
|
||||
.fd-step-action-detail{font-size:11px;color:#e0a3a3;display:block;margin-top:1px}
|
||||
|
||||
/* 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}
|
||||
@@ -199,7 +231,7 @@
|
||||
<div class="fd-ap-head">
|
||||
<span class="fd-ap-logo"><svg viewBox="0 0 24 24" width="19" height="19" fill="currentColor" aria-hidden="true"><path d="M12 3l1.9 5.8a2 2 0 0 0 1.3 1.3L21 12l-5.8 1.9a2 2 0 0 0-1.3 1.3L12 21l-1.9-5.8a2 2 0 0 0-1.3-1.3L3 12l5.8-1.9a2 2 0 0 0 1.3-1.3L12 3Z"/></svg></span>
|
||||
<div class="fd-ap-head-main">
|
||||
<div class="fd-ap-title">How can I help you today?</div>
|
||||
<div class="fd-ap-title" x-text="currentConv && currentConv.title ? currentConv.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>
|
||||
@@ -223,7 +255,11 @@
|
||||
<template x-if="tab==='history'">
|
||||
<div>
|
||||
<template x-for="c in conversations" :key="c.id">
|
||||
<div class="fd-ap-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="historyClick(c.id)"
|
||||
@mouseenter="previewConversation(c, $event)" @mouseleave="schedulePreviewHide()"
|
||||
@touchstart="startLongPress(c, $event)" @touchend="onTouchEnd()"
|
||||
@touchmove="onTouchCancel()" @touchcancel="onTouchCancel()">
|
||||
<div x-text="c.title || 'New conversation'"></div>
|
||||
<div style="font-size:11px;color:var(--text-dim)" x-text="c.updated_at"></div>
|
||||
</div>
|
||||
@@ -253,6 +289,38 @@
|
||||
<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==='assistant'?'Agent':'Vous')+(m.model?' · '+m.model:'')"></div>
|
||||
<!-- Steps accordion (reasoning thoughts + tool actions) -->
|
||||
<div class="fd-steps" x-show="m.steps && m.steps.length" x-cloak>
|
||||
<button class="fd-steps-head" type="button" @click="toggleSteps(m)">
|
||||
<span class="fd-steps-chev" :class="{down: m.stepsOpen}">▶</span>
|
||||
<span class="fd-steps-count" x-text="m.steps.length === 1 ? '1 étape' : m.steps.length + ' étapes'"></span>
|
||||
<span class="fd-steps-elem" x-text="m.generating ? 'en cours…' : (m.stepsOpen ? 'réduire' : 'voir le déroulé')"></span>
|
||||
</button>
|
||||
<div class="fd-steps-body" x-show="m.stepsOpen" x-cloak>
|
||||
<template x-for="(s, si) in m.steps" :key="si">
|
||||
<div>
|
||||
<template x-if="s.type==='thought'">
|
||||
<div class="fd-step-thought">
|
||||
<button class="fd-step-thought-head" type="button" @click="toggleStep(s)">
|
||||
<span class="fd-steps-chev" :class="{down: s.open}">▶</span>
|
||||
<span class="fd-step-thought-title"><span class="fd-t-ico">💭</span> <span x-text="s.title || 'Réflexion'"></span></span>
|
||||
</button>
|
||||
<div class="fd-step-thought-body" x-show="s.open" x-text="s.content"></div>
|
||||
</div>
|
||||
</template>
|
||||
<template x-if="s.type==='action'">
|
||||
<div class="fd-step-action" :class="s.status || 'success'" :title="(s.detail && s.status==='error') ? s.detail : s.tool || ''">
|
||||
<span class="fd-step-ico" x-text="(s.status==='success') ? '✓' : '✗'"></span>
|
||||
<div>
|
||||
<div x-text="s.label"></div>
|
||||
<div class="fd-step-action-detail" x-show="s.status==='error' && s.detail" x-text="s.detail" x-cloak></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fd-agent-msg-content" x-show="m.generating" x-cloak>Génération…</div>
|
||||
<div class="fd-agent-msg-content fd-agent-msg-markdown" x-show="!m.generating && m.role==='assistant' && m.content" x-html="renderMarkdown(m.content)"></div>
|
||||
<div class="fd-agent-msg-content" x-show="!m.generating && m.role!=='assistant' && m.content" x-text="m.content"></div>
|
||||
@@ -276,7 +344,7 @@
|
||||
</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 class="fd-agent-thinking" x-show="streaming && !(_livePh && _livePh.steps && _livePh.steps.length)">L'agent réfléchit et agit… <span x-text="liveReasoning"></span></div>
|
||||
<div class="fd-ap-notice" x-show="notice" x-cloak x-text="notice"></div>
|
||||
<div class="fd-ap-err" x-show="errorMsg" x-text="errorMsg"></div>
|
||||
</div>
|
||||
@@ -372,6 +440,18 @@
|
||||
<a href="/settings" target="_blank" @click="modelOpen=false">⚙️ Configurer (clés API) →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Historique : aperçu (hover / long-press) #}
|
||||
<div class="fd-ap-preview" x-ref="convPreview" :class="{show: previewVisible}"
|
||||
@mouseenter="cancelPreviewHide()" @mouseleave="schedulePreviewHide()" x-cloak>
|
||||
<div class="fd-ap-preview-title" x-text="previewTitle"></div>
|
||||
<div class="fd-ap-preview-meta" x-text="previewMeta"></div>
|
||||
<div class="fd-ap-preview-empty" x-show="!previewStart && !previewEnd" x-text="previewEmptyHint"></div>
|
||||
<div class="fd-ap-preview-label" x-show="previewStart" x-cloak>Début</div>
|
||||
<div class="fd-ap-preview-txt" x-show="previewStart" x-text="previewStart"></div>
|
||||
<div class="fd-ap-preview-label" x-show="previewEnd" x-cloak>Réponse</div>
|
||||
<div class="fd-ap-preview-txt" x-show="previewEnd" x-text="previewEnd"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -423,6 +503,9 @@
|
||||
mentionFocus: -1, _mentionFrom: null, _mentionTimer: null, _atNode: null,
|
||||
cmdFocus: -1, slashDismiss: false,
|
||||
toastMsg: '', toastErr: false, _toastTimer: null,
|
||||
_livePh: null,
|
||||
previewVisible: false, previewTitle: '', previewMeta: '', previewStart: '', previewEnd: '',
|
||||
_previewCache: null, _previewTimer: null, _longPressTimer: null, _suppressNextClick: false,
|
||||
_slashSeq: 0,
|
||||
|
||||
init(){
|
||||
@@ -498,6 +581,7 @@
|
||||
if(!this.open) return;
|
||||
this.open = false;
|
||||
this.modelOpen = false;
|
||||
this.hidePreview();
|
||||
if(this._applyState) this._applyState();
|
||||
},
|
||||
toggle(){ this.open ? this.close() : this.openAgent(); },
|
||||
@@ -740,6 +824,7 @@
|
||||
var self = this;
|
||||
this.tab='chat';
|
||||
this.errorMsg = '';
|
||||
this.hidePreview();
|
||||
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}; });
|
||||
@@ -747,8 +832,119 @@
|
||||
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; }
|
||||
// Rebuild the steps accordion from the audit trail (actions only —
|
||||
// the reasoning thoughts are not persisted, only shown live).
|
||||
self.attachPersistedSteps(id);
|
||||
}).catch(function(){});
|
||||
},
|
||||
attachPersistedSteps(id){
|
||||
var self = this;
|
||||
fetch('/api/agent/conversations/'+id+'/actions').then(function(r){return r.json()}).then(function(d){
|
||||
var acts = d.actions || [];
|
||||
if(!acts.length) return;
|
||||
var lastAsst = null;
|
||||
for(var i=self.messages.length-1;i>=0;i--){
|
||||
var m = self.messages[i];
|
||||
if(m.role==='assistant' && !m.proposal){ lastAsst = m; break; }
|
||||
}
|
||||
if(!lastAsst) return;
|
||||
var steps = acts.map(function(a){
|
||||
var res = null;
|
||||
try{ res = a.result_json ? JSON.parse(a.result_json) : {}; }catch(e){ res = {}; }
|
||||
var msg = (res && res.message) ? res.message : '';
|
||||
return agentPanel._actionStep({
|
||||
tool: a.tool_name, status: a.status, target_id: a.target_id,
|
||||
message: msg, detail: msg || ''
|
||||
});
|
||||
});
|
||||
lastAsst.steps = steps;
|
||||
lastAsst.stepsOpen = false;
|
||||
}).catch(function(){});
|
||||
},
|
||||
get previewEmptyHint(){ return 'Aucun échange enregistré pour cette conversation.'; },
|
||||
historyClick(id){
|
||||
if(this._suppressNextClick){ this._suppressNextClick = false; return; }
|
||||
this.loadConversation(id);
|
||||
},
|
||||
_previewSummary(msgs){
|
||||
var first = '', last = '';
|
||||
for(var i=0;i<msgs.length;i++){
|
||||
var r = msgs[i].role;
|
||||
var t = (msgs[i].content || '');
|
||||
if(r==='user'){ if(!first && t.trim()) first = agentPanel._strip(t); }
|
||||
else if(r==='assistant'){ if(t.trim()) last = agentPanel._strip(t); }
|
||||
}
|
||||
return { first: first, last: last, total: msgs.length };
|
||||
},
|
||||
previewConversation(c, ev){
|
||||
var self = this;
|
||||
// On touch: a quick tap triggers a synthetic mouseenter AFTER touchend —
|
||||
// skip it so we don't flash the preview before navigating (the long-press
|
||||
// timer handles the preview itself).
|
||||
if(ev && ev.type === 'mouseenter' && self._suppressEnterUntil && Date.now() < self._suppressEnterUntil) return;
|
||||
var rect = (ev && ev.currentTarget) ? ev.currentTarget.getBoundingClientRect() : null;
|
||||
var cache = self._previewCache || (self._previewCache = {});
|
||||
function show(sum){
|
||||
if(!sum) return;
|
||||
self.previewTitle = c.title || 'New conversation';
|
||||
var dateStr = (c.updated_at || '').replace('T', ' ').slice(0, 16);
|
||||
self.previewMeta = sum.total + ' message(s)' + (dateStr ? ' · ' + dateStr : '');
|
||||
self.previewStart = sum.first || '';
|
||||
self.previewEnd = sum.last || '';
|
||||
self.previewVisible = true;
|
||||
self.cancelLongPress();
|
||||
self.$nextTick(function(){
|
||||
var el = self.$refs && self.$refs.convPreview;
|
||||
if(!el) return;
|
||||
var pw = el.offsetWidth || 290;
|
||||
var m = 10;
|
||||
var x = rect ? (rect.left - pw - m) : m;
|
||||
if(x < m && rect) x = rect.right + m;
|
||||
var y = rect ? Math.min(rect.top, window.innerHeight - el.offsetHeight - m) : m;
|
||||
if(y < m) y = m;
|
||||
el.style.left = Math.round(x) + 'px';
|
||||
el.style.top = Math.round(y) + 'px';
|
||||
});
|
||||
}
|
||||
if(cache[c.id]){ show(cache[c.id]); return; }
|
||||
fetch('/api/agent/conversations/'+c.id).then(function(r){ return r.json(); }).then(function(d){
|
||||
var sum = self._previewSummary(d.messages || []);
|
||||
cache[c.id] = sum;
|
||||
show(sum);
|
||||
}).catch(function(){ self.schedulePreviewHide(); });
|
||||
},
|
||||
schedulePreviewHide(){
|
||||
var self = this;
|
||||
if(this._previewTimer) clearTimeout(this._previewTimer);
|
||||
this._previewTimer = setTimeout(function(){ self.previewVisible = false; self._previewTimer = null; }, 220);
|
||||
},
|
||||
cancelPreviewHide(){
|
||||
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
|
||||
},
|
||||
startLongPress(c, ev){
|
||||
var self = this;
|
||||
this.cancelLongPress();
|
||||
this._longPressTimer = setTimeout(function(){
|
||||
self._suppressNextClick = true;
|
||||
self.previewConversation(c, ev);
|
||||
}, 550);
|
||||
},
|
||||
cancelLongPress(){
|
||||
if(this._longPressTimer){ clearTimeout(this._longPressTimer); this._longPressTimer = null; }
|
||||
},
|
||||
onTouchEnd(){
|
||||
// A quick tap: suppress the synthetic mouseenter preview flash right
|
||||
// before the click navigates into the conversation.
|
||||
this.cancelLongPress();
|
||||
this._suppressEnterUntil = Date.now() + 400;
|
||||
},
|
||||
onTouchCancel(){
|
||||
this.cancelLongPress();
|
||||
},
|
||||
hidePreview(){
|
||||
if(this._previewTimer){ clearTimeout(this._previewTimer); this._previewTimer = null; }
|
||||
this.previewVisible = false;
|
||||
},
|
||||
newConversation(){
|
||||
var self = this;
|
||||
var body = {title:'Nouvelle conversation'};
|
||||
@@ -1592,6 +1788,16 @@
|
||||
}).catch(function(){ self.toast('Retour non envoyé.', true); });
|
||||
},
|
||||
|
||||
toggleSteps(m){
|
||||
if(!m) return;
|
||||
m.stepsOpen = !m.stepsOpen;
|
||||
this.$nextTick(function(){ this.scrollBottom(); });
|
||||
},
|
||||
toggleStep(s){
|
||||
if(!s) return;
|
||||
s.open = !s.open;
|
||||
},
|
||||
|
||||
// ── Send / SSE ──
|
||||
async send(){
|
||||
var self = this;
|
||||
@@ -1642,6 +1848,23 @@
|
||||
|
||||
self.streaming = true;
|
||||
self.liveReasoning = '…';
|
||||
self._livePh = null;
|
||||
var runSteps = [];
|
||||
var ph = null;
|
||||
function ensurePlaceholder(finalContent){
|
||||
if(ph) return ph;
|
||||
ph = {id:'r-'+Date.now(), role:'assistant', generating: !finalContent,
|
||||
content: finalContent || '', steps: [], stepsOpen: true,
|
||||
model: self.autoLabel || ''};
|
||||
self.messages.push(ph);
|
||||
self._livePh = ph;
|
||||
return ph;
|
||||
}
|
||||
function addStep(s){
|
||||
runSteps.push(s);
|
||||
if(ph){ ph.steps = runSteps.slice(); ph.generating = true; }
|
||||
if(self._livePh){ self._livePh.steps = runSteps.slice(); }
|
||||
}
|
||||
self.messages.push({id:'u-'+Date.now(), role:'user', content: displayText});
|
||||
self.scrollBottom();
|
||||
|
||||
@@ -1673,19 +1896,31 @@
|
||||
if(!line) return;
|
||||
try{
|
||||
var ev = JSON.parse(line);
|
||||
if(ev.type==='reasoning'){ self.liveReasoning = ev.content; }
|
||||
if(ev.type==='reasoning'){
|
||||
// A thought: collapsed by default, expandable to its full text.
|
||||
addStep({type:'thought', title:agentPanel._thoughtTitle(ev.content),
|
||||
content:(ev.content||'').replace(/\s+$/,''), open:false});
|
||||
}
|
||||
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, act:true});
|
||||
addStep(agentPanel._actionStep(ev));
|
||||
}
|
||||
else if(ev.type==='final'){
|
||||
self.messages.push({id:'f-'+Date.now(), role:'assistant', content: ev.content});
|
||||
var fph = ensurePlaceholder(ev.content);
|
||||
fph.generating = false;
|
||||
fph.content = ev.content;
|
||||
fph.steps = runSteps.slice();
|
||||
fph.stepsOpen = false;
|
||||
fph.model = self.autoLabel || '';
|
||||
if(window.__fdInsertAI){ try{ var h=window.__fdInsertAI; window.__fdInsertAI=null; h(ev.content||''); }catch(e){} }
|
||||
}
|
||||
else if(ev.type==='notice'){ self.notice = ev.message; }
|
||||
else if(ev.type==='error'){ self.errorMsg = ev.message; }
|
||||
else if(ev.type==='error'){
|
||||
var eph = ensurePlaceholder();
|
||||
eph.generating = false;
|
||||
eph.content = '⚠️ ' + ev.message;
|
||||
eph.steps = runSteps.slice();
|
||||
self.errorMsg = ev.message;
|
||||
}
|
||||
}catch(e){}
|
||||
});
|
||||
self.scrollBottom();
|
||||
@@ -1695,13 +1930,22 @@
|
||||
return read();
|
||||
}).catch(function(err){ self.errorMsg = (err && err.message) ? err.message : 'Erreur agent'; })
|
||||
.finally(function(){
|
||||
self.streaming = false; self.liveReasoning='';
|
||||
self.streaming = false; self.liveReasoning=''; self._livePh = null;
|
||||
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(){});
|
||||
fetch('/api/agent/conversations').then(function(r){return r.json()}).then(function(d){
|
||||
self.conversations = d.conversations || [];
|
||||
// keep the open conversation in sync so its auto-updated title shows
|
||||
// immediately in the header and in a later History screen.
|
||||
var cur = self.currentConv;
|
||||
if(cur && cur.id){
|
||||
var match = self.conversations.find(function(c){ return c.id === cur.id; });
|
||||
if(match){ cur.title = match.title; cur.updated_at = match.updated_at; }
|
||||
}
|
||||
}).catch(function(){});
|
||||
},
|
||||
scrollBottom(){
|
||||
this.$nextTick(function(){ var el = document.querySelector('.fd-agent-body'); if(el) el.scrollTop = el.scrollHeight; });
|
||||
@@ -1713,6 +1957,63 @@
|
||||
Alpine.data('agentPanel', agentPanel);
|
||||
});
|
||||
window.agentPanel = agentPanel;
|
||||
// Plain-text helper for the history preview (strip markdown noise).
|
||||
agentPanel._strip = function(t){
|
||||
if(!t) return '';
|
||||
return String(t)
|
||||
.replace(/```[\s\S]*?```/g, ' [code] ')
|
||||
.replace(/[#*_>`~]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 280);
|
||||
};
|
||||
// Short title for a reasoning thought (first non-empty line, truncated).
|
||||
agentPanel._thoughtTitle = function(text){
|
||||
var s = (text || '').trim();
|
||||
if(!s) return 'Réflexion';
|
||||
// Skip leading blank lines and get first meaningful line
|
||||
var lines = s.split(/\r?\n/);
|
||||
var first = '';
|
||||
for(var i=0;i<lines.length;i++){
|
||||
var ln = lines[i].trim();
|
||||
if(ln){ first = ln; break; }
|
||||
}
|
||||
if(!first) return 'Réflexion';
|
||||
first = first.replace(/^#+\s*/, ''); // strip markdown heading prefix
|
||||
return first.length <= 72 ? first : first.slice(0,72).trimEnd() + '…';
|
||||
};
|
||||
// Past-tense label for each known agent action.
|
||||
agentPanel._actionLabel = {
|
||||
read_workspaces:'Espaces de travail listés', search_workspace:'Recherche effectuée',
|
||||
read_document:'Document lu', read_collection:'Collection lue', read_page:'Page lue',
|
||||
create_document:'Document créé', create_collection:'Collection créée',
|
||||
create_page:'Page créée', create_view:'Vue créée', add_property:'Propriété ajoutée',
|
||||
add_relation:'Relation ajoutée', update_page:'Page mise à jour',
|
||||
write_blocks:'Contenu écrit', delete_document:'Document supprimé',
|
||||
delete_page:'Page supprimée', delete_collection:'Collection supprimée',
|
||||
apply_template:'Template appliqué', create_sub_item:'Sous-élément créé',
|
||||
add_dependency:'Dépendance ajoutée', read_gitea_issues:'Issues Gitea lues',
|
||||
create_gitea_issue:'Issue Gitea créée', sync_gitea:'Issues Gitea synchronisées'
|
||||
};
|
||||
// Build an action step object from an SSE event or a persisted action row.
|
||||
agentPanel._actionStep = function(ev){
|
||||
var ok = ev.status !== 'error';
|
||||
var tool = String(ev.tool || '');
|
||||
var label = agentPanel._actionLabel[tool] || tool.replace(/_/g,' ');
|
||||
var extra = '';
|
||||
if(ok && ev.target_id != null && ev.target_id !== '' && ev.target_id !== 0){
|
||||
extra = ' → #' + ev.target_id;
|
||||
}
|
||||
if(ok && ev.message && ev.message.length <= 46){
|
||||
extra += (extra ? ' — ' : ' · ') + ev.message;
|
||||
}
|
||||
return {
|
||||
type:'action', tool:tool,
|
||||
status: ok ? 'success' : 'error',
|
||||
label: label + extra,
|
||||
detail: ok ? '' : (ev.detail || ev.message || '')
|
||||
};
|
||||
};
|
||||
|
||||
// ═══════════ Context reader — document (page editor) ═══════════
|
||||
window.fdReadPageContext = function(){
|
||||
|
||||
Reference in New Issue
Block a user