mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Revert "Résultats d'outils : aperçu + « voir plus », vrai compteur, MCP complet"
This reverts commit 91d1796615.
This commit is contained in:
1 parent
f54c304019
commit
6aafeeea98
9 files changed
+37
-256
No files matched your search
@@ -616,16 +616,6 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
|||||||
"name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label,
|
"name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label,
|
||||||
"result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing,
|
"result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing,
|
||||||
}
|
}
|
||||||
// Taille réelle du résultat (l'aperçu n'en montre qu'un début) : le
|
|
||||||
// compteur ~N tok de la bulle affiche la vraie valeur, pas celle de
|
|
||||||
// l'aperçu.
|
|
||||||
if ev.ToolUsed.ResultChars > 0 {
|
|
||||||
tu["result_chars"] = ev.ToolUsed.ResultChars
|
|
||||||
}
|
|
||||||
// Référence pour charger le reste à la demande (bouton « voir plus »).
|
|
||||||
if ev.ToolUsed.ResultID != "" {
|
|
||||||
tu["result_id"] = ev.ToolUsed.ResultID
|
|
||||||
}
|
|
||||||
// Corps en cours de frappe : transitoire (l'état final est le diff), on
|
// Corps en cours de frappe : transitoire (l'état final est le diff), on
|
||||||
// ne l'ajoute que quand il est là pour ne pas gonfler chaque événement.
|
// ne l'ajoute que quand il est là pour ne pas gonfler chaque événement.
|
||||||
if ev.ToolUsed.Body != "" {
|
if ev.ToolUsed.Body != "" {
|
||||||
|
|||||||
@@ -97,8 +97,7 @@ func webGrepTool() Tool {
|
|||||||
// ─── exécution des outils (appelée par le dispatch de llm_client.go) ───────────────
|
// ─── exécution des outils (appelée par le dispatch de llm_client.go) ───────────────
|
||||||
|
|
||||||
// webMaxOutput borne ce qu'UN appel d'outil web injecte dans le contexte, comme
|
// webMaxOutput borne ce qu'UN appel d'outil web injecte dans le contexte, comme
|
||||||
// toolMaxOutput (8000) pour le shell. (La mémoire et MCP, eux, renvoient tout
|
// toolMaxOutput (8000) pour le shell et mcpMaxOutput (12000) pour MCP. Sans ce
|
||||||
// au modèle et laissent l'UI charger l'aperçu.) Sans ce
|
|
||||||
// plafond, un `web_read(limit=500)` sur une page dense pouvait pousser 25 000
|
// plafond, un `web_read(limit=500)` sur une page dense pouvait pousser 25 000
|
||||||
// caractères d'un coup : la fenêtre partait en fumée en pleine recherche, ce qui
|
// caractères d'un coup : la fenêtre partait en fumée en pleine recherche, ce qui
|
||||||
// déclenchait des compactages en cascade au milieu du raisonnement.
|
// déclenchait des compactages en cascade au milieu du raisonnement.
|
||||||
|
|||||||
+15
-41
@@ -547,50 +547,24 @@ type ToolUsedEvent struct {
|
|||||||
// pensait à recopier la ligne markdown rendue par l'outil : un petit modèle
|
// pensait à recopier la ligne markdown rendue par l'outil : un petit modèle
|
||||||
// l'oublie, et l'utilisateur ne voyait jamais l'image qu'il avait demandée.
|
// l'oublie, et l'utilisateur ne voyait jamais l'image qu'il avait demandée.
|
||||||
Image string
|
Image string
|
||||||
// ResultChars : taille RÉELLE du résultat (en runes), même quand Result n'en
|
|
||||||
// porte qu'un APERÇU. Sert au compteur « ~N tok » de la bulle, qui affichait
|
|
||||||
// sinon la taille de l'aperçu — toujours le même chiffre, quelle que soit la
|
|
||||||
// page lue.
|
|
||||||
ResultChars int
|
|
||||||
// ResultID : identifiant (tool_call_id) avec lequel l'UI CHARGE le résultat
|
|
||||||
// complet à la demande via /api/chat/tool-result. Le flux ne transporte que
|
|
||||||
// l'aperçu ; le reste ne descend qu'au clic sur « voir plus ». Vide = Result
|
|
||||||
// est déjà complet (rien à charger).
|
|
||||||
ResultID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolPreviewChars borne l'aperçu de résultat d'outil envoyé à l'UI. Le résultat
|
// shownDisplayMax borne ce qu'un résultat d'outil occupe dans le FLUX vers l'UI.
|
||||||
// COMPLET reste côté serveur (conv.Messages) et se charge à la demande.
|
// Aligné sur le plus haut plafond côté modèle (mcpMaxOutput = 12000 ; shell et
|
||||||
|
// web = 8000) : le modèle et l'UI voient donc la même chose, et l'étiquette
|
||||||
|
// « ~N tok » de la bulle dit la VRAIE taille du résultat.
|
||||||
//
|
//
|
||||||
// Avant, l'UI recevait tout le résultat tronqué à 12000 caractères : chaque tour
|
// Avant, cette borne était à 4000 : toute page web un peu longue s'affichait
|
||||||
// poussait des dizaines de kilo-octets dans le flux SSE et dans le journal
|
// « ~1004 tok » — la valeur du plafond, pas celle de la page. Le compteur
|
||||||
// persisté, pour un bloc que personne ne déplie une fois sur dix.
|
// mentait, et il mentait toujours avec le même chiffre.
|
||||||
const toolPreviewChars = 1600
|
const shownDisplayMax = 12000
|
||||||
|
|
||||||
// toolResultPreview renvoie (aperçu, taille réelle en runes, coupé ?).
|
// shownResult prépare un résultat d'outil pour l'affichage.
|
||||||
func toolResultPreview(s string) (string, int, bool) {
|
func shownResult(s string) string {
|
||||||
r := []rune(s)
|
if r := []rune(s); len(r) > shownDisplayMax {
|
||||||
if len(r) <= toolPreviewChars {
|
return string(r[:shownDisplayMax]) + "\n…[tronqué]"
|
||||||
return s, len(r), false
|
|
||||||
}
|
}
|
||||||
return string(r[:toolPreviewChars]), len(r), true
|
return s
|
||||||
}
|
|
||||||
|
|
||||||
// fillToolResult pose sur l'événement l'aperçu envoyé à l'UI, la taille réelle
|
|
||||||
// (pour le compteur ~N tok) et, si le résultat est coupé et qu'on a un id, la
|
|
||||||
// référence pour charger le reste à la demande. Sans id utilisable, on envoie le
|
|
||||||
// résultat entier : mieux vaut un flux plus lourd qu'un bloc qu'on ne peut plus
|
|
||||||
// déplier.
|
|
||||||
func fillToolResult(ev *ToolUsedEvent, result, tcID string) *ToolUsedEvent {
|
|
||||||
prev, n, cut := toolResultPreview(result)
|
|
||||||
ev.ResultChars = n
|
|
||||||
if cut && tcID != "" {
|
|
||||||
ev.Result = prev
|
|
||||||
ev.ResultID = tcID
|
|
||||||
} else {
|
|
||||||
ev.Result = result
|
|
||||||
}
|
|
||||||
return ev
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dedupableTool indique si un appel RIGOUREUSEMENT identique (même outil, mêmes
|
// dedupableTool indique si un appel RIGOUREUSEMENT identique (même outil, mêmes
|
||||||
@@ -1422,7 +1396,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
|||||||
if prev, seen := doneCalls[callKey]; seen && dedupableTool(tc.Function.Name) {
|
if prev, seen := doneCalls[callKey]; seen && dedupableTool(tc.Function.Name) {
|
||||||
repeatCount[callKey]++
|
repeatCount[callKey]++
|
||||||
result = repeatedCallResult(prev, repeatCount[callKey])
|
result = repeatedCallResult(prev, repeatCount[callKey])
|
||||||
cb(StreamEvent{ToolUsed: fillToolResult(&ToolUsedEvent{Name: tc.Function.Name, Label: label, Done: true}, result, tc.ID)})
|
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shownResult(result), Done: true}})
|
||||||
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
||||||
messages = append(messages, toolMsg)
|
messages = append(messages, toolMsg)
|
||||||
extra = append(extra, toolMsg)
|
extra = append(extra, toolMsg)
|
||||||
@@ -1616,7 +1590,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
|||||||
if tc.Function.Name == "web_screenshot" {
|
if tc.Function.Name == "web_screenshot" {
|
||||||
shot = capturedRelPath(result)
|
shot = capturedRelPath(result)
|
||||||
}
|
}
|
||||||
cb(StreamEvent{ToolUsed: fillToolResult(&ToolUsedEvent{Name: tc.Function.Name, Label: label, Done: true, Diff: diff, Added: diffAdd, Removed: diffDel, Image: shot}, result, tc.ID)})
|
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shownResult(result), Done: true, Diff: diff, Added: diffAdd, Removed: diffDel, Image: shot}})
|
||||||
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
||||||
messages = append(messages, toolMsg)
|
messages = append(messages, toolMsg)
|
||||||
extra = append(extra, toolMsg)
|
extra = append(extra, toolMsg)
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const (
|
|||||||
mcpConnectTimeoutCold = 3 * time.Minute
|
mcpConnectTimeoutCold = 3 * time.Minute
|
||||||
// mcpCallTimeout borne un appel d'outil MCP.
|
// mcpCallTimeout borne un appel d'outil MCP.
|
||||||
mcpCallTimeout = 120 * time.Second
|
mcpCallTimeout = 120 * time.Second
|
||||||
|
// mcpMaxOutput cap la sortie renvoyée au modèle (cohérent avec toolMaxOutput).
|
||||||
|
mcpMaxOutput = 12000
|
||||||
)
|
)
|
||||||
|
|
||||||
// mcpSession est une connexion vivante à un serveur MCP.
|
// mcpSession est une connexion vivante à un serveur MCP.
|
||||||
@@ -396,12 +398,10 @@ func mcpCallOnce(ref mcpToolRef, args map[string]any) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
// Le résultat complet part au modèle (même règle que la lecture mémoire) :
|
|
||||||
// plus de troncature ici. Un serveur MCP est configuré EXPRÈS par le
|
|
||||||
// propriétaire de la machine pour ce qu'il rend ; le couper au milieu, c'est
|
|
||||||
// rendre la réponse inutilisable. L'UI, elle, n'affiche qu'un aperçu et
|
|
||||||
// charge le reste à la demande (voir fillToolResult et /api/chat/tool-result).
|
|
||||||
out := flattenMCPContent(res)
|
out := flattenMCPContent(res)
|
||||||
|
if r := []rune(out); len(r) > mcpMaxOutput {
|
||||||
|
out = string(r[:mcpMaxOutput]) + "\n…[tronqué]"
|
||||||
|
}
|
||||||
if res.IsError {
|
if res.IsError {
|
||||||
return "[l'outil a renvoyé une erreur]\n" + out, nil
|
return "[l'outil a renvoyé une erreur]\n" + out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,20 +479,6 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
.diff-count .a{color:var(--add)}
|
.diff-count .a{color:var(--add)}
|
||||||
.diff-count .d{color:var(--del)}
|
.diff-count .d{color:var(--del)}
|
||||||
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
||||||
/* Résultat d'outil tronqué : « voir plus » et « copier » réunis dans une barre
|
|
||||||
ancrée en bas à droite, au lieu d'une ligne séparée sous le bloc. Le pre
|
|
||||||
scrolle (max-height), donc la barre est portée par le bloc-parent NON
|
|
||||||
scrollant (.tool-block) : elle reste au coin quoi qu'on scrolle. */
|
|
||||||
.msg.tool .tool-block{position:relative}
|
|
||||||
.msg.tool .tool-block .tool-actions{position:absolute;bottom:7px;right:7px;display:flex;gap:6px;align-items:center;z-index:1}
|
|
||||||
.msg.tool .tool-actions .copybtn{position:static;bottom:auto;right:auto}
|
|
||||||
.msg.tool .tool-more{padding:3px 8px;font-size:11px;background:var(--panel);border:1px solid var(--border);color:var(--dim);border-radius:3px;cursor:pointer;opacity:.5;transition:opacity .15s,color .15s,border-color .15s}
|
|
||||||
.msg.tool .tool-block:hover .tool-actions .tool-more,.msg.tool .tool-block:hover .tool-actions .copybtn{opacity:1}
|
|
||||||
.msg.tool .tool-more:hover{border-color:var(--accent);color:var(--accent)}
|
|
||||||
.msg.tool .tool-more:disabled{cursor:default}
|
|
||||||
/* Déplié : on lève le plafond de hauteur pour voir VRAIMENT plus, au lieu de
|
|
||||||
rester enfermé dans une fenêtre de 280 px qu'il faut scroller. */
|
|
||||||
.msg.tool pre.expanded{max-height:none}
|
|
||||||
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:3px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:3px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
||||||
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
||||||
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
|
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
|
||||||
@@ -6681,10 +6667,7 @@ function renderToolMsg(el, tu){
|
|||||||
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
||||||
let lbl = meta.lbl;
|
let lbl = meta.lbl;
|
||||||
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
||||||
// Taille RÉELLE (result_chars) quand le flux n'a envoyé qu'un aperçu — sinon
|
if(tu.result){ lbl += ' · ~' + Math.max(1, Math.round(tu.result.length/4)) + ' tok'; }
|
||||||
// la bulle affichait toujours le même chiffre, celui du plafond d'aperçu.
|
|
||||||
const realChars = tu.result_chars || (tu.result ? tu.result.length : 0);
|
|
||||||
if(realChars){ lbl += ' · ~' + Math.max(1, Math.round(realChars/4)) + ' tok'; }
|
|
||||||
setLabel(el, lbl);
|
setLabel(el, lbl);
|
||||||
// Volume de l'écriture (final si le diff est là, provisoire pendant la frappe)
|
// Volume de l'écriture (final si le diff est là, provisoire pendant la frappe)
|
||||||
// reporté sur l'étiquette, pour rester lisible bulle repliée.
|
// reporté sur l'étiquette, pour rester lisible bulle repliée.
|
||||||
@@ -6767,49 +6750,8 @@ function renderToolMsg(el, tu){
|
|||||||
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
||||||
body.appendChild(sub);
|
body.appendChild(sub);
|
||||||
const pre=document.createElement('pre');
|
const pre=document.createElement('pre');
|
||||||
const code=document.createElement('code');
|
const code=document.createElement('code'); code.textContent=tu.result;
|
||||||
const preview=tu.result, rid=tu.result_id, PREVIEW=1600;
|
pre.appendChild(code); body.appendChild(pre);
|
||||||
// Deux cas :
|
|
||||||
// - result_id présent : le flux n'a envoyé qu'un aperçu (résultat coupé),
|
|
||||||
// « voir plus » CHARGE le reste à la demande via l'API ;
|
|
||||||
// - sinon : le résultat est déjà complet ; s'il est long, « voir plus » le
|
|
||||||
// déplie localement (rien à charger).
|
|
||||||
if(rid){
|
|
||||||
code.textContent=preview+'…';
|
|
||||||
pre.appendChild(code);
|
|
||||||
const more=document.createElement('button');
|
|
||||||
more.className='tool-more'; more.type='button'; more.textContent='voir plus';
|
|
||||||
let full=null, open=false;
|
|
||||||
more.onclick=async(e)=>{ e.stopPropagation();
|
|
||||||
if(full===null){
|
|
||||||
more.disabled=true; more.textContent='…';
|
|
||||||
// Résultat introuvable côté serveur (compacté, autre discussion) : on
|
|
||||||
// garde l'aperçu plutôt que de vider la bulle.
|
|
||||||
try{ const j=await jget('/api/chat/tool-result?id='+encodeURIComponent(rid)); full=(j&&j.result)||preview; }
|
|
||||||
catch(_){ full=preview; }
|
|
||||||
more.disabled=false;
|
|
||||||
}
|
|
||||||
open=!open;
|
|
||||||
code.textContent = open ? full : preview+'…';
|
|
||||||
pre.classList.toggle('expanded', open);
|
|
||||||
more.textContent = open ? 'voir moins' : 'voir plus';
|
|
||||||
};
|
|
||||||
body.appendChild(toolBlock(pre, more));
|
|
||||||
} else if(preview.length>PREVIEW){
|
|
||||||
code.textContent=preview.slice(0,PREVIEW)+'…';
|
|
||||||
pre.appendChild(code);
|
|
||||||
const more=document.createElement('button');
|
|
||||||
more.className='tool-more'; more.type='button'; more.textContent='voir plus';
|
|
||||||
let open=false;
|
|
||||||
more.onclick=(e)=>{ e.stopPropagation(); open=!open;
|
|
||||||
code.textContent = open ? preview : preview.slice(0,PREVIEW)+'…';
|
|
||||||
pre.classList.toggle('expanded', open);
|
|
||||||
more.textContent = open ? 'voir moins' : 'voir plus';
|
|
||||||
};
|
|
||||||
body.appendChild(toolBlock(pre, more));
|
|
||||||
} else {
|
|
||||||
code.textContent=preview; pre.appendChild(code); body.appendChild(pre);
|
|
||||||
}
|
|
||||||
} else if(!tu.done && !tu.typing){
|
} else if(!tu.done && !tu.typing){
|
||||||
const wait=document.createElement('div'); wait.className='tool-wait'; wait.textContent='exécution en cours…';
|
const wait=document.createElement('div'); wait.className='tool-wait'; wait.textContent='exécution en cours…';
|
||||||
body.appendChild(wait);
|
body.appendChild(wait);
|
||||||
@@ -6821,26 +6763,10 @@ function renderToolMsg(el, tu){
|
|||||||
const bw = el.querySelector('.bodywrap');
|
const bw = el.querySelector('.bodywrap');
|
||||||
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
|
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
|
||||||
}
|
}
|
||||||
// Bloc d'un résultat d'outil : le pre scrolle (max-height), donc la barre
|
|
||||||
// d'actions vit sur un bloc-parent NON scrollant, ancrée en bas à droite — elle
|
|
||||||
// reste au coin quoi qu'on scrolle dans le résultat. La barre réunit « voir
|
|
||||||
// plus » (passé ici) et, ajouté ensuite par addCopyButtons, « copier ».
|
|
||||||
function toolBlock(pre, more){
|
|
||||||
const block=document.createElement('div'); block.className='tool-block';
|
|
||||||
block.appendChild(pre);
|
|
||||||
const bar=document.createElement('div'); bar.className='tool-actions';
|
|
||||||
bar.appendChild(more);
|
|
||||||
block.appendChild(bar);
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
// Inject a "copier" button into every <pre> code block (idempotent).
|
// Inject a "copier" button into every <pre> code block (idempotent).
|
||||||
function addCopyButtons(root){
|
function addCopyButtons(root){
|
||||||
root.querySelectorAll('pre').forEach(pre=>{
|
root.querySelectorAll('pre').forEach(pre=>{
|
||||||
// Résultat d'outil : le pre est enrobé dans .tool-block et la barre
|
if(pre.querySelector('.copybtn')) return;
|
||||||
// d'actions est SŒUR du pre (pas dedans) ; on cherche donc le copier dans
|
|
||||||
// ce périmètre.
|
|
||||||
const scope = pre.closest('.tool-block') || pre;
|
|
||||||
if(scope.querySelector('.copybtn')) return;
|
|
||||||
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
||||||
if(pre.classList.contains('diff')) return;
|
if(pre.classList.contains('diff')) return;
|
||||||
const btn=document.createElement('button');
|
const btn=document.createElement('button');
|
||||||
@@ -6853,9 +6779,7 @@ function addCopyButtons(root){
|
|||||||
btn.textContent='copié ✓'; btn.classList.add('done');
|
btn.textContent='copié ✓'; btn.classList.add('done');
|
||||||
setTimeout(()=>{ btn.textContent='copier'; btn.classList.remove('done'); },1500);
|
setTimeout(()=>{ btn.textContent='copier'; btn.classList.remove('done'); },1500);
|
||||||
};
|
};
|
||||||
// S'il y a une barre d'actions (résultat d'outil avec « voir plus »), le
|
pre.appendChild(btn);
|
||||||
// bouton copier s'y range à côté ; sinon il se colle en bas à droite du bloc.
|
|
||||||
(scope.querySelector('.tool-actions')||pre).appendChild(btn);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Vider la discussion POUR TOUS LES APPAREILS : le serveur vide le fil et
|
// Vider la discussion POUR TOUS LES APPAREILS : le serveur vide le fil et
|
||||||
|
|||||||
@@ -306,10 +306,7 @@ function renderToolMsg(el, tu){
|
|||||||
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
||||||
let lbl = meta.lbl;
|
let lbl = meta.lbl;
|
||||||
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
||||||
// Taille RÉELLE (result_chars) quand le flux n'a envoyé qu'un aperçu — sinon
|
if(tu.result){ lbl += ' · ~' + Math.max(1, Math.round(tu.result.length/4)) + ' tok'; }
|
||||||
// la bulle affichait toujours le même chiffre, celui du plafond d'aperçu.
|
|
||||||
const realChars = tu.result_chars || (tu.result ? tu.result.length : 0);
|
|
||||||
if(realChars){ lbl += ' · ~' + Math.max(1, Math.round(realChars/4)) + ' tok'; }
|
|
||||||
setLabel(el, lbl);
|
setLabel(el, lbl);
|
||||||
// Volume de l'écriture (final si le diff est là, provisoire pendant la frappe)
|
// Volume de l'écriture (final si le diff est là, provisoire pendant la frappe)
|
||||||
// reporté sur l'étiquette, pour rester lisible bulle repliée.
|
// reporté sur l'étiquette, pour rester lisible bulle repliée.
|
||||||
@@ -392,49 +389,8 @@ function renderToolMsg(el, tu){
|
|||||||
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
||||||
body.appendChild(sub);
|
body.appendChild(sub);
|
||||||
const pre=document.createElement('pre');
|
const pre=document.createElement('pre');
|
||||||
const code=document.createElement('code');
|
const code=document.createElement('code'); code.textContent=tu.result;
|
||||||
const preview=tu.result, rid=tu.result_id, PREVIEW=1600;
|
pre.appendChild(code); body.appendChild(pre);
|
||||||
// Deux cas :
|
|
||||||
// - result_id présent : le flux n'a envoyé qu'un aperçu (résultat coupé),
|
|
||||||
// « voir plus » CHARGE le reste à la demande via l'API ;
|
|
||||||
// - sinon : le résultat est déjà complet ; s'il est long, « voir plus » le
|
|
||||||
// déplie localement (rien à charger).
|
|
||||||
if(rid){
|
|
||||||
code.textContent=preview+'…';
|
|
||||||
pre.appendChild(code);
|
|
||||||
const more=document.createElement('button');
|
|
||||||
more.className='tool-more'; more.type='button'; more.textContent='voir plus';
|
|
||||||
let full=null, open=false;
|
|
||||||
more.onclick=async(e)=>{ e.stopPropagation();
|
|
||||||
if(full===null){
|
|
||||||
more.disabled=true; more.textContent='…';
|
|
||||||
// Résultat introuvable côté serveur (compacté, autre discussion) : on
|
|
||||||
// garde l'aperçu plutôt que de vider la bulle.
|
|
||||||
try{ const j=await jget('/api/chat/tool-result?id='+encodeURIComponent(rid)); full=(j&&j.result)||preview; }
|
|
||||||
catch(_){ full=preview; }
|
|
||||||
more.disabled=false;
|
|
||||||
}
|
|
||||||
open=!open;
|
|
||||||
code.textContent = open ? full : preview+'…';
|
|
||||||
pre.classList.toggle('expanded', open);
|
|
||||||
more.textContent = open ? 'voir moins' : 'voir plus';
|
|
||||||
};
|
|
||||||
body.appendChild(toolBlock(pre, more));
|
|
||||||
} else if(preview.length>PREVIEW){
|
|
||||||
code.textContent=preview.slice(0,PREVIEW)+'…';
|
|
||||||
pre.appendChild(code);
|
|
||||||
const more=document.createElement('button');
|
|
||||||
more.className='tool-more'; more.type='button'; more.textContent='voir plus';
|
|
||||||
let open=false;
|
|
||||||
more.onclick=(e)=>{ e.stopPropagation(); open=!open;
|
|
||||||
code.textContent = open ? preview : preview.slice(0,PREVIEW)+'…';
|
|
||||||
pre.classList.toggle('expanded', open);
|
|
||||||
more.textContent = open ? 'voir moins' : 'voir plus';
|
|
||||||
};
|
|
||||||
body.appendChild(toolBlock(pre, more));
|
|
||||||
} else {
|
|
||||||
code.textContent=preview; pre.appendChild(code); body.appendChild(pre);
|
|
||||||
}
|
|
||||||
} else if(!tu.done && !tu.typing){
|
} else if(!tu.done && !tu.typing){
|
||||||
const wait=document.createElement('div'); wait.className='tool-wait'; wait.textContent='exécution en cours…';
|
const wait=document.createElement('div'); wait.className='tool-wait'; wait.textContent='exécution en cours…';
|
||||||
body.appendChild(wait);
|
body.appendChild(wait);
|
||||||
@@ -446,26 +402,10 @@ function renderToolMsg(el, tu){
|
|||||||
const bw = el.querySelector('.bodywrap');
|
const bw = el.querySelector('.bodywrap');
|
||||||
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
|
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
|
||||||
}
|
}
|
||||||
// Bloc d'un résultat d'outil : le pre scrolle (max-height), donc la barre
|
|
||||||
// d'actions vit sur un bloc-parent NON scrollant, ancrée en bas à droite — elle
|
|
||||||
// reste au coin quoi qu'on scrolle dans le résultat. La barre réunit « voir
|
|
||||||
// plus » (passé ici) et, ajouté ensuite par addCopyButtons, « copier ».
|
|
||||||
function toolBlock(pre, more){
|
|
||||||
const block=document.createElement('div'); block.className='tool-block';
|
|
||||||
block.appendChild(pre);
|
|
||||||
const bar=document.createElement('div'); bar.className='tool-actions';
|
|
||||||
bar.appendChild(more);
|
|
||||||
block.appendChild(bar);
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
// Inject a "copier" button into every <pre> code block (idempotent).
|
// Inject a "copier" button into every <pre> code block (idempotent).
|
||||||
function addCopyButtons(root){
|
function addCopyButtons(root){
|
||||||
root.querySelectorAll('pre').forEach(pre=>{
|
root.querySelectorAll('pre').forEach(pre=>{
|
||||||
// Résultat d'outil : le pre est enrobé dans .tool-block et la barre
|
if(pre.querySelector('.copybtn')) return;
|
||||||
// d'actions est SŒUR du pre (pas dedans) ; on cherche donc le copier dans
|
|
||||||
// ce périmètre.
|
|
||||||
const scope = pre.closest('.tool-block') || pre;
|
|
||||||
if(scope.querySelector('.copybtn')) return;
|
|
||||||
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
||||||
if(pre.classList.contains('diff')) return;
|
if(pre.classList.contains('diff')) return;
|
||||||
const btn=document.createElement('button');
|
const btn=document.createElement('button');
|
||||||
@@ -478,9 +418,7 @@ function addCopyButtons(root){
|
|||||||
btn.textContent='copié ✓'; btn.classList.add('done');
|
btn.textContent='copié ✓'; btn.classList.add('done');
|
||||||
setTimeout(()=>{ btn.textContent='copier'; btn.classList.remove('done'); },1500);
|
setTimeout(()=>{ btn.textContent='copier'; btn.classList.remove('done'); },1500);
|
||||||
};
|
};
|
||||||
// S'il y a une barre d'actions (résultat d'outil avec « voir plus »), le
|
pre.appendChild(btn);
|
||||||
// bouton copier s'y range à côté ; sinon il se colle en bas à droite du bloc.
|
|
||||||
(scope.querySelector('.tool-actions')||pre).appendChild(btn);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Vider la discussion POUR TOUS LES APPAREILS : le serveur vide le fil et
|
// Vider la discussion POUR TOUS LES APPAREILS : le serveur vide le fil et
|
||||||
|
|||||||
@@ -444,20 +444,6 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
|||||||
.diff-count .a{color:var(--add)}
|
.diff-count .a{color:var(--add)}
|
||||||
.diff-count .d{color:var(--del)}
|
.diff-count .d{color:var(--del)}
|
||||||
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
||||||
/* Résultat d'outil tronqué : « voir plus » et « copier » réunis dans une barre
|
|
||||||
ancrée en bas à droite, au lieu d'une ligne séparée sous le bloc. Le pre
|
|
||||||
scrolle (max-height), donc la barre est portée par le bloc-parent NON
|
|
||||||
scrollant (.tool-block) : elle reste au coin quoi qu'on scrolle. */
|
|
||||||
.msg.tool .tool-block{position:relative}
|
|
||||||
.msg.tool .tool-block .tool-actions{position:absolute;bottom:7px;right:7px;display:flex;gap:6px;align-items:center;z-index:1}
|
|
||||||
.msg.tool .tool-actions .copybtn{position:static;bottom:auto;right:auto}
|
|
||||||
.msg.tool .tool-more{padding:3px 8px;font-size:11px;background:var(--panel);border:1px solid var(--border);color:var(--dim);border-radius:3px;cursor:pointer;opacity:.5;transition:opacity .15s,color .15s,border-color .15s}
|
|
||||||
.msg.tool .tool-block:hover .tool-actions .tool-more,.msg.tool .tool-block:hover .tool-actions .copybtn{opacity:1}
|
|
||||||
.msg.tool .tool-more:hover{border-color:var(--accent);color:var(--accent)}
|
|
||||||
.msg.tool .tool-more:disabled{cursor:default}
|
|
||||||
/* Déplié : on lève le plafond de hauteur pour voir VRAIMENT plus, au lieu de
|
|
||||||
rester enfermé dans une fenêtre de 280 px qu'il faut scroller. */
|
|
||||||
.msg.tool pre.expanded{max-height:none}
|
|
||||||
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:3px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:3px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
||||||
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
||||||
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
|
.msg.tool .tool-caret{color:var(--accent);animation:toolblink 1s step-end infinite}
|
||||||
|
|||||||
@@ -206,35 +206,6 @@ func handleChatState(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 200, conv.state())
|
sendJSON(w, 200, conv.state())
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleToolResult renvoie le résultat COMPLET d'un appel d'outil, chargé à la
|
|
||||||
// demande par le bouton « voir plus » de la bulle (le flux ne transporte qu'un
|
|
||||||
// aperçu). Le résultat complet vit dans la vue « modèle » (conv.Messages), repéré
|
|
||||||
// par son tool_call_id. Absent (compacté, archivé, discussion changée) → 404, et
|
|
||||||
// l'UI garde son aperçu plutôt que d'afficher une erreur.
|
|
||||||
func handleToolResult(w http.ResponseWriter, r *http.Request) {
|
|
||||||
id := strings.TrimSpace(r.URL.Query().Get("id"))
|
|
||||||
if id == "" {
|
|
||||||
sendJSON(w, 400, map[string]any{"error": "id manquant"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
conv.mu.Lock()
|
|
||||||
found, ok := "", false
|
|
||||||
for _, m := range conv.Messages {
|
|
||||||
if m.Role == "tool" && m.ToolCallID == id {
|
|
||||||
if s, isStr := m.Content.(string); isStr {
|
|
||||||
found, ok = s, true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conv.mu.Unlock()
|
|
||||||
if !ok {
|
|
||||||
sendJSON(w, 404, map[string]any{"error": "résultat non disponible"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sendJSON(w, 200, map[string]any{"result": found})
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleChat(w http.ResponseWriter, r *http.Request) {
|
func handleChat(w http.ResponseWriter, r *http.Request) {
|
||||||
var body chatReq
|
var body chatReq
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
|||||||
@@ -339,13 +339,12 @@ func newWebMux() *http.ServeMux {
|
|||||||
api("/api/conversations/switch", handleConvSwitch)
|
api("/api/conversations/switch", handleConvSwitch)
|
||||||
api("/api/conversations/rename", handleConvRename)
|
api("/api/conversations/rename", handleConvRename)
|
||||||
api("/api/conversations/delete", handleConvDelete)
|
api("/api/conversations/delete", handleConvDelete)
|
||||||
api("/api/chat/mode", handleChatMode) // GET mode+critères / POST bascule Chat|Code
|
api("/api/chat/mode", handleChatMode) // GET mode+critères / POST bascule Chat|Code
|
||||||
api("/api/chat/criteria", handleChatCriteria) // édition manuelle des critères (mode code)
|
api("/api/chat/criteria", handleChatCriteria) // édition manuelle des critères (mode code)
|
||||||
api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte
|
api("/api/chat/compact", handleChatCompact) // compaction manuelle du contexte
|
||||||
api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used}
|
api("/api/chat/state", handleChatState) // instantané léger {seq, generating, ctx_used}
|
||||||
api("/api/chat/tool-result", handleToolResult) // résultat complet d'un outil, chargé à la demande (bouton « voir plus »)
|
api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json)
|
||||||
api("/api/chat/export", handleChatExport) // téléchargement du fil (?format=md|json)
|
api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais)
|
||||||
api("/api/e2e/chat", handleE2EChat) // même flux mais chiffré E2E (boîte noire via le relais)
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user