mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +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
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 5,
|
||||
"Patch": 6,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 5,
|
||||
"Patch": 6,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "Jean",
|
||||
"ProductVersion": "0.5.5",
|
||||
"ProductVersion": "0.5.6",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -111,12 +111,18 @@ func llamacppInstall(args []string) error {
|
||||
ref := ""
|
||||
force := false
|
||||
noSwitch := false
|
||||
customURL := ""
|
||||
customName := ""
|
||||
for _, a := range args {
|
||||
switch {
|
||||
case strings.HasPrefix(a, "--dir="):
|
||||
repo = strings.TrimPrefix(a, "--dir=")
|
||||
case strings.HasPrefix(a, "--ref="):
|
||||
ref = strings.TrimPrefix(a, "--ref=")
|
||||
case strings.HasPrefix(a, "--repo="):
|
||||
customURL = strings.TrimPrefix(a, "--repo=")
|
||||
case strings.HasPrefix(a, "--name="):
|
||||
customName = strings.TrimPrefix(a, "--name=")
|
||||
case a == "--force":
|
||||
force = true
|
||||
case a == "--no-switch":
|
||||
@@ -126,6 +132,23 @@ func llamacppInstall(args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Backend CUSTOM : fork llama.cpp installé depuis une URL Git dans
|
||||
// backends/<nom>. On NE touche PAS au BIN global — un backend custom se
|
||||
// choisit par modèle (preset → section Moteur → « backend détecté »). C'est
|
||||
// exactement le cas d'un moteur ternaire type PrismML qui ne sert qu'à un
|
||||
// seul modèle : le rattacher globalement casserait les autres presets.
|
||||
if customURL != "" {
|
||||
bin, err := installCustomBackend(customURL, customName, ref, func(s string) {
|
||||
fmt.Printf("%s %s\n", cyan("▶"), s)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\n%s backend custom compilé : %s\n", green("✓"), bin)
|
||||
fmt.Printf("Pour l'utiliser : édite un modèle → section %s → « backend détecté » et choisis-le.\n", bold("Moteur"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := requireTools("git", "cmake"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -347,3 +370,153 @@ func llamacppStatus(args []string) error {
|
||||
// detectBuildPlan probes the machine and returns the CMake flags for the best
|
||||
// available accelerator. Order of preference: CUDA → ROCm/HIP → Metal (macOS)
|
||||
// → Vulkan → CPU.
|
||||
// (implémentation dans backend_build.go)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Backends custom (fork llama.cpp installé depuis une URL Git)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// installCustomBackend clone (ou met à jour) un fork de llama.cpp depuis `url`
|
||||
// dans backends/<name> et compile llama-server avec le plan détecté pour la
|
||||
// machine. Il NE touche PAS à BIN : un backend custom se choisit par modèle
|
||||
// (éditeur de preset → section Moteur → « backend détecté »). Renvoie le chemin
|
||||
// du binaire compilé. `phase` reçoit les étapes de haut niveau (clone, build…) ;
|
||||
// la sortie détaillée du build passe par le sink habituel (terminal en CLI,
|
||||
// job web sinon).
|
||||
func installCustomBackend(url, name, ref string, phase func(string)) (string, error) {
|
||||
if phase == nil {
|
||||
phase = func(string) {}
|
||||
}
|
||||
url = strings.TrimSpace(url)
|
||||
if url == "" {
|
||||
return "", fmt.Errorf("URL du dépôt vide")
|
||||
}
|
||||
if !looksLikeGitURL(url) {
|
||||
return "", fmt.Errorf("URL de dépôt invalide (attendu https://…, git@… ou ssh://…) : %s", url)
|
||||
}
|
||||
if strings.TrimSpace(name) == "" {
|
||||
name = deriveBackendName(url)
|
||||
}
|
||||
dir, err := backendDir(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
phase("vérification des outils (git, cmake, compilateur)…")
|
||||
if err := requireTools("git", "cmake"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ensureCompiler(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
ensureAccelerator() // best-effort : installe le toolkit GPU si une carte est détectée
|
||||
|
||||
if isDir(filepath.Join(dir, ".git")) {
|
||||
// Backend déjà cloné : on le met à jour plutôt que de re-cloner.
|
||||
phase("dépôt déjà présent — mise à jour…")
|
||||
_ = runStep("git fetch", dir, "git", "fetch", "origin", "--quiet")
|
||||
if ref != "" {
|
||||
if err := runStep("git checkout", dir, "git", "checkout", ref); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if branch := gitOutput(dir, "rev-parse", "--abbrev-ref", "HEAD"); branch != "" && branch != "HEAD" {
|
||||
_ = runStep("git pull --ff-only", dir, "git", "pull", "--ff-only", "origin", branch)
|
||||
}
|
||||
} else {
|
||||
if err := os.MkdirAll(filepath.Dir(dir), 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
phase("clone de " + url + "…")
|
||||
if err := runStep("git clone", "", "git", "clone", "--depth=1", url, dir); err != nil {
|
||||
return "", fmt.Errorf("git clone a échoué : %w", err)
|
||||
}
|
||||
if ref != "" {
|
||||
// --depth=1 ne récupère que HEAD ; on approfondit pour atteindre le ref.
|
||||
_ = runStep("git fetch", dir, "git", "fetch", "--unshallow", "origin")
|
||||
if err := runStep("git checkout", dir, "git", "checkout", ref); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plan := detectBuildPlan()
|
||||
phase(fmt.Sprintf("compilation (backend=%s)…", plan.backend))
|
||||
if err := buildLlamacpp(dir, plan, false); err != nil {
|
||||
return "", err
|
||||
}
|
||||
bin := llamaServerBin(dir)
|
||||
if bin == "" {
|
||||
return "", fmt.Errorf("build terminé mais binaire introuvable sous %s", filepath.Join(dir, "build"))
|
||||
}
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
func looksLikeGitURL(u string) bool {
|
||||
for _, p := range []string{"https://", "http://", "git@", "ssh://", "git://"} {
|
||||
if strings.HasPrefix(u, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// deriveBackendName construit un nom de dossier de backend à partir d'une URL de
|
||||
// dépôt. On garde le nom du dépôt, préfixé du propriétaire quand ça éviterait
|
||||
// une collision avec le backend canonique (backends/llama.cpp).
|
||||
func deriveBackendName(url string) string {
|
||||
s := strings.TrimSuffix(strings.TrimSpace(url), ".git")
|
||||
s = strings.TrimRight(s, "/")
|
||||
parts := strings.FieldsFunc(s, func(r rune) bool { return r == '/' || r == ':' })
|
||||
repo, owner := "", ""
|
||||
if n := len(parts); n > 0 {
|
||||
repo = parts[n-1]
|
||||
if n > 1 {
|
||||
owner = parts[n-2]
|
||||
}
|
||||
}
|
||||
name := repo
|
||||
if name == "" {
|
||||
name = "backend"
|
||||
}
|
||||
// Un fork nommé « llama.cpp » écraserait le backend optimisé canonique ; on
|
||||
// le distingue par le propriétaire (ex. llama.cpp-prismml-eng).
|
||||
if strings.EqualFold(name, "llama.cpp") && owner != "" {
|
||||
name = name + "-" + owner
|
||||
}
|
||||
return sanitizeBackendName(name)
|
||||
}
|
||||
|
||||
// sanitizeBackendName réduit un nom à un identifiant de dossier sûr
|
||||
// ([a-z0-9._-]), pour qu'il ne puisse jamais s'échapper de backends/.
|
||||
func sanitizeBackendName(name string) string {
|
||||
name = strings.ToLower(strings.TrimSpace(name))
|
||||
var b strings.Builder
|
||||
for _, r := range name {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9', r == '.', r == '_', r == '-':
|
||||
b.WriteRune(r)
|
||||
case r == ' ' || r == '/' || r == '\\':
|
||||
b.WriteByte('-')
|
||||
}
|
||||
}
|
||||
out := strings.Trim(b.String(), ".-")
|
||||
if out == "" {
|
||||
return "backend"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// backendDir renvoie backends/<name>, en refusant tout nom qui s'échapperait du
|
||||
// dossier backends (../, chemin absolu…).
|
||||
func backendDir(name string) (string, error) {
|
||||
name = sanitizeBackendName(name)
|
||||
root, err := filepath.Abs(filepath.Join(JeanHome(), "backends"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
p := filepath.Join(root, name)
|
||||
if !strings.HasPrefix(p, root+string(filepath.Separator)) {
|
||||
return "", fmt.Errorf("nom de backend invalide")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.5.5"
|
||||
const Version = "0.5.6"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
+403
-53
@@ -311,6 +311,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)}
|
||||
@@ -403,6 +489,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
</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">
|
||||
@@ -549,6 +636,11 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<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>
|
||||
@@ -619,6 +711,36 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
</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">
|
||||
@@ -684,62 +806,131 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<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>
|
||||
@@ -934,6 +1125,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>';
|
||||
@@ -949,6 +1141,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(){
|
||||
@@ -1356,18 +1555,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';
|
||||
}
|
||||
@@ -1442,8 +1658,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');
|
||||
@@ -1452,7 +1666,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';
|
||||
@@ -1469,10 +1683,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
|
||||
@@ -1503,6 +1717,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'; }
|
||||
@@ -1973,6 +2253,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){
|
||||
@@ -2050,6 +2331,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); }
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
+137
-9
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
@@ -34,17 +35,76 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx = n
|
||||
}
|
||||
}
|
||||
// Si le modèle n'est pas prêt, on regarde POURQUOI : une tentative de
|
||||
// chargement qui a échoué (souvent un modèle incompatible avec le moteur)
|
||||
// doit être signalée clairement plutôt que de laisser un « chargement… »
|
||||
// perpétuel / un crash-loop muet.
|
||||
loadErr := ""
|
||||
if !health {
|
||||
loadErr = modelLoadError()
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{
|
||||
"state": state,
|
||||
"active": active,
|
||||
"health": health,
|
||||
"port": LLMPort(),
|
||||
"ctx": ctx,
|
||||
"version": Version,
|
||||
"warn": appWarning(), // ex. App Translocation macOS — vide si tout va bien
|
||||
"state": state,
|
||||
"active": active,
|
||||
"health": health,
|
||||
"port": LLMPort(),
|
||||
"ctx": ctx,
|
||||
"version": Version,
|
||||
"warn": appWarning(), // ex. App Translocation macOS — vide si tout va bien
|
||||
"load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon
|
||||
})
|
||||
}
|
||||
|
||||
// modelLoadError inspecte la fin du journal du service et renvoie un message
|
||||
// clair quand la DERNIÈRE tentative de chargement du modèle s'est soldée par un
|
||||
// échec. Le cas le plus courant : un GGUF dont le format de quantification n'est
|
||||
// pas reconnu par le moteur (BIN) sélectionné — typiquement un modèle ternaire
|
||||
// qui exige un fork de llama.cpp. Renvoie "" quand le dernier chargement a
|
||||
// réussi ou qu'aucune tentative n'est visible (évite les faux positifs sur un
|
||||
// service simplement arrêté).
|
||||
func modelLoadError() string {
|
||||
log := serviceLogTail(200)
|
||||
if log == "" {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(log, "\n")
|
||||
// On ne considère que ce qui suit la DERNIÈRE tentative de chargement.
|
||||
start := 0
|
||||
for i, l := range lines {
|
||||
if strings.Contains(l, "loading model") || strings.Contains(l, "load_model") {
|
||||
start = i
|
||||
}
|
||||
}
|
||||
loaded, reason := false, ""
|
||||
for _, l := range lines[start:] {
|
||||
low := strings.ToLower(l)
|
||||
switch {
|
||||
case strings.Contains(low, "model loaded"), strings.Contains(low, "server is listening"):
|
||||
loaded = true
|
||||
case strings.Contains(l, "has offset") && strings.Contains(l, "expected"):
|
||||
reason = "format de quantification non reconnu par ce moteur"
|
||||
case strings.Contains(low, "unknown model architecture"),
|
||||
strings.Contains(low, "unknown ftype"),
|
||||
strings.Contains(low, "unknown type"),
|
||||
strings.Contains(low, "unsupported"):
|
||||
if reason == "" {
|
||||
reason = "type/architecture de modèle inconnu de ce moteur"
|
||||
}
|
||||
case strings.Contains(low, "error loading model"),
|
||||
strings.Contains(low, "failed to load model"),
|
||||
strings.Contains(low, "failed to read tensor data"):
|
||||
if reason == "" {
|
||||
reason = "échec du chargement du modèle"
|
||||
}
|
||||
}
|
||||
}
|
||||
if loaded || reason == "" {
|
||||
return ""
|
||||
}
|
||||
return "Le modèle n'a pas pu être chargé : " + reason +
|
||||
". Il est probablement incompatible avec le moteur sélectionné — choisis un autre backend pour ce modèle (édite le modèle → section Moteur)."
|
||||
}
|
||||
|
||||
// handleServiceLog (GET /api/service/log) : dernières lignes du journal du
|
||||
// service, pour que l'UI puisse montrer POURQUOI le modèle ne se charge pas
|
||||
// (fichier .gguf illisible, VRAM insuffisante, bibliothèque manquante…) au lieu
|
||||
@@ -102,11 +162,17 @@ func handleConfigEnv(w http.ResponseWriter, r *http.Request) {
|
||||
// trying common build subpaths (build/bin, build-sm120/bin, bin, .).
|
||||
// Returns [{name, path}].
|
||||
func handleBackends(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, listBackendBins())
|
||||
}
|
||||
|
||||
// listBackendBins scanne JEAN_HOME/backends/<name>/ pour un binaire llama-server
|
||||
// (essaie les sous-chemins des différents générateurs CMake). Renvoie
|
||||
// [{name, path}] — un par dossier de backend contenant un binaire.
|
||||
func listBackendBins() []map[string]any {
|
||||
root := JeanHome() + "/backends"
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
sendJSON(w, 200, []map[string]any{})
|
||||
return
|
||||
return []map[string]any{}
|
||||
}
|
||||
subpaths := []string{
|
||||
"build/bin/llama-server", "build-sm120/bin/llama-server",
|
||||
@@ -130,9 +196,71 @@ func handleBackends(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// handleBackendsCustom liste les seuls backends CUSTOM : les dossiers de
|
||||
// backends/ hors moteurs gérés par les cartes ⚡ (prebuilt) et 🔧 (build
|
||||
// canonique). Marque celui qui sert de moteur au modèle actif (in_use).
|
||||
func handleBackendsCustom(w http.ResponseWriter, r *http.Request) {
|
||||
canonical := filepath.Base(defaultRepoDir()) // "llama.cpp"
|
||||
prebuilt := filepath.Base(prebuiltDir()) // "llama.cpp-prebuilt"
|
||||
cfgBin := ReadConfig()["BIN"]
|
||||
out := []map[string]any{}
|
||||
for _, b := range listBackendBins() {
|
||||
name, _ := b["name"].(string)
|
||||
if name == canonical || name == prebuilt {
|
||||
continue
|
||||
}
|
||||
path, _ := b["path"].(string)
|
||||
out = append(out, map[string]any{"name": name, "path": path, "in_use": samePath(path, cfgBin)})
|
||||
}
|
||||
sendJSON(w, 200, out)
|
||||
}
|
||||
|
||||
// handleLlamacppUninstallCustom supprime le dossier d'un backend custom
|
||||
// (backends/<name>). Refuse les moteurs gérés (canonique / prebuilt) et un
|
||||
// backend actuellement utilisé par le modèle actif (il faut d'abord basculer de
|
||||
// moteur pour ce modèle).
|
||||
func handleLlamacppUninstallCustom(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
name := sanitizeBackendName(req.Name)
|
||||
if name == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "nom requis"})
|
||||
return
|
||||
}
|
||||
if name == filepath.Base(defaultRepoDir()) || name == filepath.Base(prebuiltDir()) {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "ce moteur se gère depuis les cartes ⚡ / 🔧"})
|
||||
return
|
||||
}
|
||||
dir, err := backendDir(name)
|
||||
if err != nil {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !isDir(dir) {
|
||||
sendJSON(w, 404, map[string]any{"ok": false, "error": "backend introuvable"})
|
||||
return
|
||||
}
|
||||
cfgBin := ReadConfig()["BIN"]
|
||||
for _, b := range listBackendBins() {
|
||||
if b["name"] == name {
|
||||
if p, _ := b["path"].(string); samePath(p, cfgBin) {
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "error": "backend utilisé par le modèle actif — bascule d'abord ce modèle sur un autre moteur"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// handleModels lists *.gguf files in JEAN_HOME (size in bytes) for the preset
|
||||
// editor's model picker.
|
||||
func handleModels(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -233,6 +233,41 @@ func handleLlamacppInstall(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// handleLlamacppInstallCustom lance un job d'installation d'un backend CUSTOM
|
||||
// (fork llama.cpp) depuis une URL de dépôt Git : cloné dans backends/<name> et
|
||||
// compilé pour la machine, SANS toucher au BIN global. Il apparaît ensuite dans
|
||||
// /api/backends et se choisit par modèle (éditeur de preset → Moteur).
|
||||
func handleLlamacppInstallCustom(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Repo string `json:"repo"`
|
||||
Name string `json:"name"`
|
||||
Ref string `json:"ref"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if strings.TrimSpace(req.Repo) == "" {
|
||||
sendJSON(w, 400, map[string]any{"ok": false, "error": "URL du dépôt requise"})
|
||||
return
|
||||
}
|
||||
if err := startLcJob("custom", func() { lcRunCustomInstall(req.Repo, req.Name, req.Ref) }); err != nil {
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// lcRunCustomInstall : corps du job d'install d'un backend custom (sans bascule
|
||||
// de BIN — c'est volontaire, il se rattache par preset).
|
||||
func lcRunCustomInstall(url, name, ref string) {
|
||||
bin, err := installCustomBackend(url, name, ref, lcPhase)
|
||||
if err != nil {
|
||||
lcFail(err)
|
||||
return
|
||||
}
|
||||
lcAppend("binaire compilé : " + bin)
|
||||
lcAppend("→ pour l'utiliser : édite un modèle → section Moteur → « backend détecté » et choisis-le.")
|
||||
lcDone("backend custom installé")
|
||||
}
|
||||
|
||||
// handleLlamacppUpdate lance le job de mise à jour (pull + rebuild + restart).
|
||||
// {clean:true} force une recompilation from scratch même sans nouveau commit.
|
||||
func handleLlamacppUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -90,9 +90,12 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/models/download", handleModelDownload)
|
||||
api("/api/models/download/status", handleModelDownloadStatus)
|
||||
api("/api/backends", handleBackends)
|
||||
api("/api/backends/custom", handleBackendsCustom) // backends custom uniquement (hors ⚡/🔧)
|
||||
api("/api/llamacpp", handleLlamacpp) // statut du backend llama.cpp
|
||||
api("/api/llamacpp/check", handleLlamacppCheck) // git fetch + retard sur origin
|
||||
api("/api/llamacpp/install", handleLlamacppInstall) // job : clone + build + BIN
|
||||
api("/api/llamacpp/install-custom", handleLlamacppInstallCustom) // job : clone d'un fork depuis une URL Git (par preset, sans BIN global)
|
||||
api("/api/llamacpp/uninstall-custom", handleLlamacppUninstallCustom) // supprime un backend custom (backends/<name>)
|
||||
api("/api/llamacpp/update", handleLlamacppUpdate) // job : pull + rebuild + restart
|
||||
api("/api/llamacpp/job", handleLlamacppJob) // progression + logs du job
|
||||
api("/api/llamacpp/prebuilt", handleLlamacppPrebuilt) // job : binaires officiels précompilés
|
||||
|
||||
Reference in new issue
Block a user