mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
vitesse des bulles calculee depuis les horodatages serveur (ts par evenement)
Le calcul cote client (performance.now) donnait une vitesse presente ou non selon que le tour etait live ou rejoue. On horodate chaque LogEvent cote serveur (TS) et le client derive tok/s de firstTs->lastTs : correct en direct ET au replay. Hack replayTarget supprime.
This commit is contained in:
1 parent
3748d3a25f
commit
54e14c7935
2 files changed
+26
-25
No files matched your search
+8
-3
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// État de conversation CÔTÉ SERVEUR — une seule conversation partagée par tous
|
||||
@@ -30,9 +31,12 @@ import (
|
||||
const maxLogEvents = 5000
|
||||
|
||||
// LogEvent = un événement d'affichage rejouable (un delta SSE + son numéro de
|
||||
// séquence monotone).
|
||||
// séquence monotone + un horodatage serveur en ms). Le TS permet au client de
|
||||
// calculer la vitesse (tok/s) à partir du temps RÉEL de génération — correct
|
||||
// aussi bien en direct qu'au replay (où tout arrive d'un bloc côté client).
|
||||
type LogEvent struct {
|
||||
Seq int `json:"seq"`
|
||||
TS int64 `json:"ts"`
|
||||
Delta map[string]any `json:"delta"`
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@ func (c *Conversation) persist() {
|
||||
func (c *Conversation) appendDelta(delta map[string]any) {
|
||||
c.mu.Lock()
|
||||
c.Seq++
|
||||
c.Log = append(c.Log, LogEvent{Seq: c.Seq, Delta: delta})
|
||||
c.Log = append(c.Log, LogEvent{Seq: c.Seq, TS: time.Now().UnixMilli(), Delta: delta})
|
||||
if len(c.Log) > maxLogEvents {
|
||||
c.Log = c.Log[len(c.Log)-maxLogEvents:]
|
||||
}
|
||||
@@ -281,8 +285,9 @@ func (c *Conversation) Subscribe(ctx context.Context, from int, emit func(map[st
|
||||
}
|
||||
last = ev.Seq
|
||||
delta := ev.Delta
|
||||
ts := ev.TS
|
||||
c.mu.Unlock()
|
||||
out := map[string]any{"seq": ev.Seq}
|
||||
out := map[string]any{"seq": ev.Seq, "ts": ts}
|
||||
for k, v := range delta {
|
||||
out[k] = v
|
||||
}
|
||||
|
||||
+18
-22
@@ -1373,25 +1373,20 @@ function saveChat(){ try{ localStorage.setItem('jean.chat', JSON.stringify(msgs)
|
||||
// permanent (connectStream), qui rejoue tout le fil depuis le serveur — texte,
|
||||
// appels d'outils, vitesses, raisonnement — puis suit le direct. Plus de
|
||||
// localStorage : le même contexte est partagé par tous les appareils.
|
||||
async function restoreChat(){
|
||||
// On note où en est le serveur : tout ce qui existe déjà sera rejoué d'un bloc
|
||||
// (replay, sans vitesse) ; ce qui vient après est du direct (avec vitesse).
|
||||
try{ const s=await (await jfetch('/api/chat/state')).json(); replayTarget=s.seq||0; }catch(e){}
|
||||
connectStream();
|
||||
}
|
||||
// Source de vérité = SERVEUR : on ouvre le flux d'abonnement permanent qui rejoue
|
||||
// tout le fil (texte, outils, vitesses via les horodatages serveur, raisonnement)
|
||||
// puis suit le direct. Partagé par tous les appareils.
|
||||
function restoreChat(){ connectStream(); }
|
||||
function onKey(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); } }
|
||||
// ===== Conversation SERVEUR (source de vérité, partagée entre appareils) =====
|
||||
// L'historique et la génération vivent sur le serveur jean. Le client ouvre un
|
||||
// flux d'ABONNEMENT permanent (SSE) qui rejoue le journal depuis lastSeq puis
|
||||
// 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.
|
||||
// replayTarget = dernier Seq déjà présent sur le serveur au moment où l'on se
|
||||
// (re)connecte. Les événements jusque-là sont du REPLAY (arrivent d'un bloc) →
|
||||
// on n'affiche PAS de vitesse (elle serait absurde). Au-delà = direct → vitesse.
|
||||
let lastSeq=0, streamAbort=null, replayTarget=0;
|
||||
let lastSeq=0, streamAbort=null;
|
||||
// État de rendu du tour courant, délimité par les événements user / turn_done.
|
||||
let T=null;
|
||||
function newTurn(){ T={ reasonEl:null, contentEl:null, pendingToolEl:null, typingEl:null, fullContent:'', fullReason:'', turnCollapsibles:[], serverStats:null, reasonTok:0, contentTok:0, reasonStart:0, contentStart:0 }; }
|
||||
function newTurn(){ T={ reasonEl:null, contentEl:null, pendingToolEl:null, typingEl:null, fullContent:'', fullReason:'', turnCollapsibles:[], serverStats:null, reasonTok:0, contentTok:0, reasonFirstTs:0, reasonLastTs:0, contentFirstTs:0, contentLastTs:0 }; }
|
||||
newTurn();
|
||||
const simpleMode=()=>document.documentElement.getAttribute('data-display')==='simple';
|
||||
function removeTyping(){ if(T.typingEl){ T.typingEl.remove(); T.typingEl=null; } }
|
||||
@@ -1409,14 +1404,15 @@ function renderStats(el, s){
|
||||
if(s.gen_tokens) parts.push('decode '+s.gen_tokens+' tok · '+(s.gen_per_second||0).toFixed(1)+' tok/s');
|
||||
setLabel(el, parts.join(' · '));
|
||||
}
|
||||
// Label LIVE d'une bulle : nombre de tokens + vitesse mesurée côté client au fil
|
||||
// des deltas (comme avant la refonte). En replay (seq<=replayTarget), on n'affiche
|
||||
// que le nombre de tokens — pas la vitesse, car tout arrive d'un bloc.
|
||||
function labelTokens(el, role, n, start, seq){
|
||||
// Label d'une bulle : nombre de tokens + vitesse. La vitesse est calculée à
|
||||
// partir des HORODATAGES SERVEUR (firstTs→lastTs) : le temps réel de génération,
|
||||
// donc correct aussi bien en direct qu'au replay (où les deltas arrivent d'un
|
||||
// bloc côté client, mais leurs ts serveur restent espacés du vrai temps écoulé).
|
||||
function labelTokens(el, role, n, firstTs, lastTs){
|
||||
if(!el) return;
|
||||
if(seq && seq<=replayTarget){ setLabel(el, role+' · '+n+' tok'); return; }
|
||||
const secs=(performance.now()-start)/1000; const tps= secs>0.05 ? (n/secs):0;
|
||||
setLabel(el, role+' · '+n+' tok · '+tps.toFixed(1)+' tok/s');
|
||||
const secs=(lastTs-firstTs)/1000;
|
||||
if(secs>0.05 && n>1){ setLabel(el, role+' · '+n+' tok · '+(n/secs).toFixed(1)+' tok/s'); }
|
||||
else { setLabel(el, role+' · '+n+' tok'); }
|
||||
}
|
||||
function setBusy(on){ busy=on;
|
||||
document.getElementById('send').style.display=on?'none':'inline-block';
|
||||
@@ -1447,15 +1443,15 @@ function handleDelta(d){
|
||||
killTyping();
|
||||
if(!T.reasonEl){ collapseAll(T.turnCollapsibles); T.reasonEl=addMsg('reasoning',''); T.fullReason=''; T.turnCollapsibles.push(T.reasonEl); }
|
||||
showTyping(); T.fullReason+=d.reasoning_content; renderBody(T.reasonEl, T.fullReason);
|
||||
if(!T.reasonStart) T.reasonStart=performance.now(); T.reasonTok++;
|
||||
labelTokens(T.reasonEl, 'reasoning', T.reasonTok, T.reasonStart, d.seq);
|
||||
if(!T.reasonFirstTs) T.reasonFirstTs=d.ts||0; T.reasonLastTs=d.ts||T.reasonLastTs; T.reasonTok++;
|
||||
labelTokens(T.reasonEl, 'reasoning', T.reasonTok, T.reasonFirstTs, T.reasonLastTs);
|
||||
return; }
|
||||
if(d.content){
|
||||
removeTyping();
|
||||
if(!T.contentEl){ collapseAll(T.turnCollapsibles); T.contentEl=addMsg('assistant',''); T.fullContent=''; }
|
||||
T.fullContent+=d.content; renderBody(T.contentEl, T.fullContent);
|
||||
if(!T.contentStart) T.contentStart=performance.now(); T.contentTok++;
|
||||
labelTokens(T.contentEl, 'assistant', T.contentTok, T.contentStart, d.seq);
|
||||
if(!T.contentFirstTs) T.contentFirstTs=d.ts||0; T.contentLastTs=d.ts||T.contentLastTs; T.contentTok++;
|
||||
labelTokens(T.contentEl, 'assistant', T.contentTok, T.contentFirstTs, T.contentLastTs);
|
||||
return; }
|
||||
}
|
||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||
|
||||
Reference in new issue
Block a user