Cartes raisonnement qui suivent l'écriture ; micro aligné ; moyenne tok/s

- Le défilement interne des cartes bornées ne suivait PAS la génération :
  le rendu en attente porte la bulle entière (.msg), pas .body — le test
  « parent = bodywrap » ne passait jamais et l'épinglage bas était mort.
  On cherche maintenant le bodywrap DANS la bulle : la carte suit le texte
  au fur et à mesure, et une remontée manuelle est respectée. Les cartes
  d'outil suivent aussi la ligne en cours, puis remontent en tête une fois
  l'appel terminé.

- Bouton micro : il gardait la bordure des boutons génériques, décalé de
  ses voisins. Ajouté aux règles communes des boutons de saisie (34 px,
  sans bordure, fond au survol) avec #attach et #files-btn.

- La carte du temps total (au-dessus de la saisie) affiche la vitesse
  moyenne de la conversation : « en cours — 12 s · moy 21.3 tok/s ».
  Alimentée par les événements stats journalisés (gen_tokens/gen_ms,
  cumulatifs par complétion — seul le delta est ajouté), donc rejouée au
  chargement : la moyenne survit au refresh. Remise à zéro au reset.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-08-20 22:24:07 +02:00
1 parent 86d711e772
commit 481028b023
4 files changed
+72 -20

No files matched your search

+36 -10
View File
@@ -1774,7 +1774,7 @@ button:active:not(:disabled){transform:translateY(1px)}
⚠️ Une seule règle pour les quatre. Il y en avait trois générations empilées
(pilule large « Envoyer », aplat noir, aplat sauge) : chaque état tirait sa
taille, sa forme et son alignement d'une couche différente. */
#inputbar #attach,#inputbar #files-btn,#send,#stop{
#inputbar #attach,#inputbar #files-btn,#inputbar #mic,#send,#stop{
width:34px;height:34px;min-height:34px;flex:none;margin:0;padding:0;
display:inline-flex;align-items:center;justify-content:center;align-self:center;
border:0;border-radius:4px;background:transparent;color:var(--dim);
@@ -1785,8 +1785,8 @@ button:active:not(:disabled){transform:translateY(1px)}
#stop{display:none}
html[data-busy="1"] #send{display:none}
html[data-busy="1"] #stop{display:inline-flex}
#inputbar #attach svg,#inputbar #files-btn svg,#send svg,#stop svg{display:block;width:18px;height:18px}
#inputbar #attach:hover,#inputbar #files-btn:hover{background:var(--row-bg-on);color:var(--text)}
#inputbar #attach svg,#inputbar #files-btn svg,#inputbar #mic svg,#send svg,#stop svg{display:block;width:18px;height:18px}
#inputbar #attach:hover,#inputbar #files-btn:hover,#inputbar #mic:hover{background:var(--row-bg-on);color:var(--text)}
html[data-files="1"] #files-btn{color:var(--accent)}
#send{color:var(--accent)}
#send:hover:not(:disabled){background:var(--accent-bg);color:var(--accent);opacity:1;transform:none}
@@ -2060,8 +2060,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
/* Dictée : anneau rouge pulsé pendant l'enregistrement, estompé pendant la
transcription. Le bouton garde le gabarit de ses voisins (joindre, fichiers). */
#mic.rec{color:var(--err);border-color:var(--err);animation:micpulse 1.2s ease-in-out infinite}
transcription. Le gabarit (34 px, fond au survol) vient de la règle commune
des boutons de saisie — #mic y est listé avec #attach et #files-btn. */
#mic.rec{color:var(--err);animation:micpulse 1.2s ease-in-out infinite}
@keyframes micpulse{50%{box-shadow:0 0 0 5px color-mix(in srgb,var(--err) 25%,transparent)}}
#mic.busy{opacity:.55}
@@ -5988,6 +5989,11 @@ function renderToolMsg(el, tu){
body.appendChild(wait);
}
addCopyButtons(body); scrollMaybe();
// Carte à hauteur bornée : tant que l'outil TRAVAILLE, on suit le bas (la
// ligne en cours d'écriture, la commande qui se tape). Terminé, on remonte en
// tête — c'est le résumé qu'on relit, pas la dernière ligne.
const bw = el.querySelector('.bodywrap');
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
}
// Inject a "copier" button into every <pre> code block (idempotent).
function addCopyButtons(root){
@@ -6209,9 +6215,28 @@ function paintTurnClock(){
}
const ms = (T.doneTs || nowServer()) - T.startTs;
if(!(ms > 0)){ el.remove(); return; }
el.textContent = (T.doneTs ? 'tour terminé en ' : 'en cours — ') + fmtDur(ms);
// Vitesse moyenne de la CONVERSATION à côté du temps du tour : le débit
// typique de ce modèle sur ce fil, toutes réponses confondues.
const avg = (CONV_MS > 1000 && CONV_TOK > 20)
? ' · moy ' + (CONV_TOK / (CONV_MS/1000)).toFixed(1) + ' tok/s' : '';
el.textContent = (T.doneTs ? 'tour terminé en ' : 'en cours — ') + fmtDur(ms) + avg;
el.classList.toggle('done', !!T.doneTs);
}
// --- Vitesse moyenne de la conversation --------------------------------------
// Alimentée par les événements `stats` (journalisés, donc rejoués au
// chargement : la moyenne survit au refresh). gen_tokens / gen_ms sont
// CUMULATIFS au sein d'une complétion et retombent à la suivante : on n'ajoute
// que le delta, et une valeur qui recule signale une nouvelle complétion.
let CONV_TOK = 0, CONV_MS = 0, _cgTok = 0, _cgMs = 0;
function feedConvSpeed(s){
const gt = s.gen_tokens || 0, gms = s.gen_ms || 0;
if(!gt) return;
if(gt < _cgTok){ _cgTok = 0; _cgMs = 0; }
CONV_TOK += gt - _cgTok;
CONV_MS += Math.max(0, gms - _cgMs);
_cgTok = gt; _cgMs = gms;
}
function resetConvSpeed(){ CONV_TOK = 0; CONV_MS = 0; _cgTok = 0; _cgMs = 0; }
// Le compteur se montre AUSSI sur l'indicateur « … » : pendant un appel d'outil
// ou un long prefill il n'y a encore aucune réponse sous laquelle écrire, et
// c'est précisément le moment où l'on se demande si ça avance.
@@ -6434,8 +6459,9 @@ function flushRender(final){
}
// Carte bornée (raisonnement/outil) : si on était collé en bas, on y reste —
// la carte suit la génération. Un défilement manuel vers le haut est respecté.
const bw=p.el.parentElement;
const scrollable=bw && bw.classList.contains('bodywrap');
// ⚠️ p.el est la BULLE (.msg), pas .body : le bodywrap est dedans, pas au-dessus.
const bw=p.el.querySelector ? p.el.querySelector(':scope > .bodywrap') : null;
const scrollable=!!bw;
const stick=scrollable && bw.scrollTop+bw.clientHeight>=bw.scrollHeight-60;
const t0=performance.now();
renderBody(p.el, text);
@@ -6517,7 +6543,7 @@ function handleDelta(d){
// serveur) : on nettoie l'écran et on resynchronise la liste, car la bascule
// a pu être déclenchée depuis un autre appareil. Les fichiers suivent : ils
// appartiennent à la discussion, le panneau doit changer avec elle.
if(d.reset!==undefined){ smoothReset(); cancelRender(); stopWorkTimer(); const tc=document.getElementById('turn-clock'); if(tc) tc.remove(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); if(typeof modeOnConvChange==='function') modeOnConvChange(); return; }
if(d.reset!==undefined){ smoothReset(); cancelRender(); stopWorkTimer(); resetConvSpeed(); const tc=document.getElementById('turn-clock'); if(tc) tc.remove(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); if(typeof modeOnConvChange==='function') modeOnConvChange(); return; }
// --- Mode code (20-mode.js) -----------------------------------------------
if(d.mode!==undefined){ if(typeof applyModeDelta==='function') applyModeDelta(d.mode); return; }
if(d.code_hint){ if(typeof showCodeHint==='function') showCodeHint(); return; }
@@ -6565,7 +6591,7 @@ function handleDelta(d){
// une trace du fil et DOIT être rejouée.
if(d.compact_noop){ setCompacting(false); if(!REPLAYING) toast('rien à compacter (contexte déjà minimal)'); return; }
if(d.ctx_used!==undefined){ setCtxUsed(d.ctx_used); return; }
if(d.stats){ T.serverStats=d.stats;
if(d.stats){ T.serverStats=d.stats; feedConvSpeed(d.stats);
// prompt_tokens_total = comptage exact du moteur (include_usage). Absent sur
// certains llama-server récents : le serveur publie alors un `ctx_used`
// estimé en fin de tour, traité plus bas — on ne remet donc PAS la jauge à
@@ -368,6 +368,11 @@ function renderToolMsg(el, tu){
body.appendChild(wait);
}
addCopyButtons(body); scrollMaybe();
// Carte à hauteur bornée : tant que l'outil TRAVAILLE, on suit le bas (la
// ligne en cours d'écriture, la commande qui se tape). Terminé, on remonte en
// tête — c'est le résumé qu'on relit, pas la dernière ligne.
const bw = el.querySelector('.bodywrap');
if(bw){ bw.scrollTop = tu.done ? 0 : bw.scrollHeight; }
}
// Inject a "copier" button into every <pre> code block (idempotent).
function addCopyButtons(root){
+25 -5
View File
@@ -89,9 +89,28 @@ function paintTurnClock(){
}
const ms = (T.doneTs || nowServer()) - T.startTs;
if(!(ms > 0)){ el.remove(); return; }
el.textContent = (T.doneTs ? 'tour terminé en ' : 'en cours — ') + fmtDur(ms);
// Vitesse moyenne de la CONVERSATION à côté du temps du tour : le débit
// typique de ce modèle sur ce fil, toutes réponses confondues.
const avg = (CONV_MS > 1000 && CONV_TOK > 20)
? ' · moy ' + (CONV_TOK / (CONV_MS/1000)).toFixed(1) + ' tok/s' : '';
el.textContent = (T.doneTs ? 'tour terminé en ' : 'en cours — ') + fmtDur(ms) + avg;
el.classList.toggle('done', !!T.doneTs);
}
// --- Vitesse moyenne de la conversation --------------------------------------
// Alimentée par les événements `stats` (journalisés, donc rejoués au
// chargement : la moyenne survit au refresh). gen_tokens / gen_ms sont
// CUMULATIFS au sein d'une complétion et retombent à la suivante : on n'ajoute
// que le delta, et une valeur qui recule signale une nouvelle complétion.
let CONV_TOK = 0, CONV_MS = 0, _cgTok = 0, _cgMs = 0;
function feedConvSpeed(s){
const gt = s.gen_tokens || 0, gms = s.gen_ms || 0;
if(!gt) return;
if(gt < _cgTok){ _cgTok = 0; _cgMs = 0; }
CONV_TOK += gt - _cgTok;
CONV_MS += Math.max(0, gms - _cgMs);
_cgTok = gt; _cgMs = gms;
}
function resetConvSpeed(){ CONV_TOK = 0; CONV_MS = 0; _cgTok = 0; _cgMs = 0; }
// Le compteur se montre AUSSI sur l'indicateur « … » : pendant un appel d'outil
// ou un long prefill il n'y a encore aucune réponse sous laquelle écrire, et
// c'est précisément le moment où l'on se demande si ça avance.
@@ -314,8 +333,9 @@ function flushRender(final){
}
// Carte bornée (raisonnement/outil) : si on était collé en bas, on y reste —
// la carte suit la génération. Un défilement manuel vers le haut est respecté.
const bw=p.el.parentElement;
const scrollable=bw && bw.classList.contains('bodywrap');
// ⚠️ p.el est la BULLE (.msg), pas .body : le bodywrap est dedans, pas au-dessus.
const bw=p.el.querySelector ? p.el.querySelector(':scope > .bodywrap') : null;
const scrollable=!!bw;
const stick=scrollable && bw.scrollTop+bw.clientHeight>=bw.scrollHeight-60;
const t0=performance.now();
renderBody(p.el, text);
@@ -397,7 +417,7 @@ function handleDelta(d){
// serveur) : on nettoie l'écran et on resynchronise la liste, car la bascule
// a pu être déclenchée depuis un autre appareil. Les fichiers suivent : ils
// appartiennent à la discussion, le panneau doit changer avec elle.
if(d.reset!==undefined){ smoothReset(); cancelRender(); stopWorkTimer(); const tc=document.getElementById('turn-clock'); if(tc) tc.remove(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); if(typeof modeOnConvChange==='function') modeOnConvChange(); return; }
if(d.reset!==undefined){ smoothReset(); cancelRender(); stopWorkTimer(); resetConvSpeed(); const tc=document.getElementById('turn-clock'); if(tc) tc.remove(); PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); if(typeof loadConversations==='function') loadConversations(); if(typeof filesOnConvChange==='function') filesOnConvChange(); if(typeof modeOnConvChange==='function') modeOnConvChange(); return; }
// --- Mode code (20-mode.js) -----------------------------------------------
if(d.mode!==undefined){ if(typeof applyModeDelta==='function') applyModeDelta(d.mode); return; }
if(d.code_hint){ if(typeof showCodeHint==='function') showCodeHint(); return; }
@@ -445,7 +465,7 @@ function handleDelta(d){
// une trace du fil et DOIT être rejouée.
if(d.compact_noop){ setCompacting(false); if(!REPLAYING) toast('rien à compacter (contexte déjà minimal)'); return; }
if(d.ctx_used!==undefined){ setCtxUsed(d.ctx_used); return; }
if(d.stats){ T.serverStats=d.stats;
if(d.stats){ T.serverStats=d.stats; feedConvSpeed(d.stats);
// prompt_tokens_total = comptage exact du moteur (include_usage). Absent sur
// certains llama-server récents : le serveur publie alors un `ctx_used`
// estimé en fin de tour, traité plus bas — on ne remet donc PAS la jauge à
+6 -5
View File
@@ -1743,7 +1743,7 @@ button:active:not(:disabled){transform:translateY(1px)}
⚠️ Une seule règle pour les quatre. Il y en avait trois générations empilées
(pilule large « Envoyer », aplat noir, aplat sauge) : chaque état tirait sa
taille, sa forme et son alignement d'une couche différente. */
#inputbar #attach,#inputbar #files-btn,#send,#stop{
#inputbar #attach,#inputbar #files-btn,#inputbar #mic,#send,#stop{
width:34px;height:34px;min-height:34px;flex:none;margin:0;padding:0;
display:inline-flex;align-items:center;justify-content:center;align-self:center;
border:0;border-radius:4px;background:transparent;color:var(--dim);
@@ -1754,8 +1754,8 @@ button:active:not(:disabled){transform:translateY(1px)}
#stop{display:none}
html[data-busy="1"] #send{display:none}
html[data-busy="1"] #stop{display:inline-flex}
#inputbar #attach svg,#inputbar #files-btn svg,#send svg,#stop svg{display:block;width:18px;height:18px}
#inputbar #attach:hover,#inputbar #files-btn:hover{background:var(--row-bg-on);color:var(--text)}
#inputbar #attach svg,#inputbar #files-btn svg,#inputbar #mic svg,#send svg,#stop svg{display:block;width:18px;height:18px}
#inputbar #attach:hover,#inputbar #files-btn:hover,#inputbar #mic:hover{background:var(--row-bg-on);color:var(--text)}
html[data-files="1"] #files-btn{color:var(--accent)}
#send{color:var(--accent)}
#send:hover:not(:disabled){background:var(--accent-bg);color:var(--accent);opacity:1;transform:none}
@@ -2029,8 +2029,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
/* Dictée : anneau rouge pulsé pendant l'enregistrement, estompé pendant la
transcription. Le bouton garde le gabarit de ses voisins (joindre, fichiers). */
#mic.rec{color:var(--err);border-color:var(--err);animation:micpulse 1.2s ease-in-out infinite}
transcription. Le gabarit (34 px, fond au survol) vient de la règle commune
des boutons de saisie — #mic y est listé avec #attach et #files-btn. */
#mic.rec{color:var(--err);animation:micpulse 1.2s ease-in-out infinite}
@keyframes micpulse{50%{box-shadow:0 0 0 5px color-mix(in srgb,var(--err) 25%,transparent)}}
#mic.busy{opacity:.55}