mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-19 05:07:20 +02:00
v0.8.0 : code mort supprime, UI et docs en ajean, release unique
- suppression des fonctions et types inutilises (staticcheck U1000) - sources UI, workflows, README passes en ajean - release.yml ne publie plus que les binaires ajean-* - notes de release redigees
This commit is contained in:
1 parent
62ad4155a2
commit
687ec81023
26 files changed
+123
-214
No files matched your search
@@ -22,7 +22,7 @@
|
||||
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
||||
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
||||
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
||||
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<script>try{var t=localStorage.getItem('ajean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('ajean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<style>
|
||||
@@CSS@@
|
||||
</style></head><body>
|
||||
@@ -180,7 +180,7 @@
|
||||
trois îlots pour un même réglage. -->
|
||||
<div class="slist">
|
||||
<div class="rowbody">
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>jean</code></div>
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>ajean</code></div>
|
||||
<div class="row" style="margin:0">
|
||||
<input id="oai-url" readonly class="io-input">
|
||||
<button class="iconbtn" onclick="copyText(document.getElementById('oai-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
|
||||
@@ -210,7 +210,7 @@
|
||||
</div></div>
|
||||
</details>
|
||||
<details id="remote-details"><summary><span class="stitle">Accès distant</span><span id="remote-badge" class="sbadge"></span></summary>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur AJEAN depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
||||
<div id="remote-off" class="slist"><div class="rowbody">
|
||||
<p class="muted" style="margin:0 0 8px">Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.</p>
|
||||
|
||||
@@ -56,13 +56,13 @@ function toggleSide(){ document.getElementById('side').classList.toggle('open');
|
||||
let _sysT = null;
|
||||
function saveSys(){
|
||||
const v = document.getElementById('sysprompt').value;
|
||||
localStorage.setItem('jean.sys', v);
|
||||
localStorage.setItem('ajean.sys', v);
|
||||
clearTimeout(_sysT);
|
||||
_sysT = setTimeout(()=>{ jpost('/api/sysprompt', {text:v}).catch(()=>{}); }, 600);
|
||||
}
|
||||
async function loadSys(){
|
||||
try{
|
||||
const d = await jget('/api/sysprompt');
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('jean.sys', d.text || ''); }
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('ajean.sys', d.text || ''); }
|
||||
}catch(e){}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ function applyTheme(id){
|
||||
// désormais la variante, déduite du suffixe.
|
||||
const dark = id==='dark' || (typeof id==='string' && id.endsWith('-dark'));
|
||||
document.documentElement.setAttribute('data-theme', dark?'dark':'light');
|
||||
try{ localStorage.setItem('jean-theme', dark?'dark':'light'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-theme', dark?'dark':'light'); }catch(e){}
|
||||
const sw=document.getElementById('theme-dark'); if(sw) sw.checked=dark;
|
||||
}
|
||||
function toggleThemeDark(){
|
||||
@@ -17,7 +17,7 @@ function toggleThemeDark(){
|
||||
savePrefs();
|
||||
}
|
||||
function initTheme(){
|
||||
let id='light'; try{ id=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
let id='light'; try{ id=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
applyTheme(id);
|
||||
}
|
||||
// ===== Affichage ============================================================
|
||||
@@ -34,7 +34,7 @@ const VIEW_OPTS=[
|
||||
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
||||
function applyView(id, on){
|
||||
document.documentElement.setAttribute('data-'+id, on?'1':'0');
|
||||
try{ localStorage.setItem('jean-'+id, on?'1':'0'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-'+id, on?'1':'0'); }catch(e){}
|
||||
const box=document.getElementById('view-'+id); if(box) box.checked=!!on;
|
||||
// La barre latérale peut rester ouverte quand on repasse en mode fixe.
|
||||
if(id==='hide-side' && !on){
|
||||
@@ -46,7 +46,7 @@ function applyView(id, on){
|
||||
function toggleView(id){ applyView(id, document.getElementById('view-'+id).checked); savePrefs(); }
|
||||
function initView(){
|
||||
VIEW_OPTS.forEach(o=>{
|
||||
let v=null; try{ v=localStorage.getItem('jean-'+o.id); }catch(e){}
|
||||
let v=null; try{ v=localStorage.getItem('ajean-'+o.id); }catch(e){}
|
||||
applyView(o.id, v==='1');
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// ===== Persistance côté serveur (partagée entre appareils) ==================
|
||||
// L'apparence est aussi enregistrée sur la machine jean (/api/prefs) : ainsi le
|
||||
// L'apparence est aussi enregistrée sur la machine ajean (/api/prefs) : ainsi le
|
||||
// thème/affichage choisi sur un appareil se retrouve sur tous les autres. Le
|
||||
// localStorage reste utilisé pour appliquer instantanément au chargement (sans
|
||||
// flash), puis loadPrefs() aligne sur la valeur du serveur (source de vérité).
|
||||
function savePrefs(){
|
||||
let theme='light';
|
||||
try{ theme=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
try{ theme=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
const p={theme};
|
||||
VIEW_OPTS.forEach(o=>{ p[o.id.replace('-','_')] = viewOn(o.id)?'1':'0'; });
|
||||
jpost('/api/prefs', p).catch(()=>{});
|
||||
@@ -27,4 +27,4 @@ async function loadPrefs(){
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('ajean.sys') || ''; loadSys(); restoreChat(); });
|
||||
@@ -49,7 +49,7 @@ function _askKey(e){
|
||||
else if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); askResolve(true); }
|
||||
}
|
||||
function _openAsk(kind, message, opts){
|
||||
// Compat 2 conventions : (message, opts) [UI jean] ET l'objet unique
|
||||
// Compat 2 conventions : (message, opts) [UI ajean] ET l'objet unique
|
||||
// {title,msg,yes,no,placeholder} passé par e2e.js sur ajean.link (server.html,
|
||||
// où cette fonction remplace le window.askConfirm du bootstrap boîte noire).
|
||||
if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Clé de pilotage : envoyée en Authorization: Bearer sur chaque appel /api/*.
|
||||
// Sur un 401 on la (re)demande et on rejoue la requête. Stockée en localStorage.
|
||||
let TOKEN = localStorage.getItem('jean.key') || '';
|
||||
let TOKEN = localStorage.getItem('ajean.key') || '';
|
||||
function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorization']='Bearer '+TOKEN; return h; }
|
||||
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
||||
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
||||
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
||||
// Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le
|
||||
// navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint
|
||||
// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS :
|
||||
// dès qu'on laisse traîner plusieurs onglets AJEAN) reste suspendue POUR TOUJOURS :
|
||||
// ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher.
|
||||
// Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal
|
||||
// et ne sont donc jamais concernés.
|
||||
@@ -30,8 +30,8 @@ async function jfetch(u, opts){
|
||||
}
|
||||
finally{ if(timer) clearTimeout(timer); }
|
||||
if(r.status === 401){
|
||||
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const HRESERVE=['cfg','vram','ram-details','presets','internet-status','mcp-list
|
||||
function reserveHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
try{
|
||||
const h=parseInt(localStorage.getItem('jean-h-'+id)||'0',10);
|
||||
const h=parseInt(localStorage.getItem('ajean-h-'+id)||'0',10);
|
||||
const el=document.getElementById(id);
|
||||
if(el && h>0) el.style.minHeight=h+'px';
|
||||
}catch(e){}
|
||||
@@ -19,7 +19,7 @@ function releaseHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
const el=document.getElementById(id); if(!el) return;
|
||||
el.style.minHeight='';
|
||||
try{ localStorage.setItem('jean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', reserveHeights);
|
||||
@@ -98,7 +98,7 @@ async function loadPresets(){
|
||||
});
|
||||
}
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « ajean » → « ajean ».
|
||||
async function loadAgent(){
|
||||
const s=await jget('/api/agent');
|
||||
const on = s.enabled;
|
||||
|
||||
@@ -194,7 +194,7 @@ async function populateModelPicker(){
|
||||
const list = await jget('/api/models');
|
||||
const cur = currentModelInTextarea();
|
||||
// Les modèles peuvent venir de plusieurs dossiers (disque externe…) : on les
|
||||
// regroupe par dossier, JEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
// regroupe par dossier, AJEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
const groups = [];
|
||||
for(const m of (list||[])){
|
||||
let g = groups.find(x => x.dir === m.dir);
|
||||
@@ -213,7 +213,7 @@ async function populateModelPicker(){
|
||||
opts += '<option value="'+escHtml(m.value)+'"'+on+'>'+escHtml(m.name)+' ('+fmtSize(m.size)+')</option>';
|
||||
}
|
||||
html += groups.length > 1
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier jean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier ajean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
: opts;
|
||||
}
|
||||
// MODEL pointe vers un fichier qu'on ne trouve dans aucun dossier déclaré :
|
||||
@@ -224,7 +224,7 @@ async function populateModelPicker(){
|
||||
sel.innerHTML = html;
|
||||
if(cur && !matched) setModelDirsOpen(true);
|
||||
}
|
||||
// ---- Dossiers de modèles : JEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// ---- Dossiers de modèles : AJEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// Réglage rare : replié derrière une ligne, comme l'éditeur de .env brut.
|
||||
function setModelDirsOpen(open){
|
||||
const b = document.getElementById('m-dirs-body');
|
||||
@@ -253,7 +253,7 @@ async function populateModelDirs(){
|
||||
info.className = 'muted';
|
||||
const n = x.count >= 0 ? (x.count + ' modèle' + (x.count>1?'s':'')) : 'illisible';
|
||||
const free = x.free >= 0 ? ', ' + fmtSize(x.free) + ' libres' : '';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier jean' : '') + ')';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier ajean' : '') + ')';
|
||||
row.append(p, info);
|
||||
if(!x.home){
|
||||
const del = document.createElement('button');
|
||||
@@ -324,13 +324,13 @@ function underDir(p, dir){
|
||||
}
|
||||
let beFastPath = '', beOptPath = '', beFastDir = '';
|
||||
async function populateBackend(){
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier jean).
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier ajean).
|
||||
let lc = {}; try{ lc = await jget('/api/llamacpp'); }catch(_){}
|
||||
beFastPath = (lc.prebuilt && lc.prebuilt.bin) || '';
|
||||
beFastDir = (lc.prebuilt && lc.prebuilt.dir) || '';
|
||||
beOptPath = lc.bin || '';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de jean (l'utilisateur peut y déposer son propre build).
|
||||
// le dossier backends de ajean (l'utilisateur peut y déposer son propre build).
|
||||
const detected = await jget('/api/backends');
|
||||
const sel = document.getElementById('m-backend-detected');
|
||||
let html = '<option value="">— ou choisir un backend détecté —</option>';
|
||||
@@ -664,7 +664,7 @@ function populateSettings(){
|
||||
// --- Décodage spéculatif (--spec-type / --spec-draft-n-max) ----------------
|
||||
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
|
||||
// par des virgules ; une valeur composée (ou un type sorti après cette version
|
||||
// de jean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// de ajean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// au premier changement. On l'ajoute donc telle quelle à la liste plutôt que de
|
||||
// la perdre.
|
||||
function setSpecType(v){
|
||||
@@ -860,7 +860,7 @@ function watchDownload(fname){
|
||||
e.bar.className = 'pe-bar done'; e.bar.firstElementChild.style.width = '100%';
|
||||
stop();
|
||||
await Promise.all([populateModelPicker(), populateDlDirs()]);
|
||||
// Le modèle a pu atterrir hors du dossier jean : l'option porte alors le
|
||||
// Le modèle a pu atterrir hors du dossier ajean : l'option porte alors le
|
||||
// chemin complet, pas le simple nom de fichier.
|
||||
const sel = document.getElementById('m-model');
|
||||
const opt = Array.from(sel.options).find(o => samePath(baseName(o.value), fname));
|
||||
|
||||
@@ -115,7 +115,7 @@ function setLabelCounts(el, add, del){
|
||||
if(del) cnt.appendChild(Object.assign(document.createElement('span'),{className:'d',textContent:'-'+del}));
|
||||
lab.appendChild(cnt);
|
||||
}
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/AJEAN
|
||||
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
|
||||
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
|
||||
// par la préférence d'affichage « masquer la vitesse de génération » (CSS).
|
||||
@@ -276,7 +276,7 @@ async function compactContext(){
|
||||
}
|
||||
// Persistance de la conversation : on garde user+assistant en localStorage pour
|
||||
// survivre à un refresh (les bulles tool/reasoning sont éphémères, non stockées).
|
||||
function saveChat(){ try{ localStorage.setItem('jean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
function saveChat(){ try{ localStorage.setItem('ajean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
// Source de vérité = SERVEUR. Au chargement on ouvre le flux d'abonnement
|
||||
// permanent (connectStream), qui rejoue tout le fil depuis le serveur — texte,
|
||||
// appels d'outils, vitesses, raisonnement — puis suit le direct. Plus de
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ===== 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
|
||||
// L'historique et la génération vivent sur le serveur ajean. 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.
|
||||
@@ -197,7 +197,7 @@ function handleDelta(d){
|
||||
}
|
||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||
// re-téléchargement complet après une coupure / bascule d'appareil).
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet AJEAN laissé
|
||||
// ouvert monopolise une des ~6 connexions simultanées autorisées par domaine :
|
||||
// au-delà, toute requête (journal, installation du moteur…) reste en file
|
||||
// d'attente sans jamais partir ni échouer — un blocage silencieux très
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
|
||||
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
|
||||
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
|
||||
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
|
||||
// l'agent local (/api/link/connect) qui fait le `ajean link` (token + service).
|
||||
|
||||
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
.ask-ok.danger{border-color:var(--err);color:var(--err)}
|
||||
#brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px}
|
||||
/* Mode agent : le A passe en couleur d'accent (il était masqué hors agent,
|
||||
la marque se lisant alors « JEAN »). L'état agent reste signalé par sa
|
||||
la marque se lisant alors « AJEAN »). L'état agent reste signalé par sa
|
||||
pastille dans les réglages. */
|
||||
#brand.agent #brand-a{color:var(--accent)}
|
||||
/* interrupteur (switch) */
|
||||
@@ -935,7 +935,7 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
/* ===== Mise en page de la conversation =============================
|
||||
Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond),
|
||||
question de l'utilisateur en pastille arrondie à droite, composer en
|
||||
« pilule », pas d'étiquettes VOUS/JEAN. Ces règles surchargent la mise en
|
||||
« pilule », pas d'étiquettes VOUS/AJEAN. Ces règles surchargent la mise en
|
||||
page générique plus haut : elles doivent donc rester APRÈS elle. */
|
||||
body{font-size:15px}
|
||||
h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
@@ -949,7 +949,7 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
/* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */
|
||||
#chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6}
|
||||
#chat .msg.user .label{color:var(--dim)}
|
||||
/* Étiquettes VOUS / JEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
/* Étiquettes VOUS / AJEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
||||
#chat .msg .label{display:none}
|
||||
#chat .msg.collapsible .label{display:block}
|
||||
|
||||
Reference in new issue
Block a user