mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Un .gguf ne pouvait s'effacer qu'en cochant « supprimer aussi le fichier »
au moment de supprimer son preset. Tout ce qui avait été téléchargé sans
preset — une quantization essayée puis abandonnée, un projecteur du mauvais
dépôt — restait donc sur le disque pour des dizaines de gigaoctets, sans
aucun moyen de partir depuis Loki.
L'éditeur de preset porte désormais une ligne « Modèles installés » (repliée,
sous le sélecteur de fichier) : chaque .gguf des dossiers déclarés avec sa
taille, ses tranches, qui le réclame, et un bouton « supprimer ».
Rien n'est réversible ici, d'où deux garde-fous côté serveur, partagés par
/api/models/delete ET par la case de l'éditeur de preset :
- le modèle CHARGÉ est intouchable — l'effacer sous le moteur ne rend pas
l'espace tant qu'il tient le descripteur, et le démarrage suivant meurt
sur un fichier introuvable ;
- un modèle réclamé par des presets ne part qu'avec `force`, le refus 409
nommant les presets pour que la seconde confirmation dise ce qu'on casse.
Supprimer un preset ne peut donc plus emporter le modèle de son voisin.
La suppression emporte toute la famille de tranches et annonce la place
réellement libérée. /api/models marque au passage le modèle chargé et les
presets qui référencent chaque fichier.
Le sélecteur de l'en-tête ne liste plus que des presets. Il proposait aussi
les .gguf du disque, chargés en n'écrivant que MODEL= : le contexte, le NGL
et l'échantillonnage du modèle PRÉCÉDENT restaient en place, et un 27B chargé
par-dessus les réglages d'un 4B partait sur 100k de contexte pour mourir en
OOM sans que rien ne dise pourquoi. Charger un modèle, c'est appliquer TOUS
ses réglages : ça s'appelle un preset, et c'est le seul chemin. /api/models/use
disparaît avec le groupe qu'il servait.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoKfYRvVHTf8GyKhgY5WxT
134 lines
6.2 KiB
JavaScript
134 lines
6.2 KiB
JavaScript
// ─── Coque de l'interface ────────────────────────────────────────────────────
|
|
// Les commandes de la refonte : escamoter la barre latérale, replier le
|
|
// moniteur machine, basculer le thème depuis l'en-tête, changer de modèle sans
|
|
// ouvrir les réglages.
|
|
//
|
|
// Chaque état vit sur <html> (data-side, data-perf, data-theme) et dans
|
|
// localStorage, et il est REPOSÉ AVANT LE RENDU par le script du <head> : sans
|
|
// ça, la barre latérale s'afficherait puis se refermerait sous les yeux à chaque
|
|
// chargement de page.
|
|
|
|
// Escamotage de la barre latérale (grand écran). Sur téléphone elle est déjà un
|
|
// tiroir — c'est toggleSide() qui s'en charge, et le bouton ☰ qui l'ouvre.
|
|
function toggleSideCollapse(){
|
|
if(window.innerWidth <= 720){ toggleSide(); return; }
|
|
const on = document.documentElement.getAttribute('data-side') !== '0';
|
|
document.documentElement.setAttribute('data-side', on ? '0' : '1');
|
|
try{ localStorage.setItem('loki-side', on ? '0' : '1'); }catch(_){}
|
|
// La largeur du fil change sans qu'aucun `resize` ne soit émis : la gouttière
|
|
// de barre de défilement mesurée dans --sbw resterait celle d'avant, et la
|
|
// carte de saisie serait décalée du fil.
|
|
if(typeof syncGutter === 'function') requestAnimationFrame(syncGutter);
|
|
}
|
|
|
|
// Bascule clair/sombre depuis l'en-tête. Elle passe par applyTheme + savePrefs
|
|
// (01-theme.js) pour rester alignée avec l'interrupteur des réglages et avec le
|
|
// serveur — deux chemins vers le même réglage, une seule source de vérité.
|
|
function toggleThemeQuick(){
|
|
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
applyTheme(dark ? 'light' : 'dark');
|
|
if(typeof savePrefs === 'function') savePrefs();
|
|
}
|
|
|
|
// Déplie un <details> ET tous ceux qui le contiennent. Depuis que les réglages
|
|
// vivent sous un repli unique, ouvrir une section par programme (journal du
|
|
// moteur, installation llama.cpp) ne suffit plus : elle s'ouvrait à l'intérieur
|
|
// d'un parent fermé, donc invisible.
|
|
function openDetails(el){
|
|
if(typeof el === 'string') el = document.getElementById(el);
|
|
while(el){
|
|
if(el.tagName === 'DETAILS') el.open = true;
|
|
// La cible vit dans un panneau de la modale de réglages : ouvrir la modale
|
|
// sur ce panneau. C'est ce qui garde tous les appelants historiques
|
|
// (pastille d'état, jobs moteur) valides depuis la refonte en modale.
|
|
if(el.classList && el.classList.contains('set-pane') && typeof openSettings === 'function'){
|
|
openSettings(el.dataset.pane);
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
}
|
|
|
|
// Sélecteur de modèle de l'en-tête. Il ne liste QUE des presets ; en choisir un
|
|
// revient à cliquer la ligne correspondante dans les réglages (switchTo demande
|
|
// confirmation puis relance le moteur).
|
|
//
|
|
// Il a listé un temps les .gguf du disque, qu'on chargeait en n'écrivant que
|
|
// MODEL= : le contexte, le NGL et l'échantillonnage du modèle PRÉCÉDENT
|
|
// restaient en place. Un 27B chargé par-dessus les réglages d'un 4B partait sur
|
|
// 100k de contexte et mourait en OOM, sans que rien ne dise pourquoi. Charger un
|
|
// modèle, c'est appliquer TOUS ses réglages : ça s'appelle un preset, et c'est
|
|
// le seul chemin.
|
|
function onModelSwitch(sel){
|
|
const v = sel.value;
|
|
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
|
|
// annulée) : on ne touche pas à la sélection ici, sinon l'affichage mentirait
|
|
// pendant les quelques secondes du redémarrage.
|
|
switchTo(n, name);
|
|
}
|
|
|
|
// 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
|
|
// empreinte correspond exactement à la configuration courante. Une configuration
|
|
// écrite à la main, héritée d'une image, ou modifiée d'un cran depuis le dernier
|
|
// preset ne correspond à AUCUN d'entre eux — et l'en-tête n'affichait alors rien
|
|
// alors qu'un modèle était bel et bien chargé. On retombe donc sur le nom du
|
|
// fichier de la configuration active.
|
|
//
|
|
// 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 = '';
|
|
|
|
function renderModelSwitch(presets, active){
|
|
MS_PRESETS = presets || []; MS_ACTIVE = active || null;
|
|
paintModelSwitch();
|
|
}
|
|
// Appelé par loadCfg avec la valeur MODEL de la configuration active.
|
|
function setLoadedModel(name){
|
|
MS_MODEL = (name || '').trim();
|
|
paintModelSwitch();
|
|
}
|
|
// Nom lisible d'un fichier de modèle : sans dossier ni extension. Un .gguf
|
|
// s'appelle « Qwen3-14B-Instruct-Q4_K_M.gguf » — le chemin et le suffixe
|
|
// n'apprennent rien et mangent toute la place.
|
|
function modelLabel(path){
|
|
const base = path.split(/[\\/]/).pop();
|
|
return base.replace(/\.gguf$/i, '');
|
|
}
|
|
function paintModelSwitch(){
|
|
const sel = document.getElementById('model-switch');
|
|
if(!sel) return;
|
|
const prev = sel.value;
|
|
sel.textContent = '';
|
|
// Ligne de tête : ce qui est RÉELLEMENT chargé, quand aucun preset ne le
|
|
// revendique. Value vide → onModelSwitch l'ignore, elle ne bascule rien.
|
|
if(!MS_ACTIVE){
|
|
const o = document.createElement('option');
|
|
o.value = '';
|
|
o.textContent = MS_MODEL ? modelLabel(MS_MODEL) : (MS_PRESETS.length ? 'aucun modèle actif' : 'aucun preset');
|
|
o.selected = true;
|
|
sel.appendChild(o);
|
|
}
|
|
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);
|
|
}
|
|
sel.disabled = !MS_PRESETS.length;
|
|
sel.title = MS_MODEL
|
|
? 'modèle chargé : ' + MS_MODEL + ' — changer de preset relance le moteur'
|
|
: 'aucun modèle configuré';
|
|
// Une bascule en cours ne doit pas voir sa sélection sauter sous la souris.
|
|
if(prev && [...sel.options].some(o => o.value === prev)) sel.value = prev;
|
|
}
|