// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs. // « llama.cpp précompilé » = binaires officiels, aucune compilation // « llama.cpp compilé » = compilé ici, pour cette machine // « llama.cpp personnalisé » = un fork (modèles à quant spéciale) // Le CHOIX du moteur utilisé se fait par modèle (preset), pas ici — voir la // section « Moteur » dans l'éditeur de modèle. Ça évite que la barre latérale // et les presets se battent pour la ligne BIN. let lcState = null, lcPoll = null, lcLogNext = 0; async function loadLlamacpp(){ let s; try{ s = await jget('/api/llamacpp'); }catch(_){ return; } lcState = s; const pb = s.prebuilt || {}; const fastInstalled = !!pb.bin; const optInstalled = !!s.bin; // Moteur fourni par l'image Docker : les trois modes d'installation n'ont // aucun sens (rien à installer, aucun compilateur dans l'image). On affiche // l'état réel à la place — les boutons du service, eux, restent utiles. lcRenderProvided(s); if(!s.provided){ lcRenderReco(s.reco); lcRenderMode('fast', fastInstalled); lcRenderMode('opt', optInstalled); lcRenderCustomCard(); // Job en cours (page rechargée pendant une install) → on raccroche l'affichage. if(s.job && s.job.exists && s.job.running && !lcPoll){ openDetails('lc-details'); lcStartPolling(); } // Job terminé/interrompu qu'on n'a pas encore montré (rechargement APRÈS coup, // typiquement quand le service a redémarré) : on l'affiche sans polling. else if(s.job && s.job.exists && !s.job.running && !lcPoll && s.job.error && !lcEndShown){ openDetails('lc-details'); document.getElementById('lc-job').style.display = ''; lcLogNext = 0; document.getElementById('lc-log').textContent = ''; lcEndShown = true; lcPollJob(true); // quiet : simple rattrapage, pas de toast ni de rechargement } lcChipSync(s.job); } // Préchauffe la liste des cartes du moteur actif : interroger le moteur prend // 1 à 3 s (init CUDA/Vulkan), et sans ça l'encart « cartes graphiques » de // l'éditeur apparaissait après coup, une fois le reste déjà affiché. if(typeof prefetchGpuDevices === 'function') prefetchGpuDevices(s.config_bin); } // --- Pastille de rappel hors panneau --------------------------------------- // Le détail de l'installation vit dans le panneau latéral, qui est un tiroir // FERMÉ sur téléphone : après un rechargement, rien ne disait qu'une // compilation tournait encore. La pastille le dit, et y ramène en un clic. let lcSeenEnd = false, lcEndShown = false; function lcChipLabel(action){ return {install:'Compilation du moteur', update:'Mise à jour du moteur', prebuilt:'Téléchargement du moteur', engine:'Mise à jour du moteur', custom:'Installation du backend', strata:'Installation de Strata'}[action] || 'Installation du moteur'; } function lcChipSync(j){ const chip = document.getElementById('lc-chip'); if(!chip) return; if(!j || !j.exists || (!j.running && (!j.error || lcSeenEnd))){ chip.hidden = true; return; } chip.hidden = false; chip.classList.toggle('failed', !j.running && !!j.error); chip.textContent = j.running ? '⏳ ' + lcChipLabel(j.action) + ' — ' + (j.phase || '…') : '✗ ' + lcChipLabel(j.action) + ' interrompue'; } // Clic sur la pastille : ouvrir le tiroir sur la section Moteur. function lcChipOpen(){ const side = document.getElementById('side'); if(!side.classList.contains('open')) toggleSide(); const det = document.getElementById('lc-details'); openDetails(det); // déplie aussi « Réglages », qui contient la section det.scrollIntoView({block:'center'}); if(!document.getElementById('lc-chip').classList.contains('failed')) return; lcSeenEnd = true; lcChipSync(null); } // Place la pastille « conseillé » sur la carte que le SERVEUR recommande pour // cette machine (voir recommendedMode) : le précompilé partout, sauf sur Linux // avec une carte NVIDIA où il ne donnerait que du Vulkan. La raison est ajoutée // à la description de la carte, pour expliquer plutôt que d'imposer. function lcRenderReco(reco){ const mode = (reco && reco.mode) || 'fast'; for(const m of ['fast','opt']){ const badge = document.getElementById('lc-reco-'+m); if(badge) badge.hidden = (m !== mode); } // La raison REMPLACE la description générique de la carte conseillée : elle // dit déjà ce que fait l'option et pourquoi c'est le bon choix ici. (loadAll // repasse par là, donc pas d'accumulation possible.) const desc = document.getElementById('lc-desc-'+mode); if(desc && reco && reco.why) desc.textContent = reco.why; } // Bascule entre la carte « moteur de l'image » et les trois modes // d'installation gérés par Loki. function lcRenderProvided(s){ const box = document.getElementById('lc-provided'); const modes = document.querySelector('.lc-modes'); if(!box || !modes) return; box.style.display = s.provided ? '' : 'none'; modes.style.display = s.provided ? 'none' : ''; if(!s.provided) return; const bin = document.getElementById('lc-provided-bin'); if(bin) bin.textContent = s.config_bin || ''; loadEngine(); // Un job de mise à jour du moteur peut être en cours : le bloc de progression // était masqué ici tant que la carte ne servait qu'à dire « rien à faire ». if(s.job && s.job.exists && s.job.running && !lcPoll){ openDetails('lc-details'); lcStartPolling(); } lcChipSync(s.job); } // --- Moteur de l'image : le mettre à jour sans reconstruire l'image --------- // llama.cpp publie plusieurs versions par jour ; son moteur déjà compilé vit // dans l'image officielle, dont on n'extrait que /app (~170 Mo). Voir // web_engine.go pour le détail, et pourquoi ce n'est pas une release GitHub : // llama.cpp n'en publie aucune pour CUDA/Linux. let engState = null; async function loadEngine(){ let s; try{ s = await jget('/api/engine'); }catch(_){ return; } engState = s; const build = document.getElementById('eng-build'); if(build) build.textContent = s.build ? ('b'+s.build+(s.commit ? ' · '+s.commit : '')) : 'version inconnue'; const src = document.getElementById('eng-source'); if(src) src.textContent = {image:'fourni par l\'image', downloaded:'mis à jour par Loki', prebuilt:'précompilé', custom:'personnalisé'}[s.source] || ''; const repo = document.getElementById('eng-repo'); if(repo) repo.textContent = s.repo + ':' + (s.variant || 'server'); // Version précédente (celle qui tournait avant la dernière bascule) : un // bouton à elle, sans réseau. Les autres boutons ne la répètent pas. const prev = s.previous || null; const prevBtn = document.getElementById('eng-prev'); if(prevBtn){ prevBtn.style.display = prev ? '' : 'none'; if(prev) prevBtn.textContent = 'Revenir à la version précédente' + (prev.source === 'image' ? ' (moteur de l\'image' + (prev.build ? ', b'+prev.build : '') + ')' : (prev.build ? ' (b'+prev.build+')' : '')); } // Le retour arrière n'a de sens que si on n'est pas déjà dessus. const revert = document.getElementById('eng-revert'); if(revert) revert.style.display = (s.image_bin && s.source !== 'image' && !(prev && prev.source === 'image')) ? '' : 'none'; // Versions téléchargées gardées à côté (la précédente survit à une mise à // jour) : revenir dessus ne demande ni réseau ni que le tag soit encore publié. const others = document.getElementById('eng-others'); if(others) others.innerHTML = (s.installed || []).filter(v=>!v.in_use && !(prev && prev.tag === v.tag)).map(v=>{ const tag = String(v.tag).replace(/[^A-Za-z0-9._-]/g,''); return ''; }).join(' '); // Build trop ancien pour les points de reprise des hybrides (voir // engineBuildNotice) : un avis, rien n'est changé d'office ici. // L'encart « version recommandée » reprend l'avis : il le remplace quand il // est affiché. const notice = document.getElementById('eng-notice'); if(notice){ notice.textContent = s.notice ? '⚠ '+s.notice : ''; notice.style.display = (s.notice && !s.recommend) ? '' : 'none'; } // Encart « version recommandée » : décidé côté serveur sans réseau ; le tag // n'est choisi qu'au clic (engRecommended). const reco = document.getElementById('eng-reco'); if(reco){ const r = s.recommend; reco.style.display = r ? '' : 'none'; if(r){ document.getElementById('eng-reco-min').textContent = r.min; document.getElementById('eng-reco-cur').textContent = r.current; document.getElementById('eng-reco-gains').innerHTML = (r.gains||[]) .map(g=>'
REASONING_PRESERVE=off dans le preset rétablit le rendu d\'avant ; « Revenir à la version précédente » annule la mise à jour.';
} else {
el.innerHTML = 'rendu du gabarit non vérifié après '+esc(c.tag)+(c.detail ? ' : '+esc(c.detail) : '');
}
}
// engSay écrit sous la ligne d'état : c'est le résultat d'une vérification, qui
// doit rester lisible après le toast (on ne se souvient pas d'un toast).
function engSay(html, cls){
const el = document.getElementById('eng-latest');
if(!el) return;
el.innerHTML = html;
el.className = cls || 'muted';
el.style.display = html ? '' : 'none';
}
async function engCheck(){
toast('vérification…');
let r;
try{ r = await jpost('/api/engine/check', {}); }catch(_){ toast('erreur réseau'); return; }
if(!r.ok){ engSay('✗ '+String(r.error||'').replace(/[<>&]/g,'')); toast('erreur'); return; }
if(r.update){
engSay('Version disponible : b'+r.latest+''+(r.current ? ' (installée : b'+r.current+')' : '')
+ ' — « mettre à jour le moteur » pour l\'installer.');
toast('mise à jour disponible : b'+r.latest);
} else {
engSay('Moteur à jour ✓ (b'+r.latest+')');
toast('moteur à jour ✓');
}
}
async function engUpdate(){ return engPlanAndUpdate(false); }
async function engRecommended(){ return engPlanAndUpdate(true); }
// engPlanAndUpdate : sur clic seulement. Le serveur choisit la version (la
// dernière publiée, ou la plus récente ≥ minimum recommandé) et dit si le
// prompt rendu va changer (preserve_reasoning par défaut depuis b10763) ; la
// confirmation montre les deux, et la case REASONING_PRESERVE=off laisse le
// choix à l'utilisateur — cochée d'office seulement quand le changement est
// établi, puisque sur un gabarit qui garde déjà la réflexion, off changerait
// à son tour le rendu.
async function engPlanAndUpdate(recommended){
toast(recommended ? 'recherche de la version recommandée…' : 'recherche de la dernière version…');
let p;
try{ p = await jpost('/api/engine/plan', {recommended}); }catch(_){ toast('erreur réseau'); return; }
if(!p.ok){ engSay('✗ '+String(p.error||'').replace(/[<>&]/g,'')); toast('erreur'); return; }
if(p.uptodate){ engSay('Moteur à jour ✓ (b'+p.build+')'); toast('moteur à jour ✓'); return; }
const what = p.build ? 'b'+p.build+' ('+p.tag+')' : p.tag;
let msg = 'Télécharger llama.cpp '+what+' (~170 Mo) et l\'utiliser à la place du moteur actuel'
+ (p.current ? ' (b'+p.current+')' : '') + '.\n\n'
+ 'Le moteur est essayé avant d\'être adopté ; '
+ (p.keeps ? 'la version actuelle ('+p.keeps+') est gardée' : 'celui de l\'image reste intact')
+ ' pour revenir en arrière. Le moteur redémarre à la fin — la génération en cours sera coupée.';
const pr = p.preserve || {risk:'no'};
const opts = {title: recommended ? 'Version recommandée' : 'Mettre à jour le moteur', okText:'Mettre à jour'};
if(pr.risk !== 'no'){
msg += '\n\n⚠ Rendu du raisonnement : '+(pr.why||'');
opts.check = 'poser REASONING_PRESERVE=off (garde le rendu actuel de l\'historique)';
opts.checked = !!pr.suggest;
}
if(!await askConfirm(msg, opts)) return;
const preserve_off = pr.risk !== 'no' && askChecked();
let r;
try{ r = await jpost('/api/engine/update', {tag: p.tag, preserve_off}); }catch(_){ toast('erreur réseau'); return; }
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
openDetails('lc-details');
lcStartPolling();
}
// Retour au moteur d'avant la dernière bascule — sans réseau.
async function engRollback(){
const prev = (engState && engState.previous) || null;
if(!prev) return;
const what = prev.source === 'image' ? 'le moteur de l\'image' : (prev.build ? 'b'+prev.build : (prev.tag || prev.bin));
if(!await askConfirm('Revenir à la version précédente ('+what+'). Le moteur redémarre.',
{title:'Revenir à la version précédente', okText:'Revenir'})) return;
let r;
try{ r = await jpost('/api/engine/rollback', {}); }catch(_){ toast('erreur réseau'); return; }
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
toast('version précédente rétablie — redémarrage en cours');
loadAll();
}
// Bascule vers une version déjà installée — « image » = celle d'origine.
async function engUse(tag){
const image = tag === 'image';
if(!await askConfirm(image ? 'Repasser sur le moteur livré avec l\'image Docker. Le moteur redémarre.'
: 'Utiliser la version « '+tag+' ». Le moteur redémarre.',
{title:'Changer de moteur', okText:'Basculer'})) return;
let r;
try{ r = await jpost('/api/engine/use', {tag}); }catch(_){ toast('erreur réseau'); return; }
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
toast('moteur basculé — redémarrage en cours');
loadAll();
}
function lcRenderMode(mode, installed){
const card = document.getElementById(mode==='fast' ? 'lc-mode-fast' : 'lc-mode-opt');
const state = document.getElementById(mode==='fast' ? 'lc-fast-state' : 'lc-opt-state');
card.classList.toggle('installed', installed);
// Lien de vérification : interroge la dernière version SANS rien installer.
// event.stopPropagation empêche le clic de la carte (qui lance l'install).
const check = 'vérifier la version';
if(installed){
state.innerHTML = '✓ installée'
+ '↻ mettre à jour'
+ check;
} else {
state.innerHTML = '→ cliquer pour installer' + check;
}
}
// lcCheck vérifie s'il existe une version plus récente AVANT toute installation
// (endpoints de check dédiés, sans effet de bord). Résultat affiché en toast.
async function lcCheck(mode){
toast('vérification…');
try{
if(mode === 'fast'){
const r = await jpost('/api/llamacpp/prebuilt/check', {});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
if(r.update) toast('nouvelle version disponible : '+r.latest+(r.current ? ' (installée : '+r.current+')' : ''));
else toast('llama.cpp précompilé à jour ✓'+(r.latest ? ' ('+r.latest+')' : ''));
} else {
const r = await jpost('/api/llamacpp/check', {});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
if(r.behind > 0) toast(r.behind+' nouveau(x) commit(s) disponible(s) — utilisez « mettre à jour »');
else toast('llama.cpp compilé à jour ✓');
}
}catch(_){ toast('erreur réseau'); }
}
// Clic sur une carte : installer le moteur (s'il ne l'est pas déjà).
async function lcPick(mode){
const s = lcState || {}, pb = s.prebuilt || {};
const installed = mode==='fast' ? !!pb.bin : !!s.bin;
if(installed){
toast('déjà installée — choisissez-la dans l\'édition d\'un modèle (⚙)');
return;
}
if(mode === 'fast'){
if(!await askConfirm('Télécharger le binaire officiel de llama.cpp, prêt à l\'emploi (~2 min, aucune compilation).', {title:'llama.cpp précompilé', okText:'Installer'})) return;
const r = await jpost('/api/llamacpp/prebuilt', {});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
} else {
if(!await askConfirm('Compiler llama.cpp pour votre machine. Ça peut prendre de longues minutes (surtout avec une carte NVIDIA).', {title:'llama.cpp compilé', okText:'Compiler'})) return;
const r = await jpost('/api/llamacpp/install', {});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
}
lcStartPolling();
}
// « Mettre à jour » sur une carte installée.
async function lcUpdate(mode){
if(mode === 'fast'){
if(!await askConfirm('Vérifier et installer le dernier binaire précompilé.', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
const r = await jpost('/api/llamacpp/prebuilt', {});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
} else {
if(!await askConfirm('Vérifier et installer la dernière version compilée (recompilation si besoin).', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
const r = await jpost('/api/llamacpp/update', {clean:false});
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
}
lcStartPolling();
}
// --- Backends personnalisés (3e carte + modal de gestion) ------------------
let lcCustomBackends = [];
// État affiché sur la carte « Backend personnalisé » : nombre d'installés.
async function lcRenderCustomCard(){
const state = document.getElementById('lc-custom-state');
if(!state) return;
try{ lcCustomBackends = await jget('/api/backends/custom') || []; }catch(_){ lcCustomBackends = []; }
const n = lcCustomBackends.length;
document.getElementById('lc-mode-custom').classList.toggle('installed', n>0);
state.innerHTML = n>0
? '✓ '+n+' installé'+(n>1?'s':'')+'→ gérer'
: '→ voir / installer';
}
function openCustomBackends(){
showModal('lc-custom-modal');
document.getElementById('lc-custom-url').value = '';
document.getElementById('lc-custom-name').value = '';
loadCustomBackends();
}
function closeCustomBackends(){ hideModal('lc-custom-modal'); }
// Liste les backends custom dans le modal, avec un bouton supprimer par ligne.
async function loadCustomBackends(){
const box = document.getElementById('lc-custom-list');
box.innerHTML = 'chargement…';
let list = [];
try{ list = await jget('/api/backends/custom') || []; }catch(_){}
lcCustomBackends = list;
if(!list.length){ box.innerHTML = 'aucun backend personnalisé pour l\'instant.'; return; }
box.innerHTML = list.map(b=>{
const nm = String(b.name).replace(/[<>&]/g,'');
const used = b.in_use ? 'utilisé' : '';
return '