Refonte de l'interface : direction « Sober Tech »

Reprise du design d'après la maquette fournie, sans changer d'architecture :
l'UI reste du HTML/CSS/JS assemblé dans le binaire (voir plus bas).

Charte
- Palette ardoise + sauge en remplacement des bruns « Terre ». Deux variantes :
  claire par défaut (celle de la maquette) et « Deep Dark » (fond #0F172A,
  cartes #1E293B), à un clic depuis l'en-tête. Une variable --on-accent porte ce
  qui s'écrit sur les aplats de sauge, pour que le contraste tienne dans les
  deux variantes.
- Typographie Inter (interface) + JetBrains Mono (code, chiffres, chemins), en
  sous-ensemble latin, embarquées comme l'étaient Bricolage et IBM Plex Mono —
  toujours aucune requête vers un service de polices.
- Boutons sans contour, fond au survol, léger enfoncement au clic.
- Coque à plat : plus de cartes flottantes séparées par une gouttière, la barre
  latérale est collée au bord et le fil occupe tout le reste.

Coque
- Barre d'en-tête : titre de la discussion + sélecteur de modèle. Changer de
  preset demandait d'ouvrir la barre latérale et de descendre jusqu'aux
  presets ; c'est désormais une liste déroulante, toujours visible.
- Barre latérale en trois zones — marque, contenu défilant, moniteur machine —
  et escamotable d'un clic sur grand écran. Les discussions y passent au
  premier plan, avec une recherche textuelle (filtrage côté client : la liste
  est déjà chargée, sans les messages) ; les réglages descendent sous un repli
  unique, d'où openDetails(), qui déplie aussi les parents.
- Moniteur machine en pied de colonne : une jauge par ressource (charge GPU,
  puis VRAM et température en détail, mémoire vive), sauge jusqu'à 75 %, ambre
  puis rouille quand la machine sature. Il remplace la section « Machine », qui
  disait la même chose en plus verbeux et en plus loin.
- L'option « barre latérale escamotable » disparaît d'Apparence : elle faisait
  de la barre un tiroir posé SUR la conversation, là où le bouton d'en-tête
  l'escamote pour de bon. Deux mécanismes concurrents pour la même intention.

Fil et saisie
- La réponse de l'IA redevient une carte, la question un aplat de sauge : sur un
  fond de fil coloré, du texte nu flottait sans ancrage.
- Saisie en pilule, ses outils (joindre, fichiers, envoyer) dans la barre
  elle-même. L'envoi est une pastille ronde à flèche ; le mot « Envoyer » reste
  porté par aria-label et l'infobulle.

Corrections croisées
- L'heure d'une discussion prenait la moitié de la ligne (flex:1 hérité de
  `.preset>span`), le titre était tronqué au premier mot.
- Une liste déroulante posée directement dans une ligne de modale débordait
  sous son intitulé quand sa valeur était longue (chemin de destination).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
This commit is contained in:
Claude committed 2026-08-16 16:17:59 +00:00
1 parent 7310c84129
commit f99e5b59b8
16 files changed
+1160 -255

No files matched your search

+54 -19
View File
@@ -42,7 +42,9 @@ async function loadStatus(){
function toggleSvcLog(){
const box=document.getElementById('svc-log-box');
if(!box) return;
box.open = !box.open;
// openDetails déplie aussi « Réglages » : sans lui, la section s'ouvrait à
// l'intérieur d'un repli fermé — donc nulle part, du point de vue de l'écran.
if(box.open) box.open = false; else openDetails(box);
}
async function loadSvcLog(){
const el=document.getElementById('svc-log');
@@ -127,30 +129,63 @@ function updateCtxMeter(){
// puisse compacter à la demande avant que l'auto-compaction (75%) ne s'en charge.
document.getElementById('ctx-compact').style.display = (pct>=50 && CTX_USED>0) ? 'inline-block' : 'none';
}
// ─── Moniteur machine (pied de barre latérale) ───────────────────────────────
// Une jauge par ressource : nom, pourcentage, barre, puis le détail chiffré en
// dessous. C'est la hiérarchie de la maquette — on lit la charge d'un coup
// d'œil, et le détail (Gio, température) reste là pour qui le cherche.
//
// Construit EN DOM et non en innerHTML : les noms de GPU viennent du pilote,
// donc d'ailleurs. Même règle que pour les noms de fichiers et les titres de
// discussions.
function perfRow(label, pct, sub, title){
pct = Math.max(0, Math.min(100, Math.round(pct||0)));
const row=document.createElement('div'); row.className='perf-row';
if(title) row.title=title;
const l=document.createElement('div'); l.className='perf-l';
const b=document.createElement('b'); b.textContent=label;
const v=document.createElement('span'); v.className='perf-pct'; v.textContent=pct+'%';
l.append(b,v);
const track=document.createElement('div'); track.className='perf-track';
const fill=document.createElement('div');
// Sauge jusqu'à 75 %, ambre au-delà, rouille à 90 : la couleur ne sert à rien
// tant que la machine respire, elle doit sauter aux yeux quand elle sature.
fill.className='perf-fill'+(pct>=90?' max':pct>=75?' hot':'');
fill.style.width=pct+'%';
track.appendChild(fill);
row.append(l,track);
if(sub){ const s=document.createElement('div'); s.className='perf-sub'; s.textContent=sub; row.appendChild(s); }
return row;
}
async function loadVram(){
const gpus=await jget('/api/vram');
// Bloc de statistique : intitulé + valeur sur une ligne, jauge, détail dessous.
// Même gabarit que la RAM (voir .stat dans le CSS) — le HTML libre d'avant
// collait aux bords de la carte.
document.getElementById('vram').innerHTML = (gpus||[]).map(g=>{
const pct=Math.round(g.used*100/g.total);
return '<div class="stat"><div class="stat-h"><span class="stat-n">'+g.name+'</span>'+
'<span class="stat-v">'+(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' GiB</span></div>'+
'<div class="bar"><div style="width:'+pct+'%"></div></div>'+
'<div class="stat-s">GPU '+g.util+' % · '+g.temp+' °C</div></div>';
}).join('') || '<div class="stat"><span class="stat-s">(pas de GPU)</span></div>';
const box=document.getElementById('vram');
if(!box) return;
box.textContent='';
if(!gpus || !gpus.length){
const d=document.createElement('div'); d.className='perf-sub'; d.textContent='(pas de GPU détecté)';
box.appendChild(d);
return;
}
gpus.forEach((g,i)=>{
// Le pourcentage affiché est la CHARGE du processeur graphique (ce que
// montre la maquette) ; la VRAM, elle, passe en détail sous la barre — deux
// mesures distinctes qu'une seule barre ne peut pas porter.
const vram=(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' Gio';
box.appendChild(perfRow(
'GPU '+i+(gpus.length>1?'':'')+' — charge', g.util,
vram+' · '+g.temp+' °C', g.name));
});
}
async function loadRam(){
const m=await jget('/api/ram');
const box=document.getElementById('ram-details');
if(!m || !m.total){ if(box) box.style.display='none'; return; }
if(box) box.style.display='';
const pct=Math.round(m.used*100/m.total);
document.getElementById('ram').innerHTML =
'<div class="stat"><div class="stat-h"><span class="stat-n">Mémoire vive</span>'+
'<span class="stat-v">'+(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' GiB</span></div>'+
'<div class="bar"><div style="width:'+pct+'%"></div></div>'+
'<div class="stat-s">'+pct+' % utilisée</div></div>';
if(!box) return;
if(!m || !m.total){ box.style.display='none'; return; }
box.style.display='';
const host=document.getElementById('ram');
host.textContent='';
host.appendChild(perfRow('Mémoire vive', m.used*100/m.total,
(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' Gio'));
}
async function loadCfg(){
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au