Sélecteur de modèle qui charge vraiment ; % de chargement qui bouge

Quatre retouches d'interface :

- Le sélecteur de l'en-tête liste maintenant les MODÈLES du disque (groupe
  « Modèles (.gguf) ») en plus des presets : en choisir un le charge —
  POST /api/models/use écrit MODEL seul (contexte, NGL et échantillonnage
  conservés) et redémarre le service en arrière-plan. Sans preset créé, le
  sélecteur n'offrait aucun choix.

- Pourcentage de chargement : sur un redémarrage à chaud, le modèle est déjà
  dans le cache disque — llama-server ne lit rien (read_bytes reste à 0) et
  la pastille passait de « 0 % » à « prêt » sans jamais monter. On prend le
  plus avancé de read_bytes et de la mémoire résidente (VmRSS), qui grandit
  cache ou pas.

- Discussions triées par date de CRÉATION (récentes en tête) : une
  discussion garde sa place, écrire dans un vieux fil ne le fait plus
  remonter.

- Bouton Réglages ancré en pied de barre latérale, juste au-dessus du
  moniteur Performance — toujours au même endroit, quel que soit le nombre
  de discussions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Fable 5 committed 2026-08-20 22:13:16 +02:00
1 parent ad107ed284
commit 86d711e772
9 files changed
+207 -46

No files matched your search

+1 -1
View File
@@ -451,7 +451,7 @@ async function loadAll(){
// MANQUANTE lève ici une ReferenceError qui est avalée en silence. Toute
// suppression de module doit donc retirer son appel de cette ligne, et le test
// navigateur écoute `pageerror` pour ne pas s'en apercevoir trop tard.
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadThinkEffort(),loadTasks()]);
await Promise.allSettled([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadModelSwitchFiles(),loadConversations(),loadIdentity(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadThinkEffort(),loadTasks()]);
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); }
+5 -6
View File
@@ -24,13 +24,12 @@ async function loadConversations(){
let r;
try{ r = await jget('/api/conversations'); }catch(_){ return; }
CONVS = r.conversations || [];
// La plus récente EN TÊTE. Le serveur trie déjà par date d'activité, mais son
// tri STABLE laissait une discussion toute neuve SOUS celle qu'on vient de
// quitter (les deux sont touchées dans la même seconde) : on départage par la
// date de création, puis par l'ordre du fichier d'index (création croissante).
// Ordre de CRÉATION, la plus récente en tête : une discussion garde sa place
// dans la liste quoi qu'il arrive — écrire dans un vieux fil ne le fait pas
// remonter (le serveur, lui, trie par activité). Départage par l'ordre du
// fichier d'index pour les créations dans la même seconde.
const ord = new Map(CONVS.map((c, i) => [c.id, i]));
CONVS.sort((a, b) => (b.updated || 0) - (a.updated || 0)
|| (b.created || 0) - (a.created || 0)
CONVS.sort((a, b) => (b.created || 0) - (a.created || 0)
|| ord.get(b.id) - ord.get(a.id));
CONV_ACTIVE = r.active || '';
CONV_BUSY = !!r.busy;
+60 -10
View File
@@ -52,7 +52,14 @@ function openDetails(el){
// à cliquer la ligne correspondante dans les réglages (switchTo demande
// confirmation puis relance le moteur).
function onModelSwitch(sel){
const n = parseInt(sel.value, 10);
const v = sel.value;
// Entrée du groupe « Modèles » : charger CE .gguf, sans preset — la config
// active garde son contexte et son échantillonnage, seul MODEL change.
if(v && v.startsWith('m:')){
switchModelFile(v.slice(2), sel.options[sel.selectedIndex].textContent);
return;
}
const n = parseInt(v, 10);
if(!n) return;
const name = sel.options[sel.selectedIndex].textContent;
// La liste est repeinte par loadPresets() une fois la bascule faite (ou
@@ -61,6 +68,22 @@ function onModelSwitch(sel){
switchTo(n, name);
}
// Bascule directe vers un fichier de modèle (POST /api/models/use) : confirme,
// écrit MODEL, le service redémarre derrière. Annulé ou en échec, la sélection
// revient sur le modèle réellement chargé.
async function switchModelFile(value, name){
if(!await askConfirm('Charger « ' + name + ' » et redémarrer le moteur ?', {title:'Changer de modèle', okText:'Charger'})){
paintModelSwitch();
return;
}
toast('changement de modèle…');
let r = null;
try{ r = await jpost('/api/models/use', {value}); }catch(_){}
if(!r || !r.ok){ toast('erreur : ' + ((r && r.error) || 'réseau')); paintModelSwitch(); return; }
toast('modèle en cours de chargement…');
loadAll();
}
// Le sélecteur affiche LE MODÈLE CHARGÉ, pas seulement le preset actif.
//
// Les deux ne coïncident pas toujours : un preset n'est « actif » que si son
@@ -73,12 +96,22 @@ function onModelSwitch(sel){
// Deux sources, deux moments : loadPresets() donne la liste, loadCfg() donne le
// modèle. Chacune dépose ce qu'elle sait et redemande le dessin — elles partent
// en parallèle (loadAll), aucun ordre n'est garanti.
let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '';
let MS_PRESETS = [], MS_ACTIVE = null, MS_MODEL = '', MS_FILES = [];
function renderModelSwitch(presets, active){
MS_PRESETS = presets || []; MS_ACTIVE = active || null;
paintModelSwitch();
}
// Les fichiers .gguf découverts sur le disque : le sélecteur doit permettre de
// CHARGER un modèle, pas seulement de rappeler un preset — sans preset créé,
// il n'offrait aucun choix. Chargé par loadAll(), rafraîchi avec le reste.
async function loadModelSwitchFiles(){
try{
const r = await jget('/api/models');
MS_FILES = Array.isArray(r) ? r : [];
}catch(_){ return; }
paintModelSwitch();
}
// Appelé par loadCfg avec la valeur MODEL de la configuration active.
function setLoadedModel(name){
MS_MODEL = (name || '').trim();
@@ -105,14 +138,31 @@ function paintModelSwitch(){
o.selected = true;
sel.appendChild(o);
}
MS_PRESETS.forEach((p, i) => {
const o = document.createElement('option');
o.value = String(i + 1);
o.textContent = p.name;
if(MS_ACTIVE && p.id === MS_ACTIVE.id) o.selected = true;
sel.appendChild(o);
});
sel.disabled = !MS_PRESETS.length;
if(MS_PRESETS.length){
const g = document.createElement('optgroup'); g.label = 'Presets';
MS_PRESETS.forEach((p, i) => {
const o = document.createElement('option');
o.value = String(i + 1);
o.textContent = p.name;
if(MS_ACTIVE && p.id === MS_ACTIVE.id) o.selected = true;
g.appendChild(o);
});
sel.appendChild(g);
}
// Modèles du disque : en choisir un le CHARGE (voir switchModelFile). Un
// preset règle tout (contexte, sampling…) ; un modèle ne change que MODEL.
if(MS_FILES.length){
const g = document.createElement('optgroup'); g.label = 'Modèles (.gguf)';
for(const m of MS_FILES){
const o = document.createElement('option');
o.value = 'm:' + m.value;
o.textContent = modelLabel(m.name);
if(m.missing && m.missing.length) o.disabled = true; // tranches manquantes
g.appendChild(o);
}
sel.appendChild(g);
}
sel.disabled = !MS_PRESETS.length && !MS_FILES.length;
sel.title = MS_MODEL
? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur'
: 'aucun modèle configuré';