Identité dans le chat, section Paramètres, logo agrandi

- Identité : prénom et avatars (emoji) affichés en tête des bulles, à la
  place des libellés « user » / « assistant ». Stockés dans les préférences
  SERVEUR, donc partagés entre appareils comme le thème. La liste d'emojis
  est servie par /api/prefs : pas de seconde copie côté client.
  Validation stricte — prénom nettoyé de ses caractères de contrôle et borné
  à 24 runes, avatar refusé s'il n'est pas dans la liste fermée.
- Barre latérale : séparateur « Paramètres » qui regroupe Identité,
  Apparence, Accès OpenAI, Accès distant et Actions, sous les réglages d'IA
  (Machine, Presets, Mode agent, System prompt, Moteur). Un séparateur plutôt
  qu'un second niveau de repli, illisible dans une colonne aussi étroite.
- Logo : 10 → 17 px, il était minuscule sur un écran dense.

Le prénom vient de l'utilisateur : les libellés sont écrits en textContent,
jamais en innerHTML.
This commit is contained in:
Loki committed 2026-08-15 11:35:40 +00:00
1 parent ecaa847439
commit 54c5eb1334
7 files changed
+417 -121

No files matched your search

+1 -1
View File
@@ -491,7 +491,7 @@ async function loadAll(){
// allSettled et pas all : un seul chargement en échec (accès distant coupé,
// clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les
// hauteurs réservées en place pour toujours.
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp(),loadRemote()]);
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadNode(),loadApiKey(),loadNetwork(),loadPrefs(),loadLlamacpp(),loadRemote()]);
releaseHeights(); // tout est en place : on rend la main et on mesure pour la prochaine fois
}
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
@@ -49,6 +49,11 @@ function addMsg(role, text){
el.querySelector('.label').onclick=()=>toggleCollapse(el);
} else {
el.innerHTML='<span class="label">'+role+'</span><div class="body"></div>';
// user / assistant portent un avatar et un nom (voir 17-identity.js) ;
// reasoning et tool gardent leur libellé technique.
if((role==='user'||role==='assistant') && typeof paintLabel==='function'){
paintLabel(el.querySelector('.label'), role);
}
}
el.querySelector('.body').textContent=text;
chatEl().appendChild(el);
+79
View File
@@ -0,0 +1,79 @@
// Identité — prénom et avatars affichés en tête des bulles du chat.
//
// Les valeurs vivent dans les préférences SERVEUR (comme le thème) : elles
// suivent donc d'un appareil à l'autre, au lieu de rester dans le localStorage
// d'un seul navigateur. La liste d'emojis est celle que sert le serveur — pas
// une seconde copie côté client, qui divergerait à la première modification.
let IDENT = {name:'', user:'🙂', ai:'🦊'};
let AVATARS = [];
// applyIdentity redessine les libellés des bulles DÉJÀ affichées : changer son
// prénom doit se voir tout de suite, sans recharger la page.
function applyIdentity(){
document.querySelectorAll('#chat .msg.user > .label, #chat .msg.assistant > .label')
.forEach(l => paintLabel(l, l.parentElement.classList.contains('user') ? 'user' : 'assistant'));
}
// paintLabel remplit un libellé : avatar + nom. Le prénom vient de
// l'utilisateur, donc textContent — jamais innerHTML.
function paintLabel(label, role){
if(!label) return;
label.textContent = '';
const av = document.createElement('i'); av.className = 'av';
av.textContent = role === 'user' ? (IDENT.user || '🙂') : (IDENT.ai || '🦊');
const who = document.createElement('span'); who.className = 'who';
who.textContent = role === 'user' ? (IDENT.name || 'toi') : 'Loki';
label.appendChild(av); label.appendChild(who);
}
async function loadIdentity(){
let r;
try{ r = await jget('/api/prefs'); }catch(_){ return; }
const p = r.prefs || {};
AVATARS = r.avatars || [];
IDENT = {
name: p.user_name || '',
user: p.avatar_user || '🙂',
ai: p.avatar_ai || '🦊',
};
const inp = document.getElementById('p-username');
if(inp && document.activeElement !== inp) inp.value = IDENT.name;
renderAvatarPicker('p-avatar-user', 'user');
renderAvatarPicker('p-avatar-ai', 'ai');
applyIdentity();
}
function renderAvatarPicker(elID, which){
const box = document.getElementById(elID);
if(!box) return;
box.innerHTML = '';
for(const e of AVATARS){
const b = document.createElement('button');
b.type = 'button'; b.textContent = e;
if(e === IDENT[which]) b.classList.add('sel');
b.onclick = () => pickAvatar(which, e);
box.appendChild(b);
}
}
async function pickAvatar(which, emoji){
IDENT[which] = emoji;
renderAvatarPicker(which === 'user' ? 'p-avatar-user' : 'p-avatar-ai', which);
applyIdentity();
try{ await jpost('/api/prefs', which === 'user' ? {avatar_user:emoji} : {avatar_ai:emoji}); }
catch(_){ toast('erreur réseau'); }
}
// Saisie du prénom : on n'écrit pas au serveur à chaque frappe.
let identTimer = null;
function saveIdentity(){
const inp = document.getElementById('p-username');
if(!inp) return;
IDENT.name = inp.value.trim();
applyIdentity();
clearTimeout(identTimer);
identTimer = setTimeout(async () => {
try{ await jpost('/api/prefs', {user_name: IDENT.name}); }catch(_){ toast('erreur réseau'); }
}, 500);
}