Merge branch 'claude/model-badge-reponses' : nom du modèle sur chaque réponse

This commit is contained in:
Michael committed 2026-08-20 15:58:57 +02:00
commit f4090059f1
5 files changed
+91 -4

No files matched your search

+23
View File
@@ -376,6 +376,13 @@ func (c *Conversation) StartTurn(text string, files []attachInfo, caps Caps, tem
// si le process meurt en pleine génération (crash, restart après MAJ), le
// message de l'utilisateur survit au lieu de disparaître avec le tour.
delta := map[string]any{"user": text}
// Nom du modèle qui va produire la réponse : journalisé avec la borne de
// tour, donc rejoué au chargement — la carte de réponse garde son modèle
// après un rafraîchissement, et un vieux tour garde le modèle de l'époque,
// pas celui chargé aujourd'hui.
if m := chatModelName(); m != "" {
delta["model"] = m
}
if len(files) > 0 {
delta["files"] = files
}
@@ -393,6 +400,22 @@ func (c *Conversation) StartTurn(text string, files []attachInfo, caps Caps, tem
return nil
}
// chatModelName renvoie le nom lisible du modèle configuré : fichier sans
// dossier ni extension .gguf (même règle que modelLabel côté UI), "" si aucun.
func chatModelName() string {
m := strings.TrimSpace(ReadConfig()["MODEL"])
if m == "" {
return ""
}
if i := strings.LastIndexAny(m, "/\\"); i >= 0 {
m = m[i+1:]
}
if strings.HasSuffix(strings.ToLower(m), ".gguf") {
m = m[:len(m)-len(".gguf")]
}
return m
}
// generate exécute un tour complet et journalise chaque événement. Détaché : la
// fermeture du navigateur n'a aucun effet ici, seul /stop (cancel) l'interrompt.
// epoch est capturé au StartTurn : si un Reset survient pendant la génération,
+34 -2
View File
@@ -2052,6 +2052,10 @@ html[data-files="1"] #files-btn{color:var(--accent)}
#criteria-list li.crit-pending{color:var(--dim)}
.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}
/* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris,
pas de capitales forcées) et borné : un nom de .gguf peut être très long,
il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */
.msg .label .modelbadge{display:inline-block;margin-left:6px;padding:0 5px;border:1px solid var(--border);border-radius:3px;color:var(--dim);font-size:9px;letter-spacing:.03em;text-transform:none;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
.msg[data-agent-role="verifier"]{border-left:2px solid var(--accent)}
.askcard .body{display:flex;flex-direction:column;gap:8px}
@@ -6064,7 +6068,7 @@ function confirmPending(text){
let REPLAYING=true;
// É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, reasonFirstTs:0, reasonLastTs:0, contentFirstTs:0, contentLastTs:0, startTs:0, doneTs:0, speedText:'' }; }
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, startTs:0, doneTs:0, speedText:'', model:'' }; }
newTurn();
// --- Temps de travail du tour ------------------------------------------------
@@ -6240,6 +6244,15 @@ function labelTokens(el, role, n, firstTs, lastTs){
if(el.classList.contains('collapsible')) setLabel(el, [role].concat(d||[], m).join(' · '));
else paintStats(el, m);
}
// Badge du modèle sur une bulle de réponse : le nom voyage avec la borne de
// tour (T.model), on le dépose dans dataset.model pour qu'applyIdentity — qui
// repeint les libellés de zéro — puisse le reposer (voir paintLabel).
function tagModel(el){
if(!el || !T.model) return;
el.dataset.model = T.model;
const l = el.querySelector('.label');
if(l && !l.querySelector('.modelbadge') && typeof modelBadge==='function') l.appendChild(modelBadge(T.model));
}
// Pendant le replay on met à jour l'état `busy` mais on NE touche PAS aux boutons
// (sinon user→stop puis turn_done→send à chaque tour rejoué = flottement visible).
// L'état final est appliqué une seule fois au caught_up via syncSendBtn().
@@ -6392,6 +6405,10 @@ function handleDelta(d){
if(d.verify_done){ if(typeof onVerifyDone==='function') onVerifyDone(d.verify_done); return; }
if(d.user!==undefined){
newTurn();
// Modèle du tour : posé par le serveur sur la borne de tour (journalisée),
// donc présent au rejeu comme en direct. Repris par tagModel sur chaque
// bulle de réponse du tour.
T.model = d.model || '';
let el=PENDING;
if(!confirmPending(d.user)) el=addMsg('user', d.user);
// Pièces jointes du tour : rendues DANS la bulle. La bulle en attente en
@@ -6459,7 +6476,7 @@ function handleDelta(d){
return; }
if(d.content){
removeTyping();
if(!T.contentEl){ collapseAll(T.turnCollapsibles); T.contentEl=addMsg('assistant',''); T.fullContent=''; resetContentStats(); }
if(!T.contentEl){ collapseAll(T.turnCollapsibles); T.contentEl=addMsg('assistant',''); tagModel(T.contentEl); T.fullContent=''; resetContentStats(); }
if(d.replace){ smoothSnap(); T.fullContent=''; resetContentStats(); }
T.fullContent+=d.content; feedBlock(T.contentEl, T.fullContent);
if(!T.contentFirstTs) T.contentFirstTs=d.ts0||d.ts||0; T.contentLastTs=d.ts||T.contentLastTs; T.contentTok+=(d.toks||1);
@@ -8163,6 +8180,21 @@ function paintLabel(label, role){
const who = document.createElement('span'); who.className = 'who';
who.textContent = role === 'user' ? (IDENT.name || 'toi') : 'Loki';
label.appendChild(av); label.appendChild(who);
// Badge du modèle : quelle IA a produit CETTE réponse. Le nom vit dans
// dataset.model (posé par tagModel, 09-stream.js) — c'est ce qui le fait
// survivre au repeint des libellés lors d'un changement d'identité.
if(role === 'assistant' && label.parentElement && label.parentElement.dataset.model){
label.appendChild(modelBadge(label.parentElement.dataset.model));
}
}
// modelBadge fabrique la pastille du nom de modèle (partagée paintLabel/tagModel).
function modelBadge(name){
const b = document.createElement('span');
b.className = 'modelbadge';
b.textContent = name;
b.title = 'modèle : ' + name;
return b;
}
async function loadIdentity(){
+15 -2
View File
@@ -51,7 +51,7 @@ function confirmPending(text){
let REPLAYING=true;
// É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, reasonFirstTs:0, reasonLastTs:0, contentFirstTs:0, contentLastTs:0, startTs:0, doneTs:0, speedText:'' }; }
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, startTs:0, doneTs:0, speedText:'', model:'' }; }
newTurn();
// --- Temps de travail du tour ------------------------------------------------
@@ -227,6 +227,15 @@ function labelTokens(el, role, n, firstTs, lastTs){
if(el.classList.contains('collapsible')) setLabel(el, [role].concat(d||[], m).join(' · '));
else paintStats(el, m);
}
// Badge du modèle sur une bulle de réponse : le nom voyage avec la borne de
// tour (T.model), on le dépose dans dataset.model pour qu'applyIdentity — qui
// repeint les libellés de zéro — puisse le reposer (voir paintLabel).
function tagModel(el){
if(!el || !T.model) return;
el.dataset.model = T.model;
const l = el.querySelector('.label');
if(l && !l.querySelector('.modelbadge') && typeof modelBadge==='function') l.appendChild(modelBadge(T.model));
}
// Pendant le replay on met à jour l'état `busy` mais on NE touche PAS aux boutons
// (sinon user→stop puis turn_done→send à chaque tour rejoué = flottement visible).
// L'état final est appliqué une seule fois au caught_up via syncSendBtn().
@@ -379,6 +388,10 @@ function handleDelta(d){
if(d.verify_done){ if(typeof onVerifyDone==='function') onVerifyDone(d.verify_done); return; }
if(d.user!==undefined){
newTurn();
// Modèle du tour : posé par le serveur sur la borne de tour (journalisée),
// donc présent au rejeu comme en direct. Repris par tagModel sur chaque
// bulle de réponse du tour.
T.model = d.model || '';
let el=PENDING;
if(!confirmPending(d.user)) el=addMsg('user', d.user);
// Pièces jointes du tour : rendues DANS la bulle. La bulle en attente en
@@ -446,7 +459,7 @@ function handleDelta(d){
return; }
if(d.content){
removeTyping();
if(!T.contentEl){ collapseAll(T.turnCollapsibles); T.contentEl=addMsg('assistant',''); T.fullContent=''; resetContentStats(); }
if(!T.contentEl){ collapseAll(T.turnCollapsibles); T.contentEl=addMsg('assistant',''); tagModel(T.contentEl); T.fullContent=''; resetContentStats(); }
if(d.replace){ smoothSnap(); T.fullContent=''; resetContentStats(); }
T.fullContent+=d.content; feedBlock(T.contentEl, T.fullContent);
if(!T.contentFirstTs) T.contentFirstTs=d.ts0||d.ts||0; T.contentLastTs=d.ts||T.contentLastTs; T.contentTok+=(d.toks||1);
+15
View File
@@ -25,6 +25,21 @@ function paintLabel(label, role){
const who = document.createElement('span'); who.className = 'who';
who.textContent = role === 'user' ? (IDENT.name || 'toi') : 'Loki';
label.appendChild(av); label.appendChild(who);
// Badge du modèle : quelle IA a produit CETTE réponse. Le nom vit dans
// dataset.model (posé par tagModel, 09-stream.js) — c'est ce qui le fait
// survivre au repeint des libellés lors d'un changement d'identité.
if(role === 'assistant' && label.parentElement && label.parentElement.dataset.model){
label.appendChild(modelBadge(label.parentElement.dataset.model));
}
}
// modelBadge fabrique la pastille du nom de modèle (partagée paintLabel/tagModel).
function modelBadge(name){
const b = document.createElement('span');
b.className = 'modelbadge';
b.textContent = name;
b.title = 'modèle : ' + name;
return b;
}
async function loadIdentity(){
+4
View File
@@ -2021,6 +2021,10 @@ html[data-files="1"] #files-btn{color:var(--accent)}
#criteria-list li.crit-pending{color:var(--dim)}
.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}
/* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris,
pas de capitales forcées) et borné : un nom de .gguf peut être très long,
il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */
.msg .label .modelbadge{display:inline-block;margin-left:6px;padding:0 5px;border:1px solid var(--border);border-radius:3px;color:var(--dim);font-size:9px;letter-spacing:.03em;text-transform:none;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}
.msg[data-agent-role="verifier"]{border-left:2px solid var(--accent)}
.askcard .body{display:flex;flex-direction:column;gap:8px}