v0.6.3 : texte non coupe autour des outils, garde-fous d'outils retires, retours UI

- Flux : le reliquat retenu par la garde « </think> » est vidé dès qu'un appel
  d'outil arrive, au lieu de la fin du flux. Sans ça il partait APRÈS
  l'événement d'outil et la fin de phrase — souvent coupée en plein mot —
  atterrissait dans une bulle séparée sous l'outil.
- Le plafond d'appels d'outils par tour (TOOL_LIMIT) et l'anti-boucle « appel
  identique répété » sont SUPPRIMÉS : ils coupaient des tours légitimes (une
  recherche enchaîne facilement des dizaines d'appels). Le bouton stop reste
  le seul frein. Endpoint /api/agent/tool-limit et son interrupteur retirés.
- Prompt système : « You are Jean » (majuscule) — le modèle recopiait la casse
  et se présentait en minuscule.
- UI : titres du menu tous en monospace capitales via une seule règle (la casse
  vient du CSS, plus du HTML).
- UI : le message envoyé s'affiche tout de suite en gris (« envoi… ») et
  s'éclaircit quand le serveur le confirme ; il ne disparaît plus pendant
  l'aller-retour, et il est retiré si l'envoi échoue.
- UI : voile de chargement du fil (« chargement de la conversation… » /
  « connexion au serveur… ») — un chat vide ne se confond plus avec une
  connexion lente.
- UI mobile : hauteur de coque 100dvh hors PWA (100vh en PWA), et plus aucun
  défilement du document. Le mode PWA est détecté par navigator.standalone :
  le média display-mode ne matche pas, faute de manifeste.
This commit is contained in:
nathaninline committed 2026-07-29 18:49:53 +02:00
1 parent a3a9c36367
commit 92c350bdb8
16 files changed
+297 -134

No files matched your search

+40 -4
View File
@@ -4,6 +4,32 @@
// suit le direct. Fermer l'onglet n'arrête plus la génération (détachée côté
// serveur) ; se reconnecter rejoue tout le fil, détails compris.
let lastSeq=0, streamAbort=null;
// Bulle « en attente » : affichée EN GRIS dès l'appui sur envoyer, avant tout
// aller-retour réseau. Le message ne disparaît donc plus de l'écran entre la
// frappe et la réponse du serveur. Elle s'éclaircit (classe retirée) quand
// l'événement `user` revient par le flux — preuve que le serveur l'a bien
// enregistré. En cas d'échec d'envoi, elle est retirée et le texte est rendu.
let PENDING=null;
function clearPending(){ if(PENDING){ PENDING.remove(); PENDING=null; } }
function addPending(text){
clearPending();
PENDING=addMsg('user', text);
PENDING.classList.add('pending');
const l=PENDING.querySelector('.label'); if(l) l.textContent='envoi…';
jumpBottom();
return PENDING;
}
// Le serveur confirme le message : on réutilise la bulle grise au lieu d'en
// ajouter une seconde (sinon le message clignoterait en double).
function confirmPending(text){
if(!PENDING) return false;
const b=PENDING.querySelector('.body');
if(!b || b.textContent!==text) return false;
PENDING.classList.remove('pending');
const l=PENDING.querySelector('.label'); if(l) l.textContent='user';
PENDING=null;
return true;
}
// REPLAYING = on est dans le replay initial (rejeu du journal au chargement).
// Pendant ce temps, les bulles raisonnement/outil sont créées DÉJÀ repliées →
// pas d'animation d'ouverture/fermeture au refresh. Le serveur envoie {caught_up}
@@ -54,10 +80,11 @@ function handleDelta(d){
if(d.caught_up){
// Fin du replay initial : on saute en bas puis on révèle (une seule fois — pas
// sur les reconnexions, pour ne pas te ramener en bas si tu lisais plus haut).
setChatLoading(null);
if(REPLAYING){ REPLAYING=false; jumpBottom(); syncSendBtn(); const c=chatEl(); c.style.transition='opacity .15s'; c.style.opacity='1'; }
return; }
if(d.reset!==undefined){ document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); const cb=document.getElementById('compact-banner'); if(cb) cb.style.display='none'; return; }
if(d.user!==undefined){ newTurn(); addMsg('user', d.user); setBusy(true); T.typingEl=addTyping(); return; }
if(d.reset!==undefined){ PENDING=null; document.getElementById('chat').innerHTML=''; newTurn(); setCtxUsed(0); lastSeq=0; setBusy(false); const cb=document.getElementById('compact-banner'); if(cb) cb.style.display='none'; return; }
if(d.user!==undefined){ newTurn(); if(!confirmPending(d.user)) addMsg('user', d.user); setBusy(true); T.typingEl=addTyping(); return; }
if(d.turn_done){ removeTyping(); collapseAll(T.turnCollapsibles); if(T.serverStats) renderStats(T.contentEl||T.reasonEl, T.serverStats); setBusy(false); 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){ const b=document.getElementById('compact-banner'); if(b) b.style.display = d.compacting ? 'flex' : 'none'; return; }
@@ -123,6 +150,7 @@ async function connectStream(){
streamAbort=new AbortController();
try{
const r=await jfetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({from:lastSeq}),signal:streamAbort.signal});
if(REPLAYING) setChatLoading('chargement de la conversation…');
const reader=r.body.getReader(); const dec=new TextDecoder(); let buf='';
while(true){
const {done,value}=await reader.read(); if(done) break;
@@ -137,6 +165,10 @@ async function connectStream(){
}
}
}catch(e){ /* coupure : on reconnecte silencieusement */ }
// Le flux s'est arrêté (coupure ou fin prématurée). Si le fil n'a JAMAIS fini
// de charger, le silence est trompeur — un chat vide sans explication. On le
// dit dans le voile ; il disparaîtra au {caught_up} de la reconnexion.
if(REPLAYING) setChatLoading('connexion au serveur…');
await new Promise(res=>setTimeout(res, 600));
}
}
@@ -153,18 +185,22 @@ async function send(){
const ta=document.getElementById('input'); const text=ta.value.trim();
if(!text) return;
ta.value=''; autoGrow(ta);
// Le message s'affiche TOUT DE SUITE, en gris : il ne disparaît plus le temps
// de l'aller-retour. Il s'éclaircit quand le flux le confirme (confirmPending).
addPending(text);
const fail=(m)=>{ clearPending(); toast(m); ta.value=text; autoGrow(ta); };
for(let attempt=0; attempt<3; attempt++){
try{
const r=await jfetch('/api/chat/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,ctx_used:CTX_USED})});
if(r.status===409) return; // déjà en cours (notre envoi a abouti) → OK
if(r.ok) return; // la bulle + les tokens arrivent par le flux
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} toast(m); ta.value=text; autoGrow(ta); return; }
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} fail(m); return; }
}catch(e){ /* réseau : on retente */ }
await new Promise(res=>setTimeout(res, 600));
}
// Après plusieurs échecs : le serveur a peut-être quand même reçu le message.
try{ const s=await (await jfetch('/api/chat/state')).json(); if(s.generating) return; }catch(_){}
toast('échec de l\'envoi — réessaie'); ta.value=text;
fail('échec de l\'envoi — réessaie');
}
loadAll();
setInterval(loadStatus, 5000);