Composeur : bandeaux au-dessus de la carte, chrono du tour, anneau réparé

Trois défauts d'affichage, une cause commune pour deux d'entre eux.

Le panneau des critères et la puce « mode Code » étaient insérés AVANT
#composer, qui est en position:absolute : ils restaient dans le flux de
la page et passaient donc SOUS la carte de saisie. Ils vivent maintenant
DANS le composeur, alignés sur sa colonne, et les critères s'ouvrent
repliés (cinq lignes dépliées mangeaient la moitié de l'écran).

Chrono du tour complet au-dessus de la carte : le temps entre l'envoi du
message et le moment où l'on peut reparler — appels d'outils, passes de
vérification et compaction compris. Les durées portées par les cartes ne
mesurent que la génération de texte ; celle-ci répond à « ça a pris
combien de temps, en tout ? ». Il avance à la seconde puis se fige sur le
total.

Anneau d'activité de la liste des discussions : l'animation portait sur
le <svg>, dont l'origine de rotation dépend de transform-box — défaut
qui a changé au fil des versions, d'où un anneau figé. Elle porte
désormais sur le conteneur HTML, qui tourne partout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-19 08:16:28 +02:00
1 parent 4e5bc44607
commit 895ff92ed8
4 files changed
+154 -42

No files matched your search

+25 -3
View File
@@ -70,7 +70,28 @@ function nowServer(){ return Date.now() - TS_SKEW; }
let WORK_TIMER = null;
function startWorkTimer(){ if(WORK_TIMER || REPLAYING) return; WORK_TIMER=setInterval(tickWork, 1000); }
function stopWorkTimer(){ if(WORK_TIMER){ clearInterval(WORK_TIMER); WORK_TIMER=null; } }
function tickWork(){ if(T.contentEl) paintStats(T.contentEl); paintTypingClock(); }
function tickWork(){ if(T.contentEl) paintStats(T.contentEl); paintTypingClock(); paintTurnClock(); }
// Chrono du TOUR COMPLET, affiché au-dessus de la carte de saisie : le temps
// entre l'envoi du message et le moment où l'on peut reparler — outils, passes
// de vérification et compaction compris. Les durées portées par les cartes ne
// disent que la génération de texte ; celle-ci répond à « ça a pris combien de
// temps, en tout ? ».
function paintTurnClock(){
const composer = document.getElementById('composer');
if(!composer || !T.startTs) return;
let el = document.getElementById('turn-clock');
if(!el){
el = document.createElement('div');
el.id = 'turn-clock';
el.className = 'composer-banner';
composer.insertBefore(el, composer.firstChild);
}
const ms = (T.doneTs || nowServer()) - T.startTs;
if(!(ms > 0)){ el.remove(); return; }
el.textContent = (T.doneTs ? 'tour terminé en ' : 'en cours — ') + fmtDur(ms);
el.classList.toggle('done', !!T.doneTs);
}
// 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.
@@ -261,7 +282,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){ stopWorkTimer(); 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){ 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; }
// --- 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; }
@@ -280,7 +301,7 @@ function handleDelta(d){
// Départ du chronomètre : l'horodatage SERVEUR du message, pas l'heure
// locale — c'est ce qui rend la durée juste au rejeu comme en direct.
T.startTs = d.ts || 0; T.doneTs = 0;
setBusy(true); startWorkTimer(); T.typingEl=addTyping(); return; }
setBusy(true); startWorkTimer(); paintTurnClock(); T.typingEl=addTyping(); return; }
// Fin de tour : la discussion vient d'être enregistrée côté serveur — son
// titre (déduit du 1er message) et son compteur d'échanges ont changé. Pas au
// replay, qui rejoue tous les tours passés d'un bloc.
@@ -290,6 +311,7 @@ function handleDelta(d){
T.doneTs = d.ts || T.doneTs || 0;
if(T.serverStats) renderStats(T.contentEl||T.reasonEl, T.serverStats);
else if(T.contentEl) paintStats(T.contentEl); // sans mesures serveur, la durée reste
paintTurnClock(); // fige le total au-dessus de la carte
setBusy(false); if(!REPLAYING && typeof loadConversations==='function') loadConversations(); return; }
if(d.error){ removeTyping(); T.contentEl=null; T.reasonEl=null; const eb=addMsg('assistant',''); eb.classList.add('errmsg'); renderBody(eb, d.error); return; }
if(d.compacting!==undefined){ setCompacting(d.compacting); return; }
+26 -11
View File
@@ -51,14 +51,28 @@ async function modeOnConvChange(){
// Émise par le serveur (delta code_hint) quand un message de mode chat
// ressemble à une tâche de code. Ignorable, jamais bloquante.
// Insère un bandeau DANS le composeur, au-dessus de la barre de saisie.
// #composer est en position:absolute : un élément posé AVANT lui dans le DOM
// reste dans le flux normal de la page et se retrouve donc CACHÉ dessous. Ses
// enfants, eux, suivent sa colonne (--col) et son empilement.
function composerBanner(id){
const composer = document.getElementById('composer');
if(!composer) return null;
let el = document.getElementById(id);
if(!el){
el = document.createElement('div');
el.id = id;
el.className = 'composer-banner';
composer.insertBefore(el, composer.firstChild);
}
return el;
}
function showCodeHint(){
if(CURRENT_MODE==='code' || document.getElementById('code-hint')) return;
const composer = document.getElementById('composer');
if(!composer) return;
const chip = document.createElement('div');
chip.id = 'code-hint';
const chip = composerBanner('code-hint');
if(!chip) return;
chip.innerHTML = 'ça ressemble à une tâche de code — <button type="button" id="code-hint-yes">passer en mode Code</button><button type="button" id="code-hint-no" title="ignorer" aria-label="ignorer">✕</button>';
composer.parentNode.insertBefore(chip, composer);
chip.querySelector('#code-hint-yes').onclick = ()=>{ setMode('code'); };
chip.querySelector('#code-hint-no').onclick = hideCodeHint;
}
@@ -76,14 +90,15 @@ function renderCriteria(list){
let box = document.getElementById('criteria-box');
if(!list || !list.length){ if(box) box.remove(); return; }
if(CURRENT_MODE!=='code'){ if(box) box.remove(); return; }
if(!box){
const composer = document.getElementById('composer');
if(!composer) return;
box = document.createElement('div');
box.id = 'criteria-box';
const fresh = !box;
box = composerBanner('criteria-box');
if(!box) return;
if(fresh){
box.innerHTML = '<div id="criteria-head"></div><ul id="criteria-list"></ul>';
composer.parentNode.insertBefore(box, composer);
box.querySelector('#criteria-head').onclick = ()=> box.classList.toggle('folded');
// Repliée par défaut : cinq critères dépliés mangeaient la moitié de
// l'écran au-dessus de la saisie. L'en-tête porte déjà le compteur.
box.classList.add('folded');
}
box.style.display='';
const passed = list.filter(c=>c.status==='passed').length;