mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-13 18:28:03 +02:00
Nom du modèle affiché à côté de « Loki » sur chaque réponse
Le nom du modèle (fichier .gguf, sans dossier ni extension) est journalisé avec la borne de tour côté serveur : chaque réponse garde le modèle qui l'a réellement produite, y compris après un rechargement de page (rejeu du journal) et même si un autre modèle est chargé depuis. Côté UI, une pastille discrète s'affiche dans l'étiquette de la bulle de réponse. Elle vit dans dataset.model pour survivre au repeint des libellés lors d'un changement d'identité (applyIdentity). Vérifié en conditions réelles (moteur simulé, rechargement en pleine génération) : badge, tok/s des bulles et chrono « en cours » sont bien rejoués après un refresh. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88dbe5642f
commit
b814862f40
5 files changed
+91
-4
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user