mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 15:08:16 +02:00
Merge branch 'claude/ajean-loki-container-fork-aep9r2'
This commit is contained in:
7 files changed
+417
-121
No files matched your search
+182
-60
@@ -130,7 +130,9 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
|
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
|
||||||
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
|
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
|
||||||
#brand{display:inline-flex;align-items:center}
|
#brand{display:inline-flex;align-items:center}
|
||||||
#brand svg{height:10px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
/* 17px : le logo pixel-art était minuscule à 10px, illisible sur un écran dense.
|
||||||
|
Multiple impair volontaire — la grille du SVG (5 px de haut) reste nette. */
|
||||||
|
#brand svg{height:17px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
||||||
#brand-word{display:inline-flex;color:var(--accent)}
|
#brand-word{display:inline-flex;color:var(--accent)}
|
||||||
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
|
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
|
||||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
|
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
|
||||||
@@ -146,6 +148,21 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
|
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
|
||||||
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
|
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
|
||||||
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
|
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
|
||||||
|
/* Séparateur de groupe dans la barre latérale : sépare les réglages d'IA des
|
||||||
|
réglages d'application, sans imbriquer un second niveau de repli (illisible
|
||||||
|
dans une colonne aussi étroite). */
|
||||||
|
.side-sep{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);
|
||||||
|
margin:18px 0 8px;padding-top:12px;border-top:1px solid var(--border)}
|
||||||
|
/* Choix d'avatar : une grille de pastilles emoji, celle retenue encadrée. */
|
||||||
|
.avpick{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
|
||||||
|
.avpick button{font-size:15px;line-height:1;padding:5px 6px;background:var(--panel);
|
||||||
|
border:1px solid var(--border);border-radius:8px;cursor:pointer}
|
||||||
|
.avpick button:hover{border-color:var(--accent)}
|
||||||
|
.avpick button.sel{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 15%,transparent)}
|
||||||
|
/* Avatar + nom en tête de bulle : l'emoji garde sa taille propre, le nom hérite
|
||||||
|
du style de .label (petites capitales) mais sans forcer la casse du prénom. */
|
||||||
|
.msg .label .av{font-style:normal;font-size:13px;margin-right:5px;vertical-align:-1px}
|
||||||
|
.msg .label .who{text-transform:none;letter-spacing:.04em;font-size:11px}
|
||||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
||||||
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
||||||
@@ -1343,6 +1360,85 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<details><summary><span class="stitle">System prompt</span></summary>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
|
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
||||||
|
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
||||||
|
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
||||||
|
étaient des cartes dans une carte). -->
|
||||||
|
<div class="slist">
|
||||||
|
<!-- Moteur fourni par l'image Docker : aucun des trois modes ne s'applique
|
||||||
|
(rien à installer, et l'image n'embarque ni cmake ni compilateur). -->
|
||||||
|
<div id="lc-provided" class="rowbody" style="display:none">
|
||||||
|
<div style="font-size:12px;line-height:1.5">Moteur <b>fourni par l'image Docker</b> — llama.cpp officiel, déjà compilé.
|
||||||
|
<div class="muted" style="margin-top:4px">Rien à installer ni à mettre à jour ici : le moteur suit l'image.
|
||||||
|
Pour en changer (version épinglée, CPU, Vulkan), reconstruis l'image avec le build-arg <code>LLAMACPP_IMAGE</code>.</div>
|
||||||
|
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- La pastille « conseillé » n'est PAS écrite ici : le bon choix dépend de
|
||||||
|
la machine (voir lcRenderReco). Sur Linux + NVIDIA le précompilé ne
|
||||||
|
donne que du Vulkan — llama.cpp ne publie aucun binaire CUDA Linux —
|
||||||
|
alors que compiler donne du CUDA natif. -->
|
||||||
|
<div class="lc-modes">
|
||||||
|
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
||||||
|
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco" id="lc-reco-fast" hidden>conseillé</span></div>
|
||||||
|
<div class="lc-mode-d" id="lc-desc-fast">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
||||||
|
<div class="lc-mode-state" id="lc-fast-state"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
||||||
|
<div class="lc-mode-t">llama.cpp compilé <span class="lc-reco" id="lc-reco-opt" hidden>conseillé</span></div>
|
||||||
|
<div class="lc-mode-d" id="lc-desc-opt">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
||||||
|
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||||
|
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
||||||
|
<div class="lc-mode-d">un fork de llama.cpp pour modèles spéciaux (quant ternaire…)</div>
|
||||||
|
<div class="lc-mode-state" id="lc-custom-state"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="lc-job" class="rowbody" style="display:none">
|
||||||
|
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
|
||||||
|
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
||||||
|
<pre id="lc-log" hidden></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||||
|
leur place est ici, avec le moteur, et pas dans une section « Service »
|
||||||
|
à part qui ne contenait qu'eux. -->
|
||||||
|
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<div class="row" style="margin:0">
|
||||||
|
<button onclick="act('start')">démarrer</button>
|
||||||
|
<button onclick="act('restart')">redémarrer</button>
|
||||||
|
<button onclick="act('stop')">arrêter</button>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
|
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||||
|
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||||
|
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||||
|
l'application elle-même. -->
|
||||||
|
<div class="side-sep">Paramètres</div>
|
||||||
|
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<div class="pe-row">
|
||||||
|
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
|
||||||
|
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi" oninput="saveIdentity()"></span>
|
||||||
|
</div>
|
||||||
|
<div class="pe-row stack">
|
||||||
|
<span class="pe-row-l">Ton avatar</span>
|
||||||
|
<div id="p-avatar-user" class="avpick"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pe-row stack">
|
||||||
|
<span class="pe-row-l">Avatar de Loki</span>
|
||||||
|
<div id="p-avatar-ai" class="avpick"></div>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
<details><summary><span class="stitle">Apparence</span></summary>
|
<details><summary><span class="stitle">Apparence</span></summary>
|
||||||
<div class="slist">
|
<div class="slist">
|
||||||
<label class="switchrow rev"><span>mode sombre</span>
|
<label class="switchrow rev"><span>mode sombre</span>
|
||||||
@@ -1358,11 +1454,6 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary><span class="stitle">System prompt</span></summary>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
|
||||||
</div></div>
|
|
||||||
</details>
|
|
||||||
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
||||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur ces adresses. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur ces adresses. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
||||||
<!-- UNE carte pour tout l'endpoint : adresse locale, bascule « public » et
|
<!-- UNE carte pour tout l'endpoint : adresse locale, bascule « public » et
|
||||||
@@ -1434,59 +1525,6 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div></div>
|
</div></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
|
||||||
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
|
||||||
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
|
||||||
étaient des cartes dans une carte). -->
|
|
||||||
<div class="slist">
|
|
||||||
<!-- Moteur fourni par l'image Docker : aucun des trois modes ne s'applique
|
|
||||||
(rien à installer, et l'image n'embarque ni cmake ni compilateur). -->
|
|
||||||
<div id="lc-provided" class="rowbody" style="display:none">
|
|
||||||
<div style="font-size:12px;line-height:1.5">Moteur <b>fourni par l'image Docker</b> — llama.cpp officiel, déjà compilé.
|
|
||||||
<div class="muted" style="margin-top:4px">Rien à installer ni à mettre à jour ici : le moteur suit l'image.
|
|
||||||
Pour en changer (version épinglée, CPU, Vulkan), reconstruis l'image avec le build-arg <code>LLAMACPP_IMAGE</code>.</div>
|
|
||||||
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- La pastille « conseillé » n'est PAS écrite ici : le bon choix dépend de
|
|
||||||
la machine (voir lcRenderReco). Sur Linux + NVIDIA le précompilé ne
|
|
||||||
donne que du Vulkan — llama.cpp ne publie aucun binaire CUDA Linux —
|
|
||||||
alors que compiler donne du CUDA natif. -->
|
|
||||||
<div class="lc-modes">
|
|
||||||
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
|
||||||
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco" id="lc-reco-fast" hidden>conseillé</span></div>
|
|
||||||
<div class="lc-mode-d" id="lc-desc-fast">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
|
||||||
<div class="lc-mode-state" id="lc-fast-state"></div>
|
|
||||||
</div>
|
|
||||||
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
|
||||||
<div class="lc-mode-t">llama.cpp compilé <span class="lc-reco" id="lc-reco-opt" hidden>conseillé</span></div>
|
|
||||||
<div class="lc-mode-d" id="lc-desc-opt">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
|
||||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
|
||||||
</div>
|
|
||||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
|
||||||
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
|
||||||
<div class="lc-mode-d">un fork de llama.cpp pour modèles spéciaux (quant ternaire…)</div>
|
|
||||||
<div class="lc-mode-state" id="lc-custom-state"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="lc-job" class="rowbody" style="display:none">
|
|
||||||
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
|
|
||||||
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
|
||||||
<pre id="lc-log" hidden></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
|
||||||
leur place est ici, avec le moteur, et pas dans une section « Service »
|
|
||||||
à part qui ne contenait qu'eux. -->
|
|
||||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="row" style="margin:0">
|
|
||||||
<button onclick="act('start')">démarrer</button>
|
|
||||||
<button onclick="act('restart')">redémarrer</button>
|
|
||||||
<button onclick="act('stop')">arrêter</button>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</details>
|
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||||
@@ -2977,7 +3015,7 @@ async function loadAll(){
|
|||||||
// allSettled et pas all : un seul chargement en échec (accès distant coupé,
|
// 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
|
// clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les
|
||||||
// hauteurs réservées en place pour toujours.
|
// 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
|
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); }
|
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
|
||||||
@@ -4032,6 +4070,11 @@ function addMsg(role, text){
|
|||||||
el.querySelector('.label').onclick=()=>toggleCollapse(el);
|
el.querySelector('.label').onclick=()=>toggleCollapse(el);
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML='<span class="label">'+role+'</span><div class="body"></div>';
|
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;
|
el.querySelector('.body').textContent=text;
|
||||||
chatEl().appendChild(el);
|
chatEl().appendChild(el);
|
||||||
@@ -5844,4 +5887,83 @@ async function convDelete(id, title){
|
|||||||
loadConversations();
|
loadConversations();
|
||||||
}catch(_){ toast('erreur réseau'); }
|
}catch(_){ toast('erreur réseau'); }
|
||||||
}
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
</script></body></html>
|
</script></body></html>
|
||||||
@@ -161,6 +161,85 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<details><summary><span class="stitle">System prompt</span></summary>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
|
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
||||||
|
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
||||||
|
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
||||||
|
étaient des cartes dans une carte). -->
|
||||||
|
<div class="slist">
|
||||||
|
<!-- Moteur fourni par l'image Docker : aucun des trois modes ne s'applique
|
||||||
|
(rien à installer, et l'image n'embarque ni cmake ni compilateur). -->
|
||||||
|
<div id="lc-provided" class="rowbody" style="display:none">
|
||||||
|
<div style="font-size:12px;line-height:1.5">Moteur <b>fourni par l'image Docker</b> — llama.cpp officiel, déjà compilé.
|
||||||
|
<div class="muted" style="margin-top:4px">Rien à installer ni à mettre à jour ici : le moteur suit l'image.
|
||||||
|
Pour en changer (version épinglée, CPU, Vulkan), reconstruis l'image avec le build-arg <code>LLAMACPP_IMAGE</code>.</div>
|
||||||
|
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- La pastille « conseillé » n'est PAS écrite ici : le bon choix dépend de
|
||||||
|
la machine (voir lcRenderReco). Sur Linux + NVIDIA le précompilé ne
|
||||||
|
donne que du Vulkan — llama.cpp ne publie aucun binaire CUDA Linux —
|
||||||
|
alors que compiler donne du CUDA natif. -->
|
||||||
|
<div class="lc-modes">
|
||||||
|
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
||||||
|
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco" id="lc-reco-fast" hidden>conseillé</span></div>
|
||||||
|
<div class="lc-mode-d" id="lc-desc-fast">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
||||||
|
<div class="lc-mode-state" id="lc-fast-state"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
||||||
|
<div class="lc-mode-t">llama.cpp compilé <span class="lc-reco" id="lc-reco-opt" hidden>conseillé</span></div>
|
||||||
|
<div class="lc-mode-d" id="lc-desc-opt">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
||||||
|
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||||
|
</div>
|
||||||
|
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||||
|
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
||||||
|
<div class="lc-mode-d">un fork de llama.cpp pour modèles spéciaux (quant ternaire…)</div>
|
||||||
|
<div class="lc-mode-state" id="lc-custom-state"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="lc-job" class="rowbody" style="display:none">
|
||||||
|
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
|
||||||
|
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
||||||
|
<pre id="lc-log" hidden></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||||
|
leur place est ici, avec le moteur, et pas dans une section « Service »
|
||||||
|
à part qui ne contenait qu'eux. -->
|
||||||
|
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<div class="row" style="margin:0">
|
||||||
|
<button onclick="act('start')">démarrer</button>
|
||||||
|
<button onclick="act('restart')">redémarrer</button>
|
||||||
|
<button onclick="act('stop')">arrêter</button>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
|
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||||
|
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||||
|
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||||
|
l'application elle-même. -->
|
||||||
|
<div class="side-sep">Paramètres</div>
|
||||||
|
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||||
|
<div class="slist"><div class="rowbody">
|
||||||
|
<div class="pe-row">
|
||||||
|
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
|
||||||
|
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi" oninput="saveIdentity()"></span>
|
||||||
|
</div>
|
||||||
|
<div class="pe-row stack">
|
||||||
|
<span class="pe-row-l">Ton avatar</span>
|
||||||
|
<div id="p-avatar-user" class="avpick"></div>
|
||||||
|
</div>
|
||||||
|
<div class="pe-row stack">
|
||||||
|
<span class="pe-row-l">Avatar de Loki</span>
|
||||||
|
<div id="p-avatar-ai" class="avpick"></div>
|
||||||
|
</div>
|
||||||
|
</div></div>
|
||||||
|
</details>
|
||||||
<details><summary><span class="stitle">Apparence</span></summary>
|
<details><summary><span class="stitle">Apparence</span></summary>
|
||||||
<div class="slist">
|
<div class="slist">
|
||||||
<label class="switchrow rev"><span>mode sombre</span>
|
<label class="switchrow rev"><span>mode sombre</span>
|
||||||
@@ -176,11 +255,6 @@
|
|||||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary><span class="stitle">System prompt</span></summary>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
|
||||||
</div></div>
|
|
||||||
</details>
|
|
||||||
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
||||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur ces adresses. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur ces adresses. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
||||||
<!-- UNE carte pour tout l'endpoint : adresse locale, bascule « public » et
|
<!-- UNE carte pour tout l'endpoint : adresse locale, bascule « public » et
|
||||||
@@ -252,59 +326,6 @@
|
|||||||
</div></div>
|
</div></div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
|
||||||
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
|
||||||
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
|
||||||
étaient des cartes dans une carte). -->
|
|
||||||
<div class="slist">
|
|
||||||
<!-- Moteur fourni par l'image Docker : aucun des trois modes ne s'applique
|
|
||||||
(rien à installer, et l'image n'embarque ni cmake ni compilateur). -->
|
|
||||||
<div id="lc-provided" class="rowbody" style="display:none">
|
|
||||||
<div style="font-size:12px;line-height:1.5">Moteur <b>fourni par l'image Docker</b> — llama.cpp officiel, déjà compilé.
|
|
||||||
<div class="muted" style="margin-top:4px">Rien à installer ni à mettre à jour ici : le moteur suit l'image.
|
|
||||||
Pour en changer (version épinglée, CPU, Vulkan), reconstruis l'image avec le build-arg <code>LLAMACPP_IMAGE</code>.</div>
|
|
||||||
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- La pastille « conseillé » n'est PAS écrite ici : le bon choix dépend de
|
|
||||||
la machine (voir lcRenderReco). Sur Linux + NVIDIA le précompilé ne
|
|
||||||
donne que du Vulkan — llama.cpp ne publie aucun binaire CUDA Linux —
|
|
||||||
alors que compiler donne du CUDA natif. -->
|
|
||||||
<div class="lc-modes">
|
|
||||||
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
|
||||||
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco" id="lc-reco-fast" hidden>conseillé</span></div>
|
|
||||||
<div class="lc-mode-d" id="lc-desc-fast">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
|
||||||
<div class="lc-mode-state" id="lc-fast-state"></div>
|
|
||||||
</div>
|
|
||||||
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
|
||||||
<div class="lc-mode-t">llama.cpp compilé <span class="lc-reco" id="lc-reco-opt" hidden>conseillé</span></div>
|
|
||||||
<div class="lc-mode-d" id="lc-desc-opt">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
|
||||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
|
||||||
</div>
|
|
||||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
|
||||||
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
|
||||||
<div class="lc-mode-d">un fork de llama.cpp pour modèles spéciaux (quant ternaire…)</div>
|
|
||||||
<div class="lc-mode-state" id="lc-custom-state"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="lc-job" class="rowbody" style="display:none">
|
|
||||||
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
|
|
||||||
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
|
||||||
<pre id="lc-log" hidden></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
|
||||||
leur place est ici, avec le moteur, et pas dans une section « Service »
|
|
||||||
à part qui ne contenait qu'eux. -->
|
|
||||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
|
||||||
<div class="slist"><div class="rowbody">
|
|
||||||
<div class="row" style="margin:0">
|
|
||||||
<button onclick="act('start')">démarrer</button>
|
|
||||||
<button onclick="act('restart')">redémarrer</button>
|
|
||||||
<button onclick="act('stop')">arrêter</button>
|
|
||||||
</div>
|
|
||||||
</div></div>
|
|
||||||
</details>
|
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
<div class="slist"><div class="rowbody">
|
<div class="slist"><div class="rowbody">
|
||||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ async function loadAll(){
|
|||||||
// allSettled et pas all : un seul chargement en échec (accès distant coupé,
|
// 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
|
// clé API absente…) ne doit pas empêcher la suite — et surtout pas laisser les
|
||||||
// hauteurs réservées en place pour toujours.
|
// 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
|
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); }
|
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);
|
el.querySelector('.label').onclick=()=>toggleCollapse(el);
|
||||||
} else {
|
} else {
|
||||||
el.innerHTML='<span class="label">'+role+'</span><div class="body"></div>';
|
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;
|
el.querySelector('.body').textContent=text;
|
||||||
chatEl().appendChild(el);
|
chatEl().appendChild(el);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -104,7 +104,9 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
|
/* Logo : « Loki » dessiné en pixels (SVG 4x5 par lettre, trait d'un module). Le « A »
|
||||||
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
|
reste un bloc à part : il apparaît en glissant quand le mode agent est actif. */
|
||||||
#brand{display:inline-flex;align-items:center}
|
#brand{display:inline-flex;align-items:center}
|
||||||
#brand svg{height:10px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
/* 17px : le logo pixel-art était minuscule à 10px, illisible sur un écran dense.
|
||||||
|
Multiple impair volontaire — la grille du SVG (5 px de haut) reste nette. */
|
||||||
|
#brand svg{height:17px;width:auto;display:block;shape-rendering:crispEdges;fill:currentColor}
|
||||||
#brand-word{display:inline-flex;color:var(--accent)}
|
#brand-word{display:inline-flex;color:var(--accent)}
|
||||||
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
|
#ver{font-size:10px;font-weight:400;color:var(--dim);margin-left:7px;letter-spacing:0;align-self:flex-end;line-height:1;position:relative;top:1px}
|
||||||
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
|
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:6px}
|
||||||
@@ -120,6 +122,21 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
|
.preset:hover .conv-acts,.preset:focus-within .conv-acts{opacity:1}
|
||||||
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
|
.conv-acts .iconbtn{font-size:11px;line-height:1;padding:4px 6px;background:transparent;border:1px solid transparent;border-radius:6px;color:var(--dim);cursor:pointer}
|
||||||
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
|
.conv-acts .iconbtn:hover{border-color:var(--border);color:var(--text)}
|
||||||
|
/* Séparateur de groupe dans la barre latérale : sépare les réglages d'IA des
|
||||||
|
réglages d'application, sans imbriquer un second niveau de repli (illisible
|
||||||
|
dans une colonne aussi étroite). */
|
||||||
|
.side-sep{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);
|
||||||
|
margin:18px 0 8px;padding-top:12px;border-top:1px solid var(--border)}
|
||||||
|
/* Choix d'avatar : une grille de pastilles emoji, celle retenue encadrée. */
|
||||||
|
.avpick{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}
|
||||||
|
.avpick button{font-size:15px;line-height:1;padding:5px 6px;background:var(--panel);
|
||||||
|
border:1px solid var(--border);border-radius:8px;cursor:pointer}
|
||||||
|
.avpick button:hover{border-color:var(--accent)}
|
||||||
|
.avpick button.sel{border-color:var(--accent);background:color-mix(in srgb,var(--accent) 15%,transparent)}
|
||||||
|
/* Avatar + nom en tête de bulle : l'emoji garde sa taille propre, le nom hérite
|
||||||
|
du style de .label (petites capitales) mais sans forcer la casse du prénom. */
|
||||||
|
.msg .label .av{font-style:normal;font-size:13px;margin-right:5px;vertical-align:-1px}
|
||||||
|
.msg .label .who{text-transform:none;letter-spacing:.04em;font-size:11px}
|
||||||
/* pastille d'état service, en haut à droite de la barre latérale */
|
/* pastille d'état service, en haut à droite de la barre latérale */
|
||||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap;cursor:pointer}
|
||||||
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
.statuspill.ok{color:var(--ok);border-color:color-mix(in srgb,var(--ok) 45%,transparent);background:color-mix(in srgb,var(--ok) 12%,transparent)}
|
||||||
|
|||||||
@@ -25,6 +25,50 @@ var webPrefsAllowed = map[string]map[string]bool{
|
|||||||
"hide_stats": {"0": true, "1": true},
|
"hide_stats": {"0": true, "1": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// avatarChoices : les emojis proposés pour toi et pour Loki. Liste FERMÉE —
|
||||||
|
// n'accepter qu'un jeu connu évite de stocker une chaîne arbitraire qui finirait
|
||||||
|
// affichée dans chaque bulle du chat.
|
||||||
|
var avatarChoices = []string{
|
||||||
|
"🙂", "😎", "🧑", "👤", "🦊", "🐱", "🐼", "🦉",
|
||||||
|
"🚀", "⚡", "🌙", "🔥", "🌿", "🎯", "🧠", "🤖",
|
||||||
|
}
|
||||||
|
|
||||||
|
// webPrefsText : préférences à valeur libre, avec un nettoyage propre à chacune.
|
||||||
|
// Renvoie la valeur retenue et si elle est acceptable.
|
||||||
|
var webPrefsText = map[string]func(string) (string, bool){
|
||||||
|
// Prénom affiché dans le chat : une ligne, sans caractères de contrôle, et
|
||||||
|
// borné en RUNES (24 « é » ne doivent pas compter double).
|
||||||
|
"user_name": func(v string) (string, bool) {
|
||||||
|
v = strings.TrimSpace(strings.Map(func(r rune) rune {
|
||||||
|
if r < 0x20 || r == 0x7f {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, v))
|
||||||
|
if r := []rune(v); len(r) > 24 {
|
||||||
|
v = strings.TrimSpace(string(r[:24]))
|
||||||
|
}
|
||||||
|
return v, true // vide = revenir au libellé par défaut
|
||||||
|
},
|
||||||
|
"avatar_user": avatarPref,
|
||||||
|
"avatar_ai": avatarPref,
|
||||||
|
}
|
||||||
|
|
||||||
|
// avatarPref n'accepte qu'un emoji de la liste fermée (ou le vide, qui rétablit
|
||||||
|
// le défaut).
|
||||||
|
func avatarPref(v string) (string, bool) {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if v == "" {
|
||||||
|
return "", true
|
||||||
|
}
|
||||||
|
for _, a := range avatarChoices {
|
||||||
|
if a == v {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
// loadWebPrefs lit les préférences enregistrées (map vide si aucune).
|
// loadWebPrefs lit les préférences enregistrées (map vide si aucune).
|
||||||
func loadWebPrefs() map[string]string {
|
func loadWebPrefs() map[string]string {
|
||||||
out := map[string]string{}
|
out := map[string]string{}
|
||||||
@@ -40,6 +84,12 @@ func saveWebPrefs(in map[string]string) (map[string]string, error) {
|
|||||||
defer webPrefsMu.Unlock()
|
defer webPrefsMu.Unlock()
|
||||||
prefs := loadWebPrefs()
|
prefs := loadWebPrefs()
|
||||||
for k, v := range in {
|
for k, v := range in {
|
||||||
|
if clean, ok := webPrefsText[k]; ok {
|
||||||
|
if val, good := clean(v); good {
|
||||||
|
prefs[k] = val
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
allowed, ok := webPrefsAllowed[k]
|
allowed, ok := webPrefsAllowed[k]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -69,5 +119,7 @@ func handleWebPrefs(w http.ResponseWriter, r *http.Request) {
|
|||||||
sendJSON(w, 200, map[string]any{"ok": true, "prefs": prefs})
|
sendJSON(w, 200, map[string]any{"ok": true, "prefs": prefs})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sendJSON(w, 200, map[string]any{"ok": true, "prefs": loadWebPrefs()})
|
// avatars : la liste fermée est servie avec les préférences, pour que l'UI
|
||||||
|
// n'ait pas sa propre copie (deux listes divergent tôt ou tard).
|
||||||
|
sendJSON(w, 200, map[string]any{"ok": true, "prefs": loadWebPrefs(), "avatars": avatarChoices})
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user