// ─── 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 (data-side, data-perf, data-theme) et dans
// localStorage, et il est REPOSÉ AVANT LE RENDU par le script du
: 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 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;
el = el.parentElement;
}
}
// Sélecteur de modèle de l'en-tête. Il liste les presets ; en choisir un revient
// à cliquer la ligne correspondante dans les réglages (switchTo demande
// confirmation puis relance le moteur).
function onModelSwitch(sel){
const n = parseInt(sel.value, 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);
}
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;
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;
}