mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-21 05:58:13 +02:00
v0.5.6 : backends custom depuis un depot Git + editeur de preset repense (champs facon Reglages iOS) + incompatibilite modele/moteur signalee
This commit is contained in:
1 parent
1af9262e97
commit
83a3a79abe
14 files changed
+1158
-119
No files matched your search
@@ -28,6 +28,7 @@
|
||||
</div>
|
||||
<!-- Avertissement de lancement (App Translocation macOS) — masqué par défaut. -->
|
||||
<div id="app-warn" class="muted" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:8px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
||||
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:8px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
||||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état.
|
||||
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
|
||||
<div id="svc-log-box" style="display:none;margin:0 0 10px">
|
||||
@@ -174,6 +175,11 @@
|
||||
<div class="lc-mode-d">compilée pour votre machine, plus longue</div>
|
||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||
</div>
|
||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||
<div class="lc-mode-t">🧩 Backend personnalisé</div>
|
||||
<div class="lc-mode-d">forks llama.cpp pour modèles spéciaux (ternaire…)</div>
|
||||
<div class="lc-mode-state" id="lc-custom-state"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lc-job" style="display:none;margin-top:10px">
|
||||
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
|
||||
@@ -244,6 +250,36 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()">
|
||||
<div class="modal-card" style="max-width:560px">
|
||||
<div class="modal-head">
|
||||
<strong>🧩 Backends personnalisés</strong>
|
||||
<button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="modal-body mform">
|
||||
<div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton ✎ → section Moteur → « backend détecté »).</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l">Installés</label>
|
||||
<div id="lc-custom-list" class="mcp-list"></div>
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="lc-custom-url">Installer depuis un dépôt Git</label>
|
||||
<input id="lc-custom-url" class="ctl" placeholder="https://github.com/owner/llama.cpp-fork">
|
||||
</div>
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="lc-custom-name">Nom <span class="opt">(optionnel — déduit de l'URL sinon)</span></label>
|
||||
<input id="lc-custom-name" class="ctl" placeholder="llama.cpp-prism">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="foot-l"></div>
|
||||
<div class="foot-r">
|
||||
<button onclick="closeCustomBackends()">fermer</button>
|
||||
<button class="btn-save" onclick="lcInstallCustom()">Installer & compiler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeMcp()">
|
||||
<div class="modal-card" style="max-width:560px">
|
||||
<div class="modal-head">
|
||||
@@ -309,62 +345,131 @@
|
||||
<strong id="modal-title">Preset</strong>
|
||||
<button class="modal-x" onclick="closeModal()" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="modal-body mform">
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="m-name">Nom <span class="opt">(espaces et accents OK, sauf / et \)</span></label>
|
||||
<input id="m-name" class="ctl">
|
||||
</div>
|
||||
|
||||
<div id="m-model-row" class="card" style="display:none">
|
||||
<div class="card-h">Modèle & moteur</div>
|
||||
|
||||
<div class="fld">
|
||||
<label class="fld-l">Moteur <span class="opt">— quel llama.cpp fait tourner ce modèle</span></label>
|
||||
<div class="seg">
|
||||
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>⚡ Rapide</span><span class="be-note" id="be-fast-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>🔧 Optimisé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>📁 Perso</span></label>
|
||||
</div>
|
||||
<div id="m-backend-custom" style="display:none;margin-top:8px">
|
||||
<input id="m-backend-path" class="ctl" oninput="onCustomPath()" placeholder="chemin vers llama-server(.exe)">
|
||||
<select id="m-backend-detected" class="ctl" onchange="onPickDetected()" style="margin-top:6px"></select>
|
||||
<div class="fld-hint" id="be-drop-hint"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="m-model">Modèle <span class="opt">(remplace la ligne MODEL=)</span></label>
|
||||
<select id="m-model" class="ctl" onchange="onPickModel()"></select>
|
||||
</div>
|
||||
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="m-hf-url">Télécharger un modèle <span class="opt">(lien direct vers un .gguf)</span></label>
|
||||
<div class="input-btn">
|
||||
<input id="m-hf-url" class="ctl" placeholder="https://…/model.gguf">
|
||||
<button id="m-hf-btn" class="btn-in" onclick="startDownload()" title="Télécharger">⬇</button>
|
||||
</div>
|
||||
<div id="m-hf-progress" class="fld-hint" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="m-quant">Quantization <span class="opt">(vide = détection auto depuis le nom)</span></label>
|
||||
<input id="m-quant" class="ctl" oninput="applyQuant()" placeholder="ex: Q4_K_M, IQ3_XXS, BF16…">
|
||||
<div class="pe-body">
|
||||
<div class="pe-group">
|
||||
<div class="pe-gh">Nom</div>
|
||||
<div class="pe-list">
|
||||
<div class="pe-row stack plain"><input id="m-name" class="pe-inp" placeholder="Nom du preset"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fld">
|
||||
<label class="fld-l" for="m-content">Contenu <span class="opt">(fichier de config brut)</span></label>
|
||||
<textarea id="m-content" class="ctl ta" spellcheck="false"></textarea>
|
||||
<div id="m-model-row" style="display:none;flex-direction:column;gap:26px">
|
||||
<div class="pe-group">
|
||||
<div class="pe-gh">Modèle</div>
|
||||
<div class="pe-list">
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Fichier</span>
|
||||
<span class="pe-row-c"><span class="pe-selc"><select id="m-model" onchange="onPickModel()"></select></span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Quantization<span class="pe-sub">auto si vide</span></span>
|
||||
<span class="pe-row-c"><input id="m-quant" class="pe-val" oninput="applyQuant()" placeholder="Auto"></span>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">lien direct vers un .gguf</span></span>
|
||||
<div class="pe-inpbtn">
|
||||
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
|
||||
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">⬇</button>
|
||||
</div>
|
||||
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pe-group">
|
||||
<div class="pe-gh">Moteur</div>
|
||||
<div class="pe-seg">
|
||||
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Rapide</span><span class="be-note" id="be-fast-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Optimisé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||
</div>
|
||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Backend</span>
|
||||
<span class="pe-row-c"><span class="pe-selc"><select id="m-backend-detected" onchange="onPickDetected()"></select></span></span>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<input id="m-backend-path" class="pe-inp" oninput="onCustomPath()" placeholder="ou un chemin vers llama-server(.exe)">
|
||||
<div class="pe-note" id="be-drop-hint"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="m-settings-row" class="pe-group" style="display:none">
|
||||
<div class="pe-gh">Réglages</div>
|
||||
<div class="pe-list">
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Contexte</span>
|
||||
<span class="pe-row-c"><input id="s-ctx" class="pe-val" type="number" min="0" step="1024" placeholder="32768" oninput="cfgWriteKey('CTX', this.value)"><span class="pe-unit">tok</span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Couches sur GPU<span class="pe-sub">999 = tout sur le GPU</span></span>
|
||||
<span class="pe-row-c"><input id="s-ngl" class="pe-val" type="number" min="0" step="1" placeholder="999" oninput="cfgWriteKey('NGL', this.value)"></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Threads CPU<span class="pe-sub">0 = auto</span></span>
|
||||
<span class="pe-row-c"><input id="s-threads" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS', this.value)"></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Cache KV<span class="pe-sub">compression du contexte</span></span>
|
||||
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value)">
|
||||
<option value="">f16 (max qualité)</option>
|
||||
<option value="q8_0">q8_0 (léger)</option>
|
||||
<option value="q4_0">q4_0 (très léger)</option>
|
||||
<option value="q5_1">q5_1</option>
|
||||
<option value="bf16">bf16</option>
|
||||
</select></span></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Raisonnement<span class="pe-sub">réflexion étape par étape</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Flash attention<span class="pe-sub">plus rapide, moins de VRAM</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-flash" onchange="eaSetValued('--flash-attn', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Garder en RAM<span class="pe-sub">évite le swap (mlock)</span></span>
|
||||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-mlock" onchange="eaToggleFlag('--mlock', this.checked)"><span class="pe-sl"></span></label></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="pe-adv" style="margin-top:8px">
|
||||
<summary>Réglages avancés</summary>
|
||||
<div class="pe-adv-body" style="padding:0">
|
||||
<div class="pe-list" style="border:none;border-radius:0">
|
||||
<div class="pe-row"><span class="pe-row-l">BATCH</span><span class="pe-row-c"><input id="s-batch" class="pe-val" type="number" min="0" step="256" placeholder="2048" oninput="cfgWriteKey('BATCH', this.value)"></span></div>
|
||||
<div class="pe-row"><span class="pe-row-l">UBATCH</span><span class="pe-row-c"><input id="s-ubatch" class="pe-val" type="number" min="0" step="128" placeholder="512" oninput="cfgWriteKey('UBATCH', this.value)"></span></div>
|
||||
<div class="pe-row"><span class="pe-row-l">Threads (batch)<span class="pe-sub">0 = auto</span></span><span class="pe-row-c"><input id="s-tbatch" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS_BATCH', this.value)"></span></div>
|
||||
<div class="pe-row"><span class="pe-row-l">Experts MoE sur CPU<span class="pe-sub">vide = non</span></span><span class="pe-row-c"><input id="s-moe" class="pe-val" type="number" min="0" step="1" placeholder="—" oninput="eaSetValued('--n-cpu-moe', this.value)"></span></div>
|
||||
<div class="pe-row"><span class="pe-row-l">Charger tout en mémoire<span class="pe-sub">désactive le mmap</span></span><span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-nommap" onchange="eaToggleFlag('--no-mmap', this.checked)"><span class="pe-sl"></span></label></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div id="m-raw" class="pe-group">
|
||||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||||
<div class="pe-list">
|
||||
<div id="m-raw-toggle" class="pe-row" style="cursor:pointer" onclick="toggleRaw()">
|
||||
<span class="pe-row-l">Éditer le fichier <code>.env</code><span class="pe-sub">configuration brute — pour les réglages avancés</span></span>
|
||||
<span class="pe-row-c"><span id="m-raw-caret" class="pe-caret"></span></span>
|
||||
</div>
|
||||
<div id="m-raw-body" class="pe-row stack" style="display:none">
|
||||
<textarea id="m-content" class="pe-inp" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="pe-foot">
|
||||
<div class="foot-l">
|
||||
<button id="m-del" class="btn-danger" onclick="delItem()">supprimer</button>
|
||||
<label id="m-del-model-wrap" class="del-model" style="display:none"><input type="checkbox" id="m-del-model"> + le modèle .gguf</label>
|
||||
<button id="m-del" class="pe-btn ghost-danger" onclick="delItem()">supprimer</button>
|
||||
<label id="m-del-model-wrap" class="pe-delmodel" style="display:none"><input type="checkbox" id="m-del-model"> + le modèle .gguf</label>
|
||||
</div>
|
||||
<div class="foot-r">
|
||||
<button onclick="closeModal()">annuler</button>
|
||||
<button class="btn-save" onclick="saveItem()">enregistrer</button>
|
||||
<button class="pe-btn" onclick="closeModal()">annuler</button>
|
||||
<button class="pe-btn primary" onclick="saveItem()">enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ async function loadStatus(){
|
||||
// chargé (loading, llama-server renvoie 503) · modèle prêt (ok).
|
||||
let cls='err', txt=s.state;
|
||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||
else if(s.load_error){ cls='err'; txt='modèle incompatible'; }
|
||||
else if(s.active){ cls='loading'; txt='chargement…'; }
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt+' <span class="port">:'+s.port+'</span>';
|
||||
@@ -20,6 +21,13 @@ async function loadStatus(){
|
||||
if(s.warn){ wb.textContent='⚠ '+s.warn; wb.style.display=''; }
|
||||
else { wb.style.display='none'; }
|
||||
}
|
||||
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
||||
// plutôt qu'un « chargement… » perpétuel ou un crash-loop muet.
|
||||
const me=document.getElementById('model-err');
|
||||
if(me){
|
||||
if(s.load_error){ me.textContent='⚠ '+s.load_error; me.style.display=''; }
|
||||
else { me.style.display='none'; }
|
||||
}
|
||||
}
|
||||
// Journal du moteur — replié par défaut, on l'ouvre en cliquant la pastille.
|
||||
function toggleSvcLog(){
|
||||
|
||||
@@ -68,18 +68,35 @@ async function openItem(kind, key){
|
||||
document.getElementById('m-del').style.display = key ? 'inline-block' : 'none';
|
||||
// Model picker is preset-only: it edits the MODEL= line of config.env.
|
||||
const modelRow = document.getElementById('m-model-row');
|
||||
const settingsRow = document.getElementById('m-settings-row');
|
||||
const delModelWrap = document.getElementById('m-del-model-wrap');
|
||||
const rawHead = document.getElementById('m-raw-head');
|
||||
const rawToggle = document.getElementById('m-raw-toggle');
|
||||
const rawBody = document.getElementById('m-raw-body');
|
||||
const rawCaret = document.getElementById('m-raw-caret');
|
||||
if(kind === 'preset'){
|
||||
modelRow.style.display = 'flex';
|
||||
settingsRow.style.display = 'flex';
|
||||
document.getElementById('m-hf-url').value = '';
|
||||
document.getElementById('m-hf-progress').style.display = 'none';
|
||||
document.getElementById('m-del-model').checked = false;
|
||||
document.getElementById('m-quant').value = currentQuantInTextarea();
|
||||
populateSettings();
|
||||
delModelWrap.style.display = key ? 'inline-flex' : 'none';
|
||||
// Preset : la config brute est une ligne repliable, fermée par défaut.
|
||||
rawHead.textContent = 'Configuration';
|
||||
rawToggle.style.display = '';
|
||||
rawBody.style.display = 'none';
|
||||
rawCaret.classList.remove('open');
|
||||
await Promise.all([populateBackend(), populateModelPicker()]);
|
||||
} else {
|
||||
modelRow.style.display = 'none';
|
||||
settingsRow.style.display = 'none';
|
||||
delModelWrap.style.display = 'none';
|
||||
// Page mémoire : le contenu EST le champ principal — affiché en clair.
|
||||
rawHead.textContent = 'Contenu';
|
||||
rawToggle.style.display = 'none';
|
||||
rawBody.style.display = '';
|
||||
}
|
||||
document.getElementById('modal').style.display = 'flex';
|
||||
}
|
||||
@@ -154,8 +171,6 @@ async function populateBackend(){
|
||||
let lc = {}; try{ lc = await jget('/api/llamacpp'); }catch(_){}
|
||||
beFastPath = (lc.prebuilt && lc.prebuilt.bin) || '';
|
||||
beOptPath = lc.bin || '';
|
||||
document.getElementById('be-fast-note').textContent = beFastPath ? '' : 'non installée';
|
||||
document.getElementById('be-opt-note').textContent = beOptPath ? '' : 'non installée';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de jean (l'utilisateur peut y déposer son propre build).
|
||||
const detected = await jget('/api/backends');
|
||||
@@ -164,7 +179,7 @@ async function populateBackend(){
|
||||
for(const b of (detected||[])) html += '<option value="'+b.path+'">'+b.name+'</option>';
|
||||
sel.innerHTML = html;
|
||||
document.getElementById('be-drop-hint').textContent = lc.backends_dir
|
||||
? ('Astuce : dépose ton binaire dans un sous-dossier de '+lc.backends_dir+' pour le voir apparaître ci-dessus.') : '';
|
||||
? ('Astuce : déposez votre binaire dans un sous-dossier de '+lc.backends_dir+' pour le voir apparaître ci-dessus.') : '';
|
||||
// Sélectionne l'option correspondant au BIN actuel du preset.
|
||||
const cur = currentBinInTextarea();
|
||||
let mode = 'custom';
|
||||
@@ -181,10 +196,10 @@ function toggleBackendCustom(mode){
|
||||
function onBackendMode(mode){
|
||||
toggleBackendCustom(mode);
|
||||
if(mode === 'fast'){
|
||||
if(!beFastPath){ toast('installe d\'abord la version rapide (section Backend llama.cpp)'); return; }
|
||||
if(!beFastPath){ toast('installez d\'abord la version rapide (section MOTEUR)'); return; }
|
||||
setBinInTextarea(beFastPath); toast('moteur : rapide');
|
||||
} else if(mode === 'opt'){
|
||||
if(!beOptPath){ toast('installe d\'abord la version optimisée (section Backend llama.cpp)'); return; }
|
||||
if(!beOptPath){ toast('installez d\'abord la version optimisée (section MOTEUR)'); return; }
|
||||
setBinInTextarea(beOptPath); toast('moteur : optimisé');
|
||||
}
|
||||
// custom : on attend que l'utilisateur saisisse un chemin / choisisse un backend
|
||||
@@ -215,6 +230,72 @@ function applyQuant(){
|
||||
ta.value = ta.value.replace(re, '').replace(/\n{3,}/g,'\n\n');
|
||||
}
|
||||
}
|
||||
// ---- Réglages simples : champs de formulaire <-> lignes du fichier .env -----
|
||||
// Le fichier de config (textarea m-content) reste la source de vérité : chaque
|
||||
// champ lit/écrit sa ligne KEY=… , comme le font déjà MODEL/BIN/QUANT.
|
||||
function cfgReadKey(key){
|
||||
const m = document.getElementById('m-content').value.match(new RegExp('^[ \\t]*'+key+'[ \\t]*=[ \\t]*"?([^"\\n]*)"?[ \\t]*$','m'));
|
||||
return m ? m[1].trim() : '';
|
||||
}
|
||||
function cfgWriteKey(key, val){
|
||||
const ta = document.getElementById('m-content');
|
||||
val = String(val==null?'':val).trim();
|
||||
const reLine = new RegExp('^[ \\t]*'+key+'[ \\t]*=.*$','m');
|
||||
if(val === ''){ // clé vidée → on retire la ligne
|
||||
ta.value = ta.value.replace(new RegExp('^[ \\t]*'+key+'[ \\t]*=.*\\n?','m'),'').replace(/\n{3,}/g,'\n\n');
|
||||
return;
|
||||
}
|
||||
const line = key+'='+(/\s/.test(val) ? '"'+val+'"' : val);
|
||||
if(reLine.test(ta.value)) ta.value = ta.value.replace(reLine, line);
|
||||
else ta.value = ta.value.replace(/\s*$/,'') + '\n'+line+'\n';
|
||||
}
|
||||
// EXTRA_ARGS : liste de drapeaux passés tels quels à llama-server. On les édite
|
||||
// jeton par jeton pour les interrupteurs (mlock, flash-attn, n-cpu-moe…), sans
|
||||
// toucher aux autres drapeaux (--jinja, --device…) qui restent en config brute.
|
||||
function eaTokens(){ return cfgReadKey('EXTRA_ARGS').split(/\s+/).filter(Boolean); }
|
||||
function eaSetTokens(t){ cfgWriteKey('EXTRA_ARGS', t.join(' ')); }
|
||||
function eaHasFlag(flag){ return eaTokens().includes(flag); }
|
||||
function eaToggleFlag(flag, on){
|
||||
const t = eaTokens().filter(x=>x!==flag);
|
||||
if(on) t.push(flag);
|
||||
eaSetTokens(t);
|
||||
}
|
||||
function eaGetValued(flag){
|
||||
const t = eaTokens(), i = t.indexOf(flag);
|
||||
return (i>=0 && i+1<t.length && !t[i+1].startsWith('-')) ? t[i+1] : '';
|
||||
}
|
||||
function eaSetValued(flag, val){
|
||||
const t = eaTokens(), i = t.indexOf(flag);
|
||||
if(i>=0){ const hadVal = i+1<t.length && !t[i+1].startsWith('-'); t.splice(i, hadVal?2:1); }
|
||||
val = String(val||'').trim();
|
||||
if(val !== '') t.push(flag, val);
|
||||
eaSetTokens(t);
|
||||
}
|
||||
// Remplit les champs de réglage depuis le contenu courant du preset.
|
||||
function populateSettings(){
|
||||
const set = (id,v)=>{ const e=document.getElementById(id); if(e) e.value=v; };
|
||||
set('s-ctx', cfgReadKey('CTX'));
|
||||
set('s-ngl', cfgReadKey('NGL'));
|
||||
set('s-threads', cfgReadKey('THREADS'));
|
||||
set('s-batch', cfgReadKey('BATCH'));
|
||||
set('s-ubatch', cfgReadKey('UBATCH'));
|
||||
set('s-tbatch', cfgReadKey('THREADS_BATCH'));
|
||||
set('s-kv', cfgReadKey('KV_TYPE'));
|
||||
set('s-moe', eaGetValued('--n-cpu-moe'));
|
||||
const chk = (id,v)=>{ const e=document.getElementById(id); if(e) e.checked=v; };
|
||||
chk('s-reasoning', /^(on|1|true|auto|deepseek)$/i.test(cfgReadKey('REASONING')));
|
||||
chk('s-flash', eaHasFlag('--flash-attn') && !/^off$/i.test(eaGetValued('--flash-attn')));
|
||||
chk('s-mlock', eaHasFlag('--mlock'));
|
||||
chk('s-nommap', eaHasFlag('--no-mmap'));
|
||||
}
|
||||
// Replie/déplie l'éditeur du fichier .env (config brute) dans le modal preset.
|
||||
function toggleRaw(){
|
||||
const b = document.getElementById('m-raw-body');
|
||||
const c = document.getElementById('m-raw-caret');
|
||||
const show = b.style.display === 'none';
|
||||
b.style.display = show ? '' : 'none';
|
||||
if(c) c.classList.toggle('open', show);
|
||||
}
|
||||
const openPreset = (id)=>openItem('preset', id);
|
||||
const openMem = (n)=>openItem('mem', n);
|
||||
function closeModal(){ document.getElementById('modal').style.display = 'none'; }
|
||||
|
||||
@@ -22,6 +22,7 @@ async function loadLlamacpp(){
|
||||
}
|
||||
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){
|
||||
@@ -99,6 +100,75 @@ async function lcUpdate(mode){
|
||||
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
|
||||
? '<span class="lc-mode-active-tag">✓ '+n+' installé'+(n>1?'s':'')+'</span><span class="lc-mode-go">→ gérer</span>'
|
||||
: '<span class="lc-mode-go">→ voir / installer</span>';
|
||||
}
|
||||
|
||||
function openCustomBackends(){
|
||||
document.getElementById('lc-custom-modal').style.display = 'flex';
|
||||
document.getElementById('lc-custom-url').value = '';
|
||||
document.getElementById('lc-custom-name').value = '';
|
||||
loadCustomBackends();
|
||||
}
|
||||
function closeCustomBackends(){ document.getElementById('lc-custom-modal').style.display = 'none'; }
|
||||
|
||||
// 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 = '<span class="muted" style="font-size:12px">chargement…</span>';
|
||||
let list = [];
|
||||
try{ list = await jget('/api/backends/custom') || []; }catch(_){}
|
||||
lcCustomBackends = list;
|
||||
if(!list.length){ box.innerHTML = '<span class="muted" style="font-size:12px">aucun backend personnalisé pour l\'instant.</span>'; return; }
|
||||
box.innerHTML = list.map(b=>{
|
||||
const nm = String(b.name).replace(/[<>&]/g,'');
|
||||
const used = b.in_use ? '<span class="mcp-tag" style="border-color:var(--accent);color:var(--accent)">utilisé</span>' : '';
|
||||
return '<div class="mcp-row" style="cursor:default">'
|
||||
+ '<span class="mcp-dot '+(b.in_use?'mcp-dot-ok':'mcp-dot-off')+'"></span>'
|
||||
+ '<div class="mcp-info"><div class="mcp-name">'+nm+'</div>'
|
||||
+ '<div class="mcp-meta">'+used+'<span class="mcp-tag" title="'+String(b.path).replace(/"/g,'"')+'">'+String(b.path).split('/').slice(-3).join('/').replace(/[<>&]/g,'')+'</span></div></div>'
|
||||
+ '<button class="btn-danger" style="padding:3px 8px;font-size:11px" onclick="lcUninstallCustom(\''+nm.replace(/'/g,"\\'")+'\')">supprimer</button>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Installer un backend CUSTOM depuis une URL de dépôt Git (fork llama.cpp).
|
||||
// Cloné + compilé dans backends/<nom>, SANS toucher au moteur global : il
|
||||
// apparaît ensuite dans le menu « backend détecté » de l'éditeur de modèle.
|
||||
async function lcInstallCustom(){
|
||||
const url = (document.getElementById('lc-custom-url').value||'').trim();
|
||||
if(!url){ toast('collez l\'URL d\'un dépôt Git'); return; }
|
||||
const name = (document.getElementById('lc-custom-name').value||'').trim();
|
||||
if(!await askConfirm('Cloner et compiler ce backend depuis :\n'+url+'\n\nCela peut prendre de longues minutes (surtout avec une carte NVIDIA). Il ne remplace pas le moteur global — vous le choisirez par modèle.', {title:'Backend personnalisé', okText:'Installer'})) return;
|
||||
const r = await jpost('/api/llamacpp/install-custom', {repo:url, name});
|
||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||
closeCustomBackends();
|
||||
document.getElementById('lc-details').open = true;
|
||||
lcStartPolling();
|
||||
}
|
||||
|
||||
// Désinstaller (supprime le dossier backends/<name>). Le serveur refuse si le
|
||||
// backend sert de moteur au modèle actif.
|
||||
async function lcUninstallCustom(name){
|
||||
if(!await askConfirm('Supprimer le backend « '+name+' » ? Son dossier compilé sera effacé. Les modèles qui l\'utilisent devront être repointés sur un autre moteur.', {title:'Supprimer le backend', okText:'Supprimer', danger:true})) return;
|
||||
const r = await jpost('/api/llamacpp/uninstall-custom', {name});
|
||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||
toast('backend supprimé');
|
||||
loadCustomBackends();
|
||||
lcRenderCustomCard();
|
||||
}
|
||||
|
||||
// --- Progression de l'installation (téléchargement / compilation) ----------
|
||||
function lcBusy(on){ document.querySelector('.lc-modes').classList.toggle('busy', on); }
|
||||
|
||||
|
||||
@@ -292,6 +292,92 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.be-opt{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text);cursor:pointer}
|
||||
.be-opt input{margin:0;flex-shrink:0}
|
||||
.be-note{font-size:11px;color:var(--dim);margin-left:auto}
|
||||
|
||||
/* ===== Éditeur de preset — listes groupées façon Réglages iOS ======== */
|
||||
#modal .modal-card{max-width:520px;overflow:hidden}
|
||||
.pe-body{padding:18px 16px 24px;display:flex;flex-direction:column;gap:26px;overflow-y:auto;overflow-x:hidden;flex:1;background:var(--bg)}
|
||||
/* Nombres : pas de flèches natives (spinner), champ épuré. */
|
||||
.pe-val[type=number]{-moz-appearance:textfield}
|
||||
.pe-val::-webkit-outer-spin-button,.pe-val::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
|
||||
/* Un groupe = un petit titre + une liste encadrée aux coins arrondis. */
|
||||
.pe-group{display:flex;flex-direction:column;gap:8px}
|
||||
.pe-gh{font-size:12px;font-weight:600;color:var(--dim);padding:0 6px}
|
||||
.pe-list{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||||
/* Ligne : libellé à gauche, contrôle à droite, séparateur fin entre lignes. */
|
||||
.pe-row{display:flex;align-items:center;gap:14px;min-height:48px;padding:9px 15px}
|
||||
.pe-row:not(:last-child){border-bottom:1px solid var(--border)}
|
||||
.pe-row-l{font-size:14px;color:var(--text);flex:1;min-width:0;line-height:1.3}
|
||||
.pe-row-l .pe-sub{display:block;font-size:11px;color:var(--dim);margin-top:2px}
|
||||
.pe-row-c{flex:none;display:flex;align-items:center;gap:6px;max-width:62%;justify-content:flex-end}
|
||||
/* Valeur éditable sans bordure, alignée à droite (nombre / texte). */
|
||||
.pe-val{border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;padding:4px 0;width:110px;max-width:44vw}
|
||||
.pe-val::placeholder{color:var(--dim)}
|
||||
.pe-val:focus{outline:none}
|
||||
.pe-unit{font-size:12px;color:var(--dim)}
|
||||
/* Select sans bordure, avec chevron discret. */
|
||||
.pe-selc{position:relative;display:flex;align-items:center;max-width:100%}
|
||||
.pe-selc select{appearance:none;-webkit-appearance:none;border:none;background:transparent;color:var(--accent);font:inherit;text-align:right;text-align-last:right;padding:4px 16px 4px 0;cursor:pointer;max-width:100%;text-overflow:ellipsis}
|
||||
.pe-selc select:focus{outline:none}
|
||||
.pe-selc::after{content:"›";position:absolute;right:0;transform:rotate(90deg);color:var(--dim);font-size:14px;pointer-events:none}
|
||||
/* Ligne « champ pleine largeur » (Nom, chemin, config brute…). */
|
||||
.pe-row.stack{flex-direction:column;align-items:stretch;gap:8px}
|
||||
.pe-inp{width:100%;box-sizing:border-box;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:9px 11px;font:inherit;transition:border-color .12s,box-shadow .12s}
|
||||
.pe-inp:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg)}
|
||||
.pe-row.stack.plain .pe-inp{border:none;background:transparent;padding:6px 0;font-size:15px}
|
||||
.pe-row.stack.plain .pe-inp:focus{box-shadow:none}
|
||||
/* Champ + bouton accolé (téléchargement) — même trait que les autres champs,
|
||||
sans pastille colorée qui ressort. */
|
||||
.pe-inpbtn{display:flex}
|
||||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0}
|
||||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-left:0;border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s}
|
||||
.pe-inpbtn button:hover{color:var(--accent);border-color:var(--accent)}
|
||||
.pe-inpbtn button:disabled{opacity:.5;cursor:default}
|
||||
/* Contrôle segmenté (moteur) façon iOS. */
|
||||
.pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%}
|
||||
.pe-seg>label{flex:1;text-align:center;font-size:13px;font-weight:500;padding:7px 4px;border-radius:7px;cursor:pointer;color:var(--dim);transition:background .12s,color .12s}
|
||||
.pe-seg>label:has(input:checked){background:var(--panel);color:var(--text);box-shadow:0 1px 2px rgba(0,0,0,.22)}
|
||||
.pe-seg input{display:none}
|
||||
.pe-seg .be-note{display:none}
|
||||
.pe-seg-note{font-size:11px;color:var(--dim);padding:0 2px;min-height:14px}
|
||||
.pe-note{font-size:11px;color:var(--dim);line-height:1.45}
|
||||
/* Interrupteur à glissière. */
|
||||
.pe-switch{position:relative;flex:none;width:40px;height:24px}
|
||||
.pe-switch input{position:absolute;opacity:0;width:0;height:0}
|
||||
.pe-sl{position:absolute;inset:0;background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:999px;transition:background .18s}
|
||||
.pe-sl::before{content:"";position:absolute;width:18px;height:18px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .18s;box-shadow:0 1px 3px rgba(0,0,0,.35)}
|
||||
.pe-switch input:checked+.pe-sl{background:var(--accent)}
|
||||
.pe-switch input:checked+.pe-sl::before{transform:translateX(16px)}
|
||||
.pe-switch input:focus-visible+.pe-sl{box-shadow:0 0 0 3px var(--accent-bg)}
|
||||
/* Repli « avancé » / config brute : une ligne cliquable dans un groupe. */
|
||||
.pe-adv{background:var(--panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||||
.pe-adv>summary{cursor:pointer;font-size:14px;color:var(--text);padding:13px 15px;list-style:none;display:flex;align-items:center;user-select:none}
|
||||
.pe-adv>summary::-webkit-details-marker{display:none}
|
||||
.pe-adv>summary::after{content:"";margin-left:auto;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:translateY(-2px) rotate(45deg);transition:transform .15s}
|
||||
.pe-adv[open]>summary::after{transform:translateY(1px) rotate(-135deg)}
|
||||
.pe-adv[open]>summary{border-bottom:1px solid var(--border)}
|
||||
.pe-adv .pe-adv-body{padding:14px 15px;display:flex;flex-direction:column;gap:10px}
|
||||
.pe-adv textarea.pe-inp{min-height:210px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
|
||||
/* Ligne « éditer le .env » : chevron de disclosure + textarea de config brute. */
|
||||
.pe-caret{display:inline-block;width:7px;height:7px;border-right:1.5px solid var(--dim);border-bottom:1.5px solid var(--dim);transform:rotate(-45deg);transition:transform .15s}
|
||||
.pe-caret.open{transform:rotate(45deg)}
|
||||
#m-content{min-height:220px;resize:vertical;line-height:1.5;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;overflow-x:hidden}
|
||||
/* Nombres avancés : chaque paire label/valeur sur sa propre ligne (réutilise pe-row). */
|
||||
.pe-adv .pe-list{margin:0}
|
||||
/* Variante « champ en clair » : page mémoire → le contenu est le champ principal. */
|
||||
.pe-adv.pe-raw-plain{border:none;background:transparent}
|
||||
.pe-adv.pe-raw-plain>summary{display:none}
|
||||
.pe-adv.pe-raw-plain .pe-adv-body{padding:0}
|
||||
/* Pied de modale. */
|
||||
.pe-foot{padding:13px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.pe-foot .foot-r{display:flex;gap:8px}
|
||||
.pe-btn{margin:0;padding:9px 18px;border-radius:9px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--panel);color:var(--text)}
|
||||
.pe-btn:hover{border-color:var(--dim)}
|
||||
.pe-btn.primary{border-color:var(--accent);background:var(--accent);color:#fff}
|
||||
.pe-btn.primary:hover{filter:brightness(1.08)}
|
||||
.pe-btn.ghost-danger{border-color:transparent;background:transparent;color:var(--err);padding:9px 10px}
|
||||
.pe-btn.ghost-danger:hover{background:var(--danger-bg)}
|
||||
.pe-delmodel{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--dim);cursor:pointer}
|
||||
.pe-delmodel input{margin:0}
|
||||
/* Éditeur de preset/mémoire — mise en forme propre du modale */
|
||||
.modal-ov{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:30;align-items:center;justify-content:center;padding:20px}
|
||||
.modal-card{background:var(--bg);border:1px solid var(--border);border-radius:12px;width:100%;max-width:600px;max-height:90vh;display:flex;flex-direction:column;box-shadow:0 16px 50px rgba(0,0,0,.5)}
|
||||
|
||||
Reference in new issue
Block a user