Files
Loki/internal/loki/ui/src/index.tmpl.html
T
MichaelandClaude Opus 5.5 986a636c20 Placement : liaison PCIe des cartes et guide d'ordre dans l'éditeur, en conseil
Deux cartes inégales (5060 Ti + 3060) se placent par deux règles du moteur
qui peuvent s'opposer : --fit remplit d'abord la DERNIÈRE carte, qui porte
la couche de sortie, et les experts MoE en RAM sont recopiés au prefill vers
la PREMIÈRE. Pour choisir l'ordre il faut voir la liaison de chaque carte ;
l'éditeur n'en montrait rien.

- detectGPUs lit aussi la liaison PCIe (génération et largeur, actuelles et
  maximales), le bus et l'horloge mémoire max, dans la même invocation de
  nvidia-smi. « [N/A] » vaut zéro ; un pilote qui refuse un champ fait
  retomber sur la requête historique (jamais de GPU perdu), un délai dépassé
  n'est pas relancé. Borné à 10 s. La largeur du bus mémoire n'existe pas
  dans nvidia-smi : absente.
- /api/backends/devices : une seule lecture nvidia-smi par énumération
  complète mémoire manquante ET liaison (annotateDevices, pure), par nom de
  carte, CUDA seulement, rien pour deux cartes homonymes. La lecture des
  jauges (gpuStatsCached) n'est pas touchée.
- Éditeur : liaison max par carte (l'actuelle et l'horloge en info-bulle),
  guide de placement dans l'ordre du moteur (--device compris), marges
  FIT_TARGET carte par carte, signalement d'un --tensor-split ou d'un
  CUDA_VISIBLE_DEVICES propre au preset, lien « inverser l'ordre » ; l'ordre
  choisi survit aux cases cochées.
- Pas de bouton de mesure : comparer deux ordres demande deux rechargements
  et un ordre inversé peut manquer de VRAM — le guide explique la marche à
  suivre (copie du preset, bench complet sur chacun).
- loki gpu affiche la liaison.

Rien ne change dans la ligne de commande du moteur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:03:02 +02:00

1412 lines
108 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<!-- GÉNÉRÉ depuis ui/src/ par tools/assemble-ui (go generate ./internal/loki) — NE PAS ÉDITER index.html DIRECTEMENT -->
<html lang="fr"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<!-- PWA standalone : "Ajouter à l'écran d'accueil" sur iPhone → app plein écran
SANS la barre d'onglets Safari (indispensable pour enregistrer des démos). -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Loki">
<title>Loki</title>
<!-- Favicon (même marque « loki » que la landing/portail) + icône PWA (écran d'accueil
iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon
(iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. -->
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'><rect width='12' height='12' rx='2' ry='2' fill='%235E7F5A'/><rect x='6' y='3' width='2' height='2' fill='%23fff'/><rect x='6' y='5' width='2' height='2' fill='%23fff'/><rect x='4' y='7' width='2' height='2' fill='%23fff'/></svg>">
<link rel="apple-touch-icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAYAAAA9zQYyAAADD0lEQVR4nOzdW27aWgBGYag6ylZqh1I6lFZKppmjEylRLlyMsbG9+n3vsP2w8mfDC193d/Dj17ene5zDuv39/bif+4xZDhAwQ8wR+GRvKGJuMVXcN7+JkJnSrWGPfrGQmdPYsL+MeZGYmdvYxq76KxAyS7hmrQcvtJhZyjXtDQpazCxtaIMXgxYzazGkxbNBi5m1udTkyaDFzFqda3PU13awVkeDts6s3alGPwUtZrbiWKuuHKS8C9o6szUfm7XQpLwGbZ3ZqrftWmhSBE3Kc9CuG2zdS8MWmhRBkyJoUvbuz5RYaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSRE0KYImRdCkCJoUQZMiaFIETYqgSbnLj9ez2/05PCxy7s/D90XOXYqFJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAiaFEGTImhSBE2KoEkRNCmCJkXQpAialK9LP8C9/Tk8LHLuz8P3Rc7911hoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZl/+PXt6elHwKmYqFJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgiZF0KQImhRBkyJoUgRNiqBJETQpgibly9/fj/ulHwKm8H/LFpoUQZMiaFKeg3aPZuteGrbQpAialNegXTvYqrftWmhS3gVtpdmaj81aaFI+BW2l2YpjrR5daFGzdqcadeUg5WTQVpq1Otfm2YUWNWtzqcmLVw5RsxZDWhx0hxY1Sxva4OAPhaJmKde0NypSPwXHPYwZ0VFf21lr5ja2sZvDtNZM6daxnGxphc0tpvqvP8vVQdwMMcfV9S53YYGzu9Nnr/8CAAD//xZRk2OJYiOqAAAAAElFTkSuQmCC">
<!-- Manifeste PWA : installabilité Android/desktop et, surtout, Web Push. iOS
s'appuie sur les métas apple-* ci-dessus ; le manifeste ne le gêne pas.
Servi à la racine par web_server.go, comme /sw.js. -->
<link rel="manifest" href="manifest.webmanifest">
<!-- Marque le mode PWA AVANT le rendu : la hauteur de coque et le défilement du
document en dépendent (voir styles.css). ⚠️ On NE PEUT PAS se fier au média
`display-mode:standalone` : WebKit l'adosse au `display` du MANIFESTE, or cette
app n'en a pas (elle est en `apple-mobile-web-app-capable`) — en PWA iOS il
répond donc `browser`. `navigator.standalone` est le signal juste ici ; on
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
<script>try{var t=localStorage.getItem('loki-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('loki-'+k)==='1'?'1':'0')});
/* Barre latérale escamotée et moniteur replié : posés AVANT le rendu, sinon la
barre s'affiche puis se referme sous les yeux à chaque chargement. Ouverts par
défaut — une valeur absente vaut « visible ». */
document.documentElement.setAttribute('data-side',localStorage.getItem('loki-side')==='0'?'0':'1');
}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
<style>
@@CSS@@
</style></head><body>
<button id="menubtn" onclick="toggleSide()" title="menu" aria-label="ouvrir le menu"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg></button>
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
fermé sur téléphone. Sans ce rappel, un rechargement de page donnait
l'impression que l'installation avait disparu. -->
<button id="lc-chip" onclick="lcChipOpen()" hidden></button>
<div id="backdrop" onclick="toggleSide()"></div>
<div class="side" id="side">
<!-- Trois zones : en-tête figé, contenu défilant, moniteur machine en pied.
C'est ce découpage qui garde les jauges visibles quel que soit le nombre
de discussions — elles les poussaient auparavant hors de l'écran. -->
<div class="sidehead">
<div class="sidebrand">
<h1 id="brand" aria-label="loki" title="Loki — fork d'AJEAN (github.com/nathaninline/ajean, MIT)"><span id="brand-a"><svg viewBox="0 0 4 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="1" height="5"/><rect x="1" y="4" width="3" height="1"/></svg></span><span id="brand-word"><svg viewBox="0 0 11 5" shape-rendering="crispEdges" fill="currentColor"><rect x="0" y="0" width="3" height="1"/><rect x="0" y="1" width="1" height="3"/><rect x="2" y="1" width="1" height="3"/><rect x="0" y="4" width="3" height="1"/><rect x="4" y="0" width="1" height="5"/><rect x="6" y="0" width="1" height="2"/><rect x="5" y="2" width="1" height="1"/><rect x="6" y="3" width="1" height="2"/><rect x="8" y="0" width="3" height="1"/><rect x="9" y="1" width="1" height="3"/><rect x="8" y="4" width="3" height="1"/></svg></span><span id="ver"></span></h1>
<!-- Attribution du fork (licence MIT de l'amont) : sous le nom de l'app,
là où elle a toujours vécu. -->
<div class="brand-fork">fork de <a href="https://github.com/nathaninline/ajean" target="_blank" rel="noopener noreferrer" title="AJEAN par nathaninline — licence MIT">AJEAN</a></div>
</div><!-- /.sidebrand -->
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
</div>
<div id="side-scroll">
<!-- Projet actif : il gouverne la mémoire, les trackers ET la liste des
discussions en dessous. Posé AU-DESSUS d'elles parce qu'il les contient —
changer de projet change la liste. Le bouton ouvre le panneau de gestion
(créer, renommer, décrire, supprimer), geste bien plus rare que la bascule. -->
<div class="projrow">
<select id="proj-switch" onchange="onProjectSwitch(this)" title="projet actif — mémoire, trackers et discussions cloisonnés" aria-label="projet actif"></select>
<button class="preset-add" onclick="openSettings('projets')" title="gérer les projets" aria-label="gérer les projets"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg></button>
</div>
<!-- Discussions : plusieurs fils, la liste la plus récente en tête. Le fil
actif est partagé par tous les appareils (comme la conversation unique
d'origine) : basculer ici bascule partout.
La recherche filtre CÔTÉ CLIENT : la liste est déjà entièrement chargée
(métadonnées seules, sans les messages), un aller-retour serveur par
frappe n'apporterait rien. -->
<details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion" aria-label="nouvelle discussion"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5.5v13"/><path d="M5.5 12h13"/></svg></button></summary>
<input id="conv-search" type="search" placeholder="rechercher une discussion…" oninput="renderConversations()" aria-label="rechercher une discussion">
<div id="conv-list" class="slist"></div>
</details>
<!-- 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:4px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
<!-- Conseil de performance (stockage Unraid via fuse.shfs) : permanent tant
que la cause dure, donc masquable — mémorisé pour CE texte seulement. -->
<div id="app-hint" class="muted" style="display:none;position:relative;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 24px 8px 10px;border-radius:4px;border:1px solid var(--border)"><span id="app-hint-text"></span><button type="button" onclick="dismissHint()" title="masquer ce conseil" aria-label="masquer ce conseil" style="position:absolute;top:3px;right:3px;margin:0;padding:0 5px;border:0;background:none;color:inherit;font-size:14px;line-height:1.3">×</button></div>
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:4px;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
OU par son propre chevron — c'est une section comme les autres, sinon on ne
trouvait pas comment la refermer.
C'est le seul endroit où l'on voit pourquoi un modèle refuse de charger. -->
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
empilait 11 arrondis et autant de gouttières : la barre latérale ne
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
<!-- Tout ce qui n'est pas la conversation tient sous CE repli : la barre
latérale par défaut, c'est les discussions et la machine. Les sections
ouvertes par programme (journal du moteur, installation llama.cpp)
passent par openDetails(), qui déplie aussi les parents. -->
</div><!-- /#side-scroll -->
<!-- Réglages : tout vit dans une modale (nav de sections à gauche, panneau à
droite) — voir js/22-settings-modal.js ; les ids historiques des sections
(svc-log-box, lc-details…) sont conservés sur les panneaux. Le bouton est
ANCRÉ en pied de barre, juste au-dessus du moniteur machine : toujours au
même endroit, quel que soit le nombre de discussions. -->
<button id="side-settings-btn" onclick="openSettings()" aria-haspopup="dialog">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.35a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.7 1.7 0 0 0 .34-1.87 1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.65 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.7 1.7 0 0 0 1.87.34H9a1.7 1.7 0 0 0 1.03-1.56V3a2 2 0 1 1 4 0v.09c0 .68.4 1.3 1.03 1.56a1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.7 1.7 0 0 0-.34 1.87v.06c.26.63.88 1.03 1.56 1.03H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.56 1.03z"/></svg>
<span>Paramètres</span>
</button>
<!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en
jauges fines — visible en permanence sans jamais disputer la place au
fil. Le détail chiffré (Gio, température) vit sous chaque jauge, et le
bloc entier se replie d'un clic. -->
<div id="side-perf">
<!-- « Libérer la VRAM » est posé LÀ, sur les jauges : c'est en les regardant
qu'on décide de rendre la carte à une autre application. Le libellé
bascule en « Recharger le modèle » dès que le moteur est arrêté — voir
paintVramBtn (js/05-status.js). -->
<div class="perf-head"><span>Performance</span><button id="vram-btn" class="perf-act" onclick="vramBtnClick()" title="décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine">Libérer la VRAM</button></div>
<div class="perf-body"><div id="vram">…</div><div id="ram-details"><div id="ram"></div></div></div>
</div>
</div>
<div class="main" style="position:relative">
<!-- Barre d'en-tête : de quoi on parle, et avec quel modèle. Le changement de
modèle demandait auparavant d'ouvrir la barre latérale et de descendre
jusqu'aux presets — trois clics pour un geste qu'on répète. -->
<header id="topbar">
<button id="sidetoggle" class="iconbtn" onclick="toggleSideCollapse()" title="afficher/masquer la barre latérale" aria-label="afficher ou masquer la barre latérale">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16"/></svg>
</button>
<div id="topbar-title">Nouvelle discussion</div>
<select id="model-switch" onchange="onModelSwitch(this)" title="modèle actif — changer relance le moteur" aria-label="modèle actif"></select>
<button id="themebtn" class="iconbtn" onclick="toggleThemeQuick()" title="thème clair / sombre" aria-label="basculer le thème">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
</button>
</header>
<div id="chat"></div>
<!-- Voile de chargement du fil : sans lui, une connexion lente donne un chat
VIDE indiscernable d'une conversation vide. Masqué au signal {caught_up}. -->
<div id="chat-loading"><span class="cl-dots"><span></span><span></span><span></span></span><span id="chat-loading-text">chargement de la conversation…</span></div>
<!-- Accueil : affiché UNIQUEMENT quand le fil est réellement vide (et une fois
le replay terminé, sinon il clignoterait au chargement de chaque page).
Le logo est cloné depuis #brand au démarrage — une seule source de vérité
pour le dessin des lettres. -->
<div id="chat-empty" aria-hidden="true">
<div id="ce-logo"></div>
<!-- Une seule ligne, NEUTRE en genre : l'UI est utilisée par tout le monde,
pas de civilité ni d'accord qui obligerait à deviner. -->
<div id="ce-title">On commence par quoi ?</div>
</div>
<button id="scrollbtn" onclick="jumpBottom()" title="aller en bas" aria-label="aller en bas"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M6 13l6 6 6-6"/></svg></button>
<!-- Voile de dépôt : révélé pendant qu'on survole la fenêtre avec un fichier. -->
<div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div>
<div id="composer">
<div id="ctxmeta">
<!-- Mode de la discussion : Chat (assistant généraliste) ou Code (agent
de code : critères, vérification, LSP). Dans le pied de carte, pas
dans la barre de saisie déjà chargée — ça se règle par discussion,
pas par message. -->
<div id="mode-seg" role="group" aria-label="Mode de la discussion">
<button type="button" data-mode="chat" class="on" onclick="setMode('chat')">Chat</button>
<button type="button" data-mode="code" onclick="setMode('code')">Code</button>
</div>
<div id="ctxtrack"><div id="ctx-fill"></div></div>
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
<!-- Pied de carte, à droite : le décompte de contexte, compacter, puis le
trombone — tous à la même échelle, c'est une seule ligne d'outils. -->
<span style="display:flex;align-items:center;gap:8px;margin-left:auto">
<span id="ctx-text" class="muted">0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
</span>
</div>
<!-- Pièces jointes en attente : remplie par addFiles(), vidée à l'envoi.
Petites bulles POSÉES AU-DESSUS de la carte, dans la bande de dégradé
qui la sépare du fil — la carte ne bouge donc pas d'un pixel. -->
<!-- Messages envoyés PENDANT une réponse (file d'attente, voir send()) : en
gris au-dessus de la carte jusqu'à ce que le serveur les prenne en compte
— injectés dans la réponse en cours, ou traités au tour suivant. -->
<div id="queue-list" aria-live="polite"></div>
<div id="attach-list"></div>
<div id="inputbar">
<textarea id="input" rows="1" placeholder="Message Loki…" onkeydown="onKey(event)" oninput="autoGrow(this)" onpaste="onPaste(event)"></textarea>
<input type="file" id="attach-input" multiple style="display:none" onchange="onAttachPick(event)">
<!-- Intensité du raisonnement : réglable ici parce que ça se décide au
moment d'écrire le message, pas dans un éditeur de preset. -->
<select id="think-effort" title="Intensité du raisonnement" aria-label="Intensité du raisonnement" onchange="setThinkEffort(this.value)">
<option value="">auto</option>
<option value="none">aucune</option>
<option value="low">basse</option>
<option value="medium">moyenne</option>
<option value="high">haute</option>
<option value="xhigh">maximale</option>
</select>
<!-- Dictée vocale : enregistre au micro, transcrit en local (Parakeet
côté serveur) et pose le texte dans le champ. Voir js/23-dictate.js.
Le navigateur n'autorise le micro qu'en HTTPS (ou localhost). -->
<button id="mic" onclick="dictateToggle()" title="dicter au micro" aria-label="dicter au micro">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0"/><path d="M12 18v3"/></svg>
</button>
<!-- Joindre un fichier. Icône dessinée en SVG : pas de dépendance à une
police d'emoji, dont le rendu varie énormément d'un système à l'autre. -->
<button id="attach" onclick="document.getElementById('attach-input').click()" title="joindre un fichier" aria-label="joindre un fichier">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 16V4"/><path d="m7.5 8.5 4.5-4.5 4.5 4.5"/><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3"/></svg>
</button>
<!-- Fichiers de la discussion : ouvre le panneau de droite. Même gabarit
que le dépôt — ce sont les deux faces du même sujet, ce qui entre et
ce qui sort. -->
<button id="files-btn" onclick="toggleFiles()" title="Fichiers de cette discussion" aria-label="Fichiers de cette discussion" aria-expanded="false">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
</button>
<!-- Envoi : pastille ronde à flèche, comme la maquette. Le mot « Envoyer »
reste porté par aria-label et l'infobulle — l'action garde son nom
pour un lecteur d'écran, elle ne l'affiche simplement plus. -->
<button id="send" onclick="send()" title="Envoyer" aria-label="Envoyer">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" stroke="none" aria-hidden="true"><path d="M3.4 20.4 21 12 3.4 3.6l.02 6.53L15.5 12 3.42 13.87z"/></svg>
</button>
<button id="stop" onclick="stopGen()" title="Arrêter la génération" aria-label="Arrêter la génération">
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" stroke="none" aria-hidden="true"><rect x="5" y="5" width="14" height="14" rx="3"/></svg>
</button>
</div>
<div id="sendhint" class="muted">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
</div>
</div>
<!-- Fichiers : ce qui appartient à la DISCUSSION ouverte — pièces jointes
déposées, captures, ce que l'agent y a écrit. Panneau de DROITE, symétrique
des réglages à gauche, ouvert et fermé par le bouton dossier du pied de
carte. Il ne se remplit qu'à l'ouverture (loadFiles), puis se redessine seul
à chaque résultat d'outil, fin de tour ou dépôt (filesOnActivity) — un ReadDir à chaque
chargement de page pour un panneau fermé serait payé par tout le monde et
utile à personne. -->
<div id="files-backdrop" onclick="toggleFiles()"></div>
<aside id="files" aria-label="Fichiers de cette discussion">
<div class="files-head">
<!-- Titre court : le panneau est déjà nommé par son contexte, et le libellé
complet reste porté par l'aria-label de la section. -->
<span class="stitle">Fichiers</span>
<button id="files-refresh" onclick="loadFiles()" title="rafraîchir" aria-label="rafraîchir"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg></button>
<button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<!-- Fil d'Ariane : le dossier de la discussion a des sous-dossiers (uploads,
captures), il faut pouvoir y descendre et en remonter. -->
<div id="files-crumb" class="files-crumb"></div>
<div id="files-list" class="files-list"></div>
<div id="files-foot" class="files-foot muted"></div>
</aside>
<div id="toast"></div>
<div id="bench-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeBenchModal()">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:4px;width:100%;max-width:480px;display:flex;flex-direction:column">
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<strong>Benchmark</strong>
<button onclick="closeBenchModal()" style="margin:0" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div>
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px">
<button id="bench-cancel" onclick="cancelBenchUI()" style="display:none">annuler</button>
<button onclick="closeBenchModal()">fermer</button>
<button id="bench-full" onclick="runBenchUI('full')" title="prefill à froid en profondeur puis 3 tours qui reprennent le cache — 1 à 5 minutes">bench complet</button>
<button id="bench-rerun" onclick="runBenchUI()" style="border-color:var(--accent);color:var(--accent)">relancer</button>
</div>
</div>
</div>
<!-- Preset « externe » : le chat part vers une API OpenAI-compatible distante
(OpenAI, Groq, OpenRouter, un vLLM sur une autre machine…) au lieu du
llama-server local. C'est un preset comme un autre — il apparaît dans la
même liste, se bascule pareil — mais ses réglages n'ont rien à voir avec
ceux d'un modèle local : ni quantification, ni couches GPU, ni moteur.
D'où une fenêtre à part plutôt que des champs grisés dans l'éditeur. -->
<div id="ext-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:30;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeExternal()">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:4px;width:100%;max-width:520px;display:flex;flex-direction:column;max-height:90vh">
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<strong id="ext-title">API externe</strong>
<button onclick="closeExternal()" style="margin:0" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div class="pe-body" style="padding:0">
<div class="pe-group">
<div class="pe-gh">Connexion</div>
<div class="pe-list">
<div class="pe-row stack plain"><input id="ext-name" class="pe-inp" placeholder="Nom du preset (ex. Groq 70B)"></div>
<div class="pe-row stack">
<span class="pe-row-l">URL<span class="pe-sub">base (…/v1) ou URL complète des complétions</span></span>
<input id="ext-url" class="pe-inp" placeholder="https://api.groq.com/openai/v1" spellcheck="false">
</div>
<div class="pe-row stack">
<span class="pe-row-l">Modèle<span class="pe-sub">le nom exact attendu par l'API</span></span>
<input id="ext-model" class="pe-inp" placeholder="llama-3.3-70b-versatile" spellcheck="false">
</div>
<div class="pe-row stack">
<span class="pe-row-l">Clé d'API<span class="pe-sub" id="ext-key-sub">vide = aucune authentification</span></span>
<input id="ext-key" class="pe-inp" type="password" autocomplete="off" spellcheck="false" placeholder="sk-…" oninput="extKeyTouched=true">
</div>
<div class="pe-row">
<span class="pe-row-l">Contexte<span class="pe-sub">pilote la jauge et le seuil de compactage</span></span>
<span class="pe-row-c"><input id="ext-ctx" class="pe-val" type="number" min="0" step="1024" placeholder="32768"><span class="pe-unit">tok</span></span>
</div>
<div class="pe-row plain">
<label class="switchrow" style="margin:0"><span class="switch"><input type="checkbox" id="ext-vision"><span class="slider"></span></span> le modèle accepte les images<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">coche si le modèle distant est <b>multimodal</b> (vision) : les images jointes lui sont alors envoyées. À laisser décoché pour un modèle texte seul.</span></span></label>
</div>
<div class="pe-row stack">
<button id="ext-test" class="pe-link" onclick="testExternal()">Tester la connexion</button>
<div id="ext-test-out" class="pe-note"></div>
</div>
</div>
</div>
</div>
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:6px">
<button id="ext-del" onclick="deleteExternal()" class="danger" style="display:none">supprimer</button>
<span style="flex:1"></span>
<button onclick="closeExternal()">annuler</button>
<button id="ext-save" onclick="saveExternal()" style="border-color:var(--accent);color:var(--accent)">enregistrer</button>
</div>
</div>
</div>
<!-- Fenêtre d'export. Un fil relu pour retrouver une réponse n'a que faire des
mille lignes de raisonnement qui l'ont produite : on choisit ce qu'on
emporte plutôt que de tout prendre ou rien. Les options de CONTENU sont les
mêmes pour les deux formats : le format ne décide que du contenant. -->
<!-- Réglages : modale à deux volets — nav de sections à gauche, LE panneau
choisi à droite. Chaque section d'antan (details de la barre latérale) est
devenue un panneau .set-pane ; les ids historiques (svc-log-box,
lc-details…) sont conservés — openDetails() les résout vers
openSettings(panneau), donc pastille d'état et jobs moteur rouvrent la
modale au bon endroit. -->
<div id="settings-modal" class="modal-ov set-ov" style="display:none" onclick="if(event.target===this)closeSettings()">
<div class="modal-card set-card">
<div class="modal-head">
<strong>Paramètres</strong>
<button class="modal-x" onclick="closeSettings()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div class="set-shell">
<nav class="set-nav" id="set-nav" aria-label="sections des réglages">
<div class="set-nav-h">Intelligence artificielle</div>
<button data-pane="projets" onclick="settingsShow('projets')">Projets</button>
<button data-pane="config" onclick="settingsShow('config')">Config active</button>
<button data-pane="presets" onclick="settingsShow('presets')">Presets</button>
<button data-pane="agent" onclick="settingsShow('agent')"><span>Mode agent</span><span id="agent-badge" class="sbadge"></span></button>
<button data-pane="tasks" onclick="settingsShow('tasks')"><span>Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></button>
<button data-pane="sysprompt" onclick="settingsShow('sysprompt')">System prompt</button>
<div class="set-nav-h">Moteur</div>
<button data-pane="engine" onclick="settingsShow('engine')">Moteur llama.cpp</button>
<button data-pane="engine-log" onclick="settingsShow('engine-log')">Journal du moteur</button>
<button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button>
<div class="set-nav-h">Application</div>
<button data-pane="identity" onclick="settingsShow('identity')">Identité</button>
<button data-pane="appearance" onclick="settingsShow('appearance')">Apparence</button>
<button data-pane="openai" onclick="settingsShow('openai')">Accès OpenAI</button>
<button data-pane="actions" onclick="settingsShow('actions')">Actions</button>
</nav>
<div class="set-body" id="set-body">
<section class="set-pane" data-pane="engine-log" id="svc-log-box">
<div class="set-pane-h"><span>Journal du moteur</span></div>
<div class="row" style="justify-content:flex-end;align-items:center">
<button onclick="loadSvcLog()" title="rafraîchir" aria-label="rafraîchir le journal"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg></button>
</div>
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
<!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
va quand on cherche à comprendre l'état de son installation. -->
<div class="row" style="justify-content:space-between;align-items:center;margin-top:12px">
<span class="io-cap">Emplacements</span>
</div>
<div id="paths-msg" class="muted" style="font-size:10px;line-height:1.5"></div>
</section>
<!-- Projets : un chantier = une mémoire, ses discussions et ses trackers. La
BASCULE se fait dans la barre latérale (geste quotidien) ; ici on gère
(créer, décrire, renommer, supprimer), ce qui est rare. -->
<section class="set-pane" data-pane="projets">
<div class="set-pane-h"><span>Projets</span></div>
<div class="muted" style="font-size:12px;line-height:1.5;margin:0 2px 8px">Chaque projet a sa <b>mémoire</b>, ses <b>discussions</b> et ses <b>trackers</b>, séparés des autres. La <b>description</b> est donnée à l'IA au début de chaque discussion du projet : elle sait sur quoi elle travaille sans qu'on le lui réexplique.</div>
<div class="slist">
<div class="rowbody">
<div id="proj-list" class="mcp-list"></div>
<button class="addbtn" style="margin-top:6px" onclick="newProject()">+ ajouter un projet</button>
</div>
</div>
</section>
<section class="set-pane" data-pane="config" id="cfg-details">
<div class="set-pane-h"><span>Config active</span></div>
<div id="cfg" class="slist">…</div>
</section>
<section class="set-pane" data-pane="presets">
<div class="set-pane-h"><span>Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openExternal('')" title="ajouter une API externe" aria-label="ajouter une API externe"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5a10 10 0 0 1 14 0"/><path d="M8.5 16a5.5 5.5 0 0 1 7 0"/><circle cx="12" cy="19.5" r="1"/></svg></button><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter" aria-label="ajouter"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5.5v13"/><path d="M5.5 12h13"/></svg></button></div>
<div class="slist"><div id="presets" class="rowlist"></div></div>
</section>
<section class="set-pane" data-pane="agent">
<div class="set-pane-h"><span>Mode agent</span></div>
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, écrire/lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label></div>
<!-- Notifications Web Push. Indépendantes du mode agent (une réponse de chat
prête est utile à savoir sans aucun outil), donc HORS du groupe grisé.
À régler sur CHAQUE appareil : l'abonnement appartient au navigateur. -->
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="push-toggle" onchange="togglePush()"><span class="slider"></span></span> me prévenir quand c'est prêt<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Le serveur envoie une <b>notification</b> à la fin d'une réponse et à la fin d'une <b>tâche planifiée</b>, même l'app fermée ou le téléphone verrouillé. Demande <b>HTTPS</b> (ou localhost). Sur iPhone : <b>ajoute d'abord Loki à l'écran d'accueil</b>, puis active depuis l'app installée. À régler sur chaque appareil.</span></span></label>
<div class="rowbody"><div id="push-status" class="muted" style="font-size:11px"></div></div>
</div>
<!-- Sous-réglages du mode agent. MÊME gabarit que « Machine » / « Apparence » :
étiquette de groupe nue, puis UNE carte de lignes (.slist). Le groupe
entier est grisé/inerte quand l'agent est off (voir setAgentGate). -->
<div id="mem-block" class="gated-group">
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
<div class="slist">
<label class="switchrow"><span>mode <small class="dim">(projet actif)</small></span>
<select id="mem-mode" onchange="setMemMode()" class="sctl">
<option value="always">auto, injectée</option>
<option value="search">auto, recherche</option>
<option value="ondemand">sur demande</option>
<option value="off">désactivée</option>
</select>
</label>
<!-- Chiffrement au repos. La clé est celle de pilotage de CET appareil
(localStorage) : le serveur n'en a que l'empreinte, il ne peut pas
ouvrir le coffre seul. Couvre les pages, les discussions, les blocs
archivés au compactage et les trackers. -->
<label class="switchrow"><span class="switch"><input type="checkbox" id="mem-enc-toggle" onchange="toggleMemEncrypt()"><span class="slider"></span></span> chiffrer la mémoire sur le disque<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Chiffre en <b>AES-256-GCM</b> les pages mémoire, les discussions, les blocs archivés et les trackers sous <code>/data</code>. La clé de données est enfermée dans un coffre (Argon2id) ouvert par la <b>clé de pilotage de cet appareil</b> ou par une <b>clé de récupération</b> donnée une seule fois à l'activation. Un <b>snapshot</b> est pris avant chaque bascule, et rien n'est supprimé avant relecture vérifiée.</span></span></label>
<div class="rowbody"><div id="mem-enc-status" class="muted" style="font-size:11px"></div></div>
<!-- Sauvegarde : le MÊME paquet chiffré que l'amont envoie à son relais,
mais en fichier local — rien ne quitte la machine sans qu'on le
demande. Nécessite le chiffrement actif : c'est sa clé qui scelle. -->
<div class="pe-row">
<span class="pe-row-l">Sauvegarde chiffrée</span>
<span class="pe-row-c">
<button onclick="backupExport()">exporter</button>
<button onclick="backupImport()">importer</button>
</span>
</div>
<div id="mem-pages-bar" class="pages-bar" onclick="toggleMemPages()">
<span class="pages-title">Voir la mémoire <span id="mem-count" class="mem-count"></span></span>
<button class="addbtn" onclick="event.stopPropagation();openMem('')" title="ajouter une page mémoire">+ ajouter</button>
</div>
<div id="mem-pages-body" hidden>
<!-- Le champ de filtre garde son rembourrage de ligne ; la liste, elle,
est à fleur de carte : ses pages sont des LIGNES séparées par un
filet, pas une pile de petites cartes. -->
<div id="mem-search-row" class="rowbody" style="display:none">
<input id="mem-search" class="mem-search" type="search" placeholder="filtrer la mémoire…" oninput="renderMemList()" style="margin:0">
</div>
<div id="mem-list" class="mem-list rowlist muted" style="font-size:12px"></div>
</div>
<!-- Trackers : le 3e type de mémoire. Rangé sous « Mémoire » parce que
c'en est une — mais séparé des pages, parce qu'on ne les lit pas
pareil : une page se lit en entier, un tracker jamais. -->
<div id="track-bar" class="pages-bar" onclick="toggleTrackers()">
<span class="pages-title">Trackers <span id="track-count" class="mem-count"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Données <b>datées qui s'accumulent</b> : compteurs, relevés, journaux d'événements (poids, abonnés, relevés de compteur…). L'IA y ajoute des points et les consulte par niveaux — jamais en entier, quelle que soit l'ancienneté. La <b>dernière valeur</b> de chaque tracker lui est donnée d'emblée : elle répond sans appeler d'outil.</span></span></span>
<button class="addbtn" onclick="event.stopPropagation();newTracker()" title="créer un tracker">+ ajouter</button>
</div>
<div id="track-body" hidden>
<div id="track-list" class="mem-list rowlist muted" style="font-size:12px"></div>
</div>
</div>
</div>
<div id="net-block" class="gated-group">
<div class="subhead">Accès internet<span id="internet-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Ajoute les outils <b>web_search / web_open / web_read / web_grep</b>. Le moteur <b>intégré</b> ne demande aucune installation. Le moteur <b>Crawl4AI</b> exige un serveur que tu héberges toi-même (ex. <code>docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest</code>) mais exécute le JavaScript des pages. Actifs seulement si le <b>mode agent</b> est actif.</span></span></div>
<div class="slist">
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
<!-- Moteur web : « go » = intégré au binaire (rien à installer, pas de rendu
JavaScript) · « crawl4ai » = serveur externe avec Chromium headless.
Réglages propres à Crawl4AI masqués avec le moteur intégré ; l'URL et
la clé s'enregistrent en quittant le champ (onchange). -->
<div class="rowbody">
<div class="row" style="margin:0">
<select id="web-engine" class="sctl" onchange="saveWebEngine()">
<option value="go">Moteur Intégré</option>
<option value="crawl4ai">Crawl4AI</option>
</select>
</div>
<!-- Masqués AU DÉPART : le moteur par défaut est l'intégré, et ces champs
ne concernent que Crawl4AI. Affichés d'emblée puis repliés par le
chargement de l'état, ils faisaient sauter la section sous les yeux. -->
<div id="crawl-fields" style="display:none">
<div class="row">
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
</div>
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
qu'un indice sur les 4 derniers caractères. -->
<div class="row">
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
</div>
<div class="muted" style="font-size:11px;margin-top:4px">enregistré automatiquement en quittant le champ</div>
<div id="crawl-key-state" class="muted" style="font-size:11px;margin-top:3px"></div>
</div>
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
</div>
</div>
</div>
<!-- Pilotage de navigateur (computer use) : axe de capacité à part, sous le
mode agent comme l'accès internet — un outil qui clique et tape dans une
page fait des actions RÉELLES, même niveau de confiance que bash. -->
<div id="computer-block" class="gated-group">
<div class="subhead">Contrôle du navigateur<span id="computer-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Donne à l'IA le contrôle d'un <b>navigateur</b> dans le conteneur (outils <b>browser_open / browser_find / browser_click / browser_type / browser_key / browser_scroll</b>). L'IA lit les éléments <b>numérotés</b> de la page et agit par numéro — aucune vision requise, ça marche avec de petits modèles. <code>browser_screenshot</code> s'ajoute si la vision est active. Utilise le Chromium de Playwright embarqué dans l'image ; à défaut, <code>LOKI_CHROME=&lt;chemin&gt;</code>. Actif seulement si le <b>mode agent</b> est actif.</span></span></div>
<div class="slist">
<label class="switchrow"><span class="switch"><input type="checkbox" id="computer-toggle" onchange="toggleComputer()"><span class="slider"></span></span> laisser l'IA piloter un navigateur</label>
<div class="rowbody">
<div id="computer-status" class="muted" style="font-size:11px;margin-top:4px"></div>
</div>
</div>
</div>
<div id="mcp-block" class="gated-group">
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
<div class="slist">
<div class="rowbody">
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
<div id="mcp-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun serveur — ajoute un serveur fichiers, une base de données, une API…)</div></div>
<div class="mcp-actions">
<button class="addbtn" onclick="openMcpCatalog()">catalogue</button>
<button class="addbtn" onclick="openMcp('')">+ ajouter un serveur</button>
</div>
</div>
</div>
</div>
<div id="param-block" class="gated-group">
<div class="subhead">Paramètres</div>
<div class="slist">
<label class="switchrow"><span class="switch"><input type="checkbox" id="compact-toggle" onchange="toggleCompact()"><span class="slider"></span></span> compactage automatique du contexte<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">activé par défaut : quand la conversation approche de la limite de contexte, les vieux tours sont résumés automatiquement (façon Hermes) au lieu de t'imposer un « vider ». Discussions quasi illimitées sans perdre le fil.</span></span></label>
</div>
</div>
</section>
<section class="set-pane" data-pane="tasks" id="tasks-details">
<div class="set-pane-h"><span>Tâches planifiées</span></div>
<!-- Tâches planifiées : l'IA exécute une consigne toute seule sur une
fréquence réglable (voir tasks.go). Chaque tâche tourne ISOLÉE de la
conversation — elle livre son résultat via les outils de l'IA (mail via
MCP, shell…). D'où l'avertissement quand le mode agent est off : sans
lui, aucune tâche ne peut agir. -->
<div class="slist">
<label class="switchrow"><span class="switch"><input type="checkbox" id="tasks-pause-toggle" onchange="toggleTasksPause()"><span class="slider"></span></span> suspendre toutes les tâches<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Interrupteur maître : coche pour que plus aucune tâche ne se déclenche. Les tâches gardent leur réglage et repartent quand tu décoches.</span></span></label>
</div>
<div id="tasks-agent-warn" class="muted" style="display:none;font-size:12px;line-height:1.5;margin:8px 2px">Le <b>mode agent</b> est désactivé : les tâches s'exécuteront mais l'IA n'aura <b>aucun outil</b> pour agir (envoyer un mail, lire des fichiers…). Active le mode agent ci-dessus.</div>
<div class="slist">
<div class="rowbody">
<div id="tasks-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucune tâche — ex. « toutes les 2 h, relève mes mails et prépare des brouillons de réponse »)</div></div>
<button class="addbtn" style="margin-top:6px" onclick="openTask('')">+ ajouter une tâche</button>
</div>
</div>
</section>
<section class="set-pane" data-pane="sysprompt">
<div class="set-pane-h"><span>System prompt</span></div>
<div class="slist"><div class="rowbody">
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
</div></div>
</section>
<section class="set-pane" data-pane="engine" id="lc-details">
<div class="set-pane-h"><span>Moteur</span></div>
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
même sujet. Les trois modes sont des LIGNES de cette carte (elles
étaient des cartes dans une carte). -->
<div class="slist">
<!-- Moteur fourni par l'image Docker : aucun des trois modes d'installation
ne s'applique (rien à compiler, et l'image n'embarque ni cmake ni
compilateur). En revanche llama.cpp publie plusieurs versions par jour,
et son moteur déjà compilé est téléchargeable seul (~170 Mo) : cette
carte le met à jour sans reconstruire l'image. -->
<div id="lc-provided" class="rowbody" style="display:none">
<div style="font-size:12px;line-height:1.5">
<div>Moteur <b>llama.cpp</b> <span id="eng-build" class="mcp-tag eng-ver"></span> <span id="eng-source" class="mcp-tag"></span></div>
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
<div id="eng-notice" class="muted" style="margin-top:6px;display:none"></div>
<div id="eng-latest" style="margin-top:6px"></div>
<div class="row" style="margin:8px 0 0">
<!-- « vérifier » ne fait QUE regarder (aucun téléchargement, aucune
confirmation) ; « mettre à jour » télécharge et bascule. Les deux
gestes n'engagent pas pareil, chacun son bouton. -->
<button onclick="engCheck()">vérifier les mises à jour</button>
<button onclick="engUpdate()">mettre à jour le moteur</button>
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
<span id="eng-others"></span>
</div>
<div class="muted" style="margin-top:6px">Télécharge llama-server et ses bibliothèques depuis l'image officielle
<code id="eng-repo"></code> (~170&nbsp;Mo), sans reconstruire l'image de Loki. Le nouveau moteur est essayé
avant d'être adopté ; celui de l'image reste intact et récupérable en un clic.</div>
</div>
</div>
<!-- La pastille « conseillé » n'est PAS écrite ici : le bon choix dépend de
la machine (voir lcRenderReco). Sur Linux + NVIDIA le précompilé ne
donne que du Vulkan — llama.cpp ne publie aucun binaire CUDA Linux —
alors que compiler donne du CUDA natif. -->
<div class="lc-modes">
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco" id="lc-reco-fast" hidden>conseillé</span></div>
<div class="lc-mode-d" id="lc-desc-fast">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
<div class="lc-mode-state" id="lc-fast-state"></div>
</div>
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
<div class="lc-mode-t">llama.cpp compilé <span class="lc-reco" id="lc-reco-opt" hidden>conseillé</span></div>
<div class="lc-mode-d" id="lc-desc-opt">compilé ici pour votre machine — plus rapide, plus long à installer</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">llama.cpp personnalisé</div>
<div class="lc-mode-d">un fork de llama.cpp pour modèles spéciaux (quant ternaire…)</div>
<div class="lc-mode-state" id="lc-custom-state"></div>
</div>
</div>
<div id="lc-job" class="rowbody" style="display:none">
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
<pre id="lc-log" hidden></pre>
</div>
</div>
<!-- Ces boutons pilotent le service qui fait tourner llama-server : leur
place est ici, avec le moteur, et pas dans une section « Service » à
part qui ne contenait qu'eux. Le dernier, « libérer la VRAM », fait
plus qu'arrêter : il éteint aussi la dictée et annonce la mémoire
vidéo rendue (c'est le même geste que le bouton du moniteur). -->
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).<br><b>Libérer la VRAM</b> arrête le moteur <i>et</i> la dictée, puis annonce la mémoire vidéo rendue : c'est ce qu'il faut quand une autre application réclame la carte. Le même bouton vit sur les jauges, en pied de barre latérale.</span></span></div>
<div class="slist"><div class="rowbody">
<div class="row" style="margin:0">
<button onclick="act('start')">démarrer</button>
<button onclick="act('restart')">redémarrer</button>
<button onclick="act('stop')">arrêter</button>
<button onclick="vramUnload(false)" title="décharge le modèle et rend la mémoire vidéo à la machine">libérer la VRAM</button>
</div>
</div></div>
</section>
<!-- Dictée vocale. Réglages de SERVEUR : le modèle chargé est une propriété
de la machine, identique pour tous les onglets.
Voir js/24-dictate-settings.js et internal/loki/dictate_*.go. -->
<section class="set-pane" data-pane="dictee">
<div class="set-pane-h"><span>Dictée</span></div>
<div class="subhead">Modèle<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Transcription par <b>Parakeet</b> (NVIDIA), servie par sherpa-onnx. Le modèle n'est <b>pas</b> dans l'image : il est téléchargé à la demande dans <code>/data/asr/</code> et survit aux recréations du conteneur. Il tourne dans un processus séparé, allumé au premier clic sur le micro et éteint après <b>dix minutes</b> sans dictée. Sur le <b>processeur</b> : la carte reste au moteur de chat, et un modèle de cette taille n'en a pas besoin sur des tranches de quelques secondes.</span></span></div>
<div class="slist">
<label class="switchrow"><span>modèle</span>
<select id="dic-model" class="sctl"></select>
</label>
<div class="rowbody">
<button id="dic-dl" class="btn" onclick="dictateDownload()">Télécharger ce modèle</button>
<span id="dic-dl-state" class="dim"></span>
</div>
</div>
<div class="subhead">Reconnaissance<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Il n'y a pas de réglage de <b>langue</b> : Parakeet la détecte lui-même, phrase par phrase. Le modèle v3 couvre 25 langues européennes dont le français ; le v2 ne comprend que l'anglais.</span></span></div>
<div class="slist">
<label class="switchrow"><span>réactivité<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Longueur des tranches transcrites au fil de la dictée. Court = le texte arrive plus tôt, avec moins de contexte donc plus d'erreurs. Long = mieux reconnu, mais plus d'attente.</span></span></span>
<select id="dic-react" class="sctl">
<option value="court">court</option>
<option value="moyen">moyen</option>
<option value="long">long</option>
</select>
</label>
<div class="rowbody">
<button class="btn primary" onclick="dictateSave()">Enregistrer</button>
<span id="dic-save-state" class="dim"></span>
</div>
</div>
<div class="subhead">Vérifier</div>
<div class="slist">
<div class="rowbody">
<button id="dic-test" class="btn" onclick="dictateTestMic()">Tester le micro (3 s)</button>
</div>
<div id="dic-test-out" class="dim"></div>
<div id="dic-state" class="dim"></div>
</div>
</section>
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
l'application elle-même. -->
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
empilait 11 arrondis et autant de gouttières : la barre latérale ne
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
<section class="set-pane" data-pane="identity" id="ident-details">
<div class="set-pane-h"><span>Identité</span></div>
<div class="slist"><div class="rowbody">
<div class="pe-row">
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
<!-- autocomplete/data-*-ignore : sans ça, les gestionnaires de mots de
passe prennent ce champ pour un identifiant de connexion et
recouvrent la saisie de comptes enregistrés. -->
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi"
autocomplete="off" name="loki-nickname" data-1p-ignore data-lpignore="true" data-form-type="other"
oninput="saveIdentity()"></span>
</div>
<div class="pe-row stack">
<span class="pe-row-l">Ton avatar</span>
<div id="p-avatar-user" class="avpick"></div>
</div>
<div class="pe-row stack">
<span class="pe-row-l">Avatar de Loki</span>
<div id="p-avatar-ai" class="avpick"></div>
</div>
</div></div>
</section>
<section class="set-pane" data-pane="appearance">
<div class="set-pane-h"><span>Apparence</span></div>
<div class="slist">
<!-- Langue de l'interface. La source est française ; « English » applique
un dictionnaire sur la coque (navigation, libellés, boutons) — ce qui
n'y figure pas reste en français plutôt que d'afficher une clé. -->
<label class="switchrow"><span>Langue</span>
<select id="ui-lang" class="sctl" onchange="setUiLang(this.value)">
<option value="fr">Français</option>
<option value="en">English</option>
</select>
</label>
<label class="switchrow rev"><span>mode sombre</span>
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
</label>
</div>
<div class="subhead">Affichage</div>
<div class="slist">
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-reasoning" onchange="toggleView('hide-reasoning')"><span class="slider"></span></span> masquer le raisonnement</label>
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-tools" onchange="toggleView('hide-tools')"><span class="slider"></span></span> masquer les appels d'outils</label>
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-fold-tools" onchange="toggleView('fold-tools')"><span class="slider"></span></span> garder les bulles repliées<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Les bulles de raisonnement et d'outil arrivent <b>déjà fermées</b> au lieu de s'ouvrir puis se refermer. Un clic sur leur étiquette les déplie.</span></span></label>
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
</div>
</section>
<section class="set-pane" data-pane="openai">
<div class="set-pane-h"><span>Accès OpenAI</span></div>
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor, Open WebUI…) sur cette adresse — le modèle s'y appelle <code>loki</code>. L'endpoint est servi par Loki <b>sur le port de cette interface</b> : aucun autre port à ouvrir. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer &lt;clé&gt;</code>.</span></span></div>
<div class="slist">
<div class="rowbody">
<div class="muted" style="margin:0 0 6px">Adresse de l'endpoint — modèle <code>loki</code></div>
<div class="row" style="margin:0">
<input id="oai-url" readonly class="io-input">
<button class="iconbtn" onclick="copyText(document.getElementById('oai-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
</div>
</div>
<!-- Adresse publique : ce que Loki ne peut pas deviner. Vide, l'adresse
ci-dessus est celle par laquelle CE navigateur a ouvert l'interface —
déjà juste dans la plupart des cas. Renseignée, elle prend le pas :
c'est le cas du reverse proxy, où Loki ne voit qu'un appel interne. -->
<div class="rowbody">
<div class="muted" style="margin:0 0 6px">Adresse publique <span style="opacity:.7">— facultative</span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Le nom de domaine ou l'IP par lesquels on atteint cette interface, par exemple <code>ia.exemple.fr</code> ou <code>192.168.1.20:8090</code>. <b>Laissez vide</b> et l'adresse affichée suit celle de votre navigateur. Renseignez-la si vous passez par un <b>reverse proxy</b> ou un nom de domaine : Loki ne voit alors qu'un appel interne et ne peut pas la deviner. Sans <code>https://</code>, il est ajouté.</span></span></div>
<div class="row" style="margin:0">
<input id="public-addr" class="io-input" placeholder="ia.exemple.fr" onkeydown="if(event.key==='Enter')savePublicAddr()">
<button onclick="savePublicAddr()">enregistrer</button>
</div>
</div>
</div>
<div class="subhead">Clé API<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Protège l'endpoint des complétions — vérifiée par Loki <b>et</b> par le moteur. Indispensable dès que l'adresse ci-dessus est joignable par d'autres. Modifier la clé <b>redémarre</b> le moteur.</span></span></div>
<div class="slist"><div class="rowbody">
<div id="oai-open-warn" style="display:none;margin:0 0 8px;font-size:11.5px;line-height:1.5;color:var(--err)"></div>
<div class="row" style="margin:0">
<input id="oai-key" readonly class="io-input">
<button id="oai-key-eye" onclick="toggleKeyReveal()" title="afficher / masquer">afficher</button>
<button class="iconbtn" onclick="copyApiKey()" title="copier la clé"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
</div>
<div class="row" style="margin-bottom:0">
<button onclick="apiKeyAction('generate')">générer</button>
<button onclick="apiKeySet()">définir…</button>
<button onclick="apiKeyAction('clear')">retirer</button>
</div>
</div></div>
</section>
<section class="set-pane" data-pane="actions" id="upd-details">
<div class="set-pane-h"><span>Actions</span></div>
<div class="slist"><div class="rowbody">
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
dans loki.db (bbolt, verrouillé tant que le service tourne) : sans le
bouton exporter, plus personne ne peut relire/archiver ses échanges. -->
<div class="row" style="margin:0">
<button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button>
<button onclick="openExportModal()" title="choisir le format et ce qu'on emporte">⤓ exporter…</button>
</div>
<div id="upd-msg" class="muted"></div>
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
<div class="row" style="margin-bottom:0">
<button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="resetChat()" title="vider la discussion"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M9.5 7V4.5h5V7"/><path d="M6.5 7l1 12.5h9l1-12.5"/></svg><span>vider la discussion</span></button>
</div>
</div></div>
</section>
</div><!-- /.set-body -->
</div><!-- /.set-shell -->
</div>
</div>
<div id="export-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeExportModal()">
<div class="modal-card" style="max-width:460px">
<div class="modal-head">
<strong>Exporter la conversation</strong>
<button class="modal-x" onclick="closeExportModal()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<!-- Conversation vide : pas d'options à régler, un seul message. Proposer
des réglages pour un fichier qui ne contiendrait rien n'a aucun sens. -->
<div id="x-empty" class="xempty" style="display:none">
<div class="xempty-i">⤓</div>
<div>Rien à exporter</div>
<div class="pe-note">La conversation est vide.</div>
</div>
<div id="x-body" class="pe-body" style="gap:20px">
<div class="pe-group">
<div class="pe-gh">Format</div>
<div class="pe-seg">
<label><input type="radio" name="x-fmt" value="md" checked onchange="onExportFormat()"><span>Markdown</span><span class="be-note">à lire</span></label>
<label><input type="radio" name="x-fmt" value="json" onchange="onExportFormat()"><span>JSON</span><span class="be-note">à retraiter</span></label>
</div>
</div>
<div class="pe-group">
<div class="pe-gh">Portée</div>
<div class="pe-list">
<div class="pe-row stack">
<div class="xr">
<div class="xr-track"><div class="xr-fill" id="x-fill"></div></div>
<input type="range" class="xr-range" id="x-turns" min="1" max="1" value="1" oninput="onExportPreview()" aria-label="nombre d'échanges à exporter">
</div>
<div class="pe-note" id="x-scope"></div>
</div>
</div>
</div>
<div class="pe-group">
<div class="pe-gh">Contenu</div>
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">Raisonnements<span class="pe-sub">la réflexion du modèle, souvent plus longue que la réponse</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="x-reasoning" checked onchange="onExportPreview()"><span class="pe-sl"></span></label></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Appels d'outils<span class="pe-sub">commandes, lectures et écritures de fichiers</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="x-tools" checked onchange="onExportPreview()"><span class="pe-sl"></span></label></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Sorties des outils<span class="pe-sub">ce que chaque outil a répondu</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="x-results" checked onchange="onExportPreview()"><span class="pe-sl"></span></label></span>
</div>
</div>
</div>
<div class="pe-note" id="x-note"></div>
</div>
<div class="modal-foot" style="justify-content:flex-end">
<button class="pe-btn" onclick="closeExportModal()">annuler</button>
<button class="pe-btn primary" id="x-go" onclick="runExport()">⤓ télécharger</button>
</div>
</div>
</div>
<div id="ask-modal" class="ask-overlay" style="display:none" onclick="if(event.target===this)askResolve(false)">
<div class="ask-box">
<div id="ask-title" class="ask-title"></div>
<div id="ask-msg" class="ask-msg"></div>
<input id="ask-input" class="ask-input" style="display:none">
<!-- Option facultative de la confirmation (ex. « supprimer aussi le .gguf ») :
sa place est ICI, dans la boîte qui pose la question, plutôt que dans le
pied de l'éditeur où elle occupait de la place en permanence. -->
<label id="ask-check" class="pe-delmodel" style="display:none"><input type="checkbox" id="ask-check-input"><span id="ask-check-label"></span></label>
<div class="ask-actions">
<button id="ask-cancel" onclick="askResolve(false)">Annuler</button>
<button id="ask-ok" class="ask-ok" onclick="askResolve(true)">Confirmer</button>
</div>
</div>
</div>
<!-- Détail d'un tracker : ses points, du plus récent au plus ancien. La LISTE
(panneau Mémoire) ne charge jamais les points — un tracker peut en compter
des milliers ; ils n'arrivent qu'ici, à l'ouverture. -->
<div id="track-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeTracker()">
<div class="modal-card" style="max-width:600px">
<div class="modal-head">
<strong id="track-modal-title">Tracker</strong>
<button class="modal-x" onclick="closeTracker()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div class="modal-body">
<div id="track-events" class="mcp-list"></div>
<button class="addbtn" style="margin-top:6px" onclick="addTrackerPoint()">+ ajouter un point</button>
</div>
<div class="modal-foot">
<div class="foot-l">
<button onclick="renameTracker()">renommer</button>
<button onclick="moveTracker()">déplacer</button>
<button class="danger" onclick="deleteTracker()">supprimer</button>
</div>
<div class="foot-r"><button onclick="closeTracker()">fermer</button></div>
</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"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></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 crayon → 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 &amp; compiler</button>
</div>
</div>
</div>
</div>
<div id="mcp-catalog-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeMcpCatalog()">
<div class="modal-card" style="max-width:640px">
<div class="modal-head">
<strong>Catalogue MCP</strong>
<button class="modal-x" onclick="closeMcpCatalog()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div class="modal-body">
<div class="muted" style="font-size:12px;margin-bottom:8px">
Serveurs connus, embarqués dans Loki — aucun appel réseau pour cette liste.
« Utiliser » remplit le formulaire d'ajout : la commande s'exécutera sur
cette machine, relis-la avant d'enregistrer.
</div>
<div id="mcp-catalog-tabs" class="mcp-cat-tabs"></div>
<div id="mcp-catalog-list" class="mcp-cat-list"></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">
<strong id="mcp-modal-title">Serveur MCP</strong>
<button class="modal-x" onclick="closeMcp()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<div class="modal-body mform">
<div class="fld">
<label class="fld-l" for="mcp-name">Nom <span class="opt">(identifiant du serveur, ex: mon-serveur-fichiers)</span></label>
<input id="mcp-name" class="ctl" placeholder="mon-serveur">
</div>
<div class="fld">
<label class="fld-l">Transport</label>
<div class="seg">
<label><input type="radio" name="mcp-transport" value="stdio" onchange="mcpTransportUI('stdio')"><span>stdio (local)</span></label>
<label><input type="radio" name="mcp-transport" value="http" onchange="mcpTransportUI('http')"><span>http (distant)</span></label>
</div>
</div>
<div id="mcp-stdio-fields">
<div class="fld">
<label class="fld-l" for="mcp-command">Commande <span class="opt">(exécutable, ex: npx, uvx, python)</span></label>
<input id="mcp-command" class="ctl" placeholder="npx">
</div>
<div class="fld">
<label class="fld-l" for="mcp-args">Arguments <span class="opt">(un par ligne)</span></label>
<textarea id="mcp-args" class="ctl ta" spellcheck="false" style="min-height:70px" placeholder="-y&#10;@scope/mon-serveur-mcp"></textarea>
</div>
<div class="fld">
<label class="fld-l" for="mcp-env">Variables d'environnement <span class="opt">(KEY=valeur, une par ligne — optionnel)</span></label>
<textarea id="mcp-env" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="API_TOKEN=xxxx"></textarea>
</div>
</div>
<div id="mcp-http-fields" style="display:none">
<div class="fld">
<label class="fld-l" for="mcp-url">URL <span class="opt">(endpoint Streamable HTTP)</span></label>
<input id="mcp-url" class="ctl" placeholder="https://exemple.com/mcp">
</div>
<div class="fld">
<label class="fld-l" for="mcp-headers">En-têtes <span class="opt">(Nom: valeur, un par ligne — ex: Authorization: Bearer …)</span></label>
<textarea id="mcp-headers" class="ctl ta" spellcheck="false" style="min-height:44px" placeholder="Authorization: Bearer sk-…"></textarea>
</div>
</div>
<div id="mcp-modal-status" class="muted" style="font-size:12px;min-height:16px"></div>
<div id="mcp-tools-section" style="display:none">
<div class="subhead" style="margin-top:6px">Outils <span id="mcp-tools-count" class="muted" style="text-transform:none;letter-spacing:0;font-weight:400;font-family:var(--font)"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Décoche un outil pour le masquer à l'IA sans déconnecter le serveur. Utile pour ne garder que ce dont tu as besoin.</span></span></div>
<div id="mcp-tools-list" class="mcp-tools-list"></div>
</div>
</div>
<div class="modal-foot">
<div class="foot-l">
<button id="mcp-del" class="btn-danger" onclick="deleteMcp()" style="display:none">supprimer</button>
</div>
<div class="foot-r">
<button onclick="closeMcp()">annuler</button>
<button class="btn-save" onclick="saveMcp()">enregistrer</button>
</div>
</div>
</div>
</div>
<div id="task-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeTask()">
<div class="modal-card">
<div class="modal-head">
<strong id="task-modal-title">Tâche planifiée</strong>
<button class="modal-x" onclick="closeTask()" aria-label="Fermer">×</button>
</div>
<div class="pe-body">
<div id="task-result-block" class="pe-group" style="display:none">
<div class="pe-gh">Dernier résultat <span id="task-result-when" class="pe-sub" style="display:inline;margin:0"></span></div>
<div id="task-result" class="task-report"></div>
</div>
<div class="pe-group">
<div class="pe-gh">Nom</div>
<div class="pe-list">
<div class="pe-row stack plain"><input id="task-name" class="pe-inp" placeholder="Nom de la tâche"></div>
</div>
</div>
<!-- Type de tâche : une consigne exécutée par l'IA, ou un script du dossier
protégé lancé SEUL — sans charger le modèle ni consommer de tokens.
Une sauvegarde, une synchro, un nettoyage n'ont rien à demander à un
LLM ; les faire passer par lui coûtait un chargement de modèle. -->
<div class="pe-group">
<div class="pe-gh">Type</div>
<div class="pe-seg">
<label><input type="radio" name="task-kind" value="agent" onchange="taskKindUI('agent')"><span>Consigne IA</span></label>
<label><input type="radio" name="task-kind" value="script" onchange="taskKindUI('script')"><span>Script seul</span></label>
</div>
</div>
<div class="pe-group" id="task-prompt-group">
<div class="pe-gh">Consigne</div>
<textarea id="task-prompt" class="pe-inp task-consigne" spellcheck="false" placeholder="Ce que l'IA doit faire, et comment livrer le résultat.&#10;&#10;Tu peux détailler librement : contexte, étapes, format du compte-rendu attendu…"></textarea>
</div>
<div class="pe-group" id="task-script-group" style="display:none">
<div class="pe-gh">Script</div>
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">À exécuter</span>
<span class="pe-row-c"><span class="pe-selc"><select id="task-script"></select></span></span>
</div>
<div class="pe-row stack plain"><div id="task-script-hint" class="muted" style="font-size:11px"></div></div>
</div>
</div>
<div class="pe-group">
<div class="pe-gh">Preset</div>
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">Modèle utilisé</span>
<span class="pe-row-c"><span class="pe-selc"><select id="task-preset"></select></span></span>
</div>
<!-- Projet visé : gouverne la MÉMOIRE et les TRACKERS que la tâche voit
quand elle s'exécute, pas sa visibilité — le panneau reste global,
sinon une tâche semblerait déplanifiée en changeant de projet. -->
<div class="pe-row">
<span class="pe-row-l">Projet visé</span>
<span class="pe-row-c"><span class="pe-selc"><select id="task-project"></select></span></span>
</div>
</div>
</div>
<div class="pe-group">
<div class="pe-gh">Fréquence</div>
<div class="pe-seg">
<label><input type="radio" name="task-freq-mode" value="interval" onchange="taskFreqUI('interval')"><span>Intervalle</span></label>
<label><input type="radio" name="task-freq-mode" value="cron" onchange="taskFreqUI('cron')"><span>Horaire précis</span></label>
</div>
<div class="pe-list" style="margin-top:8px">
<div id="task-interval-fields" class="pe-row">
<span class="pe-row-l">Toutes les</span>
<span class="pe-row-c">
<input id="task-interval-n" class="pe-val" type="number" min="1" value="2" style="width:54px">
<span class="pe-selc"><select id="task-interval-unit" onchange="taskUnitUI()">
<option value="m">minutes</option>
<option value="h" selected>heures</option>
<option value="d">jours</option>
</select></span>
</span>
</div>
<div id="task-time-row" class="pe-row" style="display:none">
<span class="pe-row-l">À</span>
<span class="pe-row-c"><input id="task-time" class="pe-val" type="time" value="09:00" style="width:110px"></span>
</div>
<div id="task-cron-fields" class="pe-row stack" style="display:none">
<input id="task-cron" class="pe-inp" placeholder="0 9 * * 1-5" spellcheck="false" style="font-family:var(--mono)">
<div class="pe-note"><code>minute heure jour mois jour-semaine</code></div>
</div>
</div>
</div>
<div class="pe-group">
<div class="pe-gh">Accès</div>
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">Accès à la mémoire</span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="task-mem" checked><span class="pe-sl"></span></label></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Accès au web</span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="task-web" checked><span class="pe-sl"></span></label></span>
</div>
</div>
</div>
<div class="pe-group">
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">Tâche active</span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="task-enabled" checked><span class="pe-sl"></span></label></span>
</div>
</div>
</div>
<div id="task-modal-status" class="pe-seg-note" style="padding:0 6px"></div>
</div>
<div class="pe-foot">
<div class="foot-l">
<button id="task-del" class="pe-del" onclick="deleteTaskUI()" title="supprimer" aria-label="supprimer" style="display:none"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
</div>
<div class="foot-r">
<button id="task-run" class="pe-btn" onclick="runTaskNow()" style="display:none">tester maintenant</button>
<button class="pe-btn" onclick="closeTask()">annuler</button>
<button class="pe-btn primary" onclick="saveTask()">enregistrer</button>
</div>
</div>
</div>
</div>
<div id="modal" class="modal-ov" style="display:none">
<div class="modal-card">
<div class="modal-head">
<strong id="modal-title">Preset</strong>
<button class="modal-x" onclick="closeModal()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div>
<!-- Chargement : la modale s'ouvre tout de suite, mais son contenu n'arrive
qu'après (jusqu'à 3 requêtes en accès distant). Ce voile se pose PAR-DESSUS
le formulaire, qui reste en place (invisible) : la carte garde sa taille
finale, donc pas de saut au moment où le contenu apparaît. -->
<div class="modal-load"><span class="spinner"></span></div>
<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 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 stack">
<span class="pe-row-l">Fichier</span>
<span class="pe-selc wide"><select id="m-model" onchange="onPickModel()"></select></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">
<span class="pe-row-l">Vision<span class="pe-sub">projecteur mmproj</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="m-mmproj" onchange="onPickMmproj()"></select></span></span>
</div>
<!-- Recherche Hugging Face. Le champ « lien direct » juste en dessous
reste là : c'est le mode expert (dépôt privé, fichier exotique,
lien reçu d'ailleurs). Celui-ci évite d'avoir à quitter Loki, et
surtout il propose le projecteur vision DU MÊME DÉPÔT — le seul
qui corresponde au modèle choisi. -->
<div class="pe-row stack">
<span class="pe-row-l">Chercher un modèle<span class="pe-sub">sur Hugging Face — dépôts GGUF uniquement</span></span>
<div class="pe-inpbtn">
<input id="hf-q" class="pe-inp" placeholder="qwen3.8, mistral, gemma…" onkeydown="if(event.key==='Enter'){event.preventDefault();hfSearch();}">
<button id="hf-go" onclick="hfSearch()" title="Chercher">⌕</button>
</div>
<div id="hf-out"></div>
</div>
<div class="pe-row stack">
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">ou un projecteur mmproj — 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" aria-label="Télécharger"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/><path d="M4.5 19.5h15"/></svg></button>
</div>
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
<button id="m-hf-cancel" class="pe-link" onclick="cancelDownload()" style="display:none">Annuler le téléchargement</button>
</div>
<div class="pe-row">
<span class="pe-row-l">Destination</span>
<span class="pe-selc"><select id="m-hf-dir"></select></span>
</div>
<div id="m-dirs-toggle" class="pe-row" style="cursor:pointer" onclick="toggleModelDirs()">
<span class="pe-row-l">Dossiers de modèles<span class="pe-sub">chercher les .gguf ailleurs (disque externe…)</span></span>
<span class="pe-row-c"><span id="m-dirs-caret" class="pe-caret"></span></span>
</div>
<div id="m-dirs-body" class="pe-row stack" style="display:none">
<div id="m-dirs-list" class="pe-note"></div>
<div class="pe-inpbtn">
<input id="m-dir-path" class="pe-inp" placeholder="/media/disque-externe/models">
<button id="m-dir-add" onclick="addModelDir()" title="Ajouter ce dossier">+</button>
</div>
<div class="pe-note">Ces dossiers servent aussi de destination de téléchargement.</div>
</div>
<!-- Modèles installés. Un .gguf survit à la suppression de son preset
(c'est voulu : on le réutilise dans un autre). Encore faut-il
pouvoir le REPRENDRE et, un jour, l'effacer — sans ça les
fichiers gardés devenaient invisibles et indéboulonnables. -->
<div id="m-inst-toggle" class="pe-row" style="cursor:pointer" onclick="toggleInstalled()">
<span class="pe-row-l">Modèles installés<span class="pe-sub">les .gguf présents sur le disque — et de quoi en effacer un</span></span>
<span class="pe-row-c"><span id="m-inst-state" class="pe-sub"></span><span id="m-inst-caret" class="pe-caret"></span></span>
</div>
<div id="m-inst-body" class="pe-row stack" style="display:none">
<div id="m-inst-list"></div>
</div>
<!-- Jeton Hugging Face. Replié comme « Dossiers de modèles » : on n'y
touche qu'une fois, le jour où un dépôt à accès restreint refuse
le téléchargement. Il vaut aussi pour la recherche, d'où sa place
dans ce groupe plutôt qu'à côté de la clé API du serveur, qui ne
parle pas du tout de la même chose. -->
<div id="m-hft-toggle" class="pe-row" style="cursor:pointer" onclick="toggleHFToken()">
<span class="pe-row-l">Jeton Hugging Face<span class="pe-sub">dépôts à accès restreint (« gated ») ou privés</span></span>
<span class="pe-row-c"><span id="m-hft-state" class="pe-sub"></span><span id="m-hft-caret" class="pe-caret"></span></span>
</div>
<div id="m-hft-body" class="pe-row stack" style="display:none">
<div class="pe-inpbtn">
<input id="m-hft-input" class="pe-inp" type="password" autocomplete="off" spellcheck="false" placeholder="hf_…" onkeydown="if(event.key==='Enter'){event.preventDefault();saveHFToken();}">
<button id="m-hft-save" onclick="saveHFToken()" title="Vérifier et enregistrer" aria-label="Enregistrer">✓</button>
</div>
<div id="m-hft-note" class="pe-note"></div>
<button id="m-hft-clear" class="pe-link" onclick="clearHFToken()" style="display:none">Retirer le jeton</button>
</div>
</div>
</div>
<!-- Cartes graphiques : la liste vient du moteur de la machine (celui de
l'image ou celui mis à jour dans Réglages → Moteur — le moteur n'est
pas un réglage de preset), car les noms ET l'ordre dépendent du backend
compilé dedans. Le
groupe reste masqué tant qu'on n'a pas AU MOINS DEUX cartes : avec
une seule, il n'y a rien à arbitrer. -->
<div class="pe-group" id="m-gpu-group" hidden>
<div class="pe-gh">Cartes graphiques</div>
<div class="pe-list" id="m-gpu-list"></div>
<div class="pe-note" id="m-gpu-note"></div>
<!-- Guide de placement (placementGuideHtml) : ordre des cartes, liaison
PCIe, marges FIT_TARGET. Un conseil, jamais appliqué d'office. -->
<div class="pe-note" id="m-gpu-guide" hidden></div>
</div>
</div>
<!-- Une seule liste : les anciens « réglages avancés » repliés sont
désormais à la suite. Les valeurs d'abord, puis TOUS les interrupteurs
groupés en fin de liste — chercher un toggle perdu entre deux champs
numériques n'aidait personne. -->
<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 ou auto = ce qui tient en VRAM · all = tout, quitte à saturer</span></span>
<span class="pe-row-c"><input id="s-ngl" class="pe-val" type="text" inputmode="numeric" placeholder="999" oninput="cfgWriteKey('NGL', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Experts MoE sur CPU<span class="pe-sub">vide = non · sur 2 GPU, coupe le pipeline entre cartes (prompt plus lent)</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">Threads CPU<span class="pe-sub">vide ou 0 = cœurs physiques</span></span>
<span class="pe-row-c"><input id="s-threads" class="pe-val" type="number" min="0" step="1" placeholder="auto" oninput="cfgWriteKey('THREADS', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Threads (batch)<span class="pe-sub">prefill et MTP · vide ou 0 = comme Threads CPU</span></span>
<span class="pe-row-c"><input id="s-tbatch" class="pe-val" type="number" min="0" step="1" placeholder="auto" oninput="cfgWriteKey('THREADS_BATCH', this.value)"></span>
</div>
<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">
<!-- Cache de prompts en RAM hôte (--cache-ram, Mio) : copie exacte de
l'état, rechargée quand la conversation revient. Vide = auto, la
valeur calculée s'affiche en indication (refreshCacheRAM). -->
<span class="pe-row-l">Cache de prompts (Mio)<span class="pe-sub" id="s-cram-sub">RAM hôte, copie exacte du contexte · jamais la VRAM</span></span>
<span class="pe-row-c"><input id="s-cram" class="pe-val" type="number" min="-1" step="1024" placeholder="auto" oninput="cfgWriteKey('CACHE_RAM', this.value)"></span>
</div>
<div class="pe-row">
<!-- Seul f16 laisse les calculs du modèle intacts : chaque autre
option le dit. Le sous-titre montre le type EFFECTIF — un
-ctk/-ctv d'EXTRA_ARGS l'emporte sur cette liste (syncKVSub). -->
<span class="pe-row-l">Cache KV<span class="pe-sub" id="s-kv-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); syncKVSub()">
<option value="">f16 (max qualité)</option>
<option value="q8_0">q8_0 (léger · modifie légèrement les sorties)</option>
<option value="q4_0">q4_0 (très léger · perte mesurable)</option>
<option value="q5_1">q5_1 (lent sur CUDA · modifie les sorties)</option>
<option value="bf16">bf16 (numérique différente de f16)</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : « auto », « MTP », « n-grammes du contexte » et
« MTP + n-grammes » écrivent la clé SPEC,
que Loki traduit au lancement (tête MTP détectée dans le fichier,
garde-fous VRAM) ; les autres types vont tels quels dans
--spec-type. « non » écrit SPEC=off. Le nombre de jetons anticipés
(--spec-draft-n-max) n'a de sens qu'une fois un type choisi, donc
sa ligne n'apparaît qu'à ce moment. -->
<div class="pe-row">
<span class="pe-row-l">Décodage spéculatif<span class="pe-sub">anticipe des jetons pour accélérer</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-spec" onchange="onSpecType()">
<option value="">non</option>
<option value="auto">auto (MTP si le modèle en a une et que la VRAM suit)</option>
<option value="ngram">n-grammes du contexte (expérimental · mode code, à mesurer)</option>
<option value="mtp+ngram">MTP + n-grammes (expérimental · à mesurer)</option>
<optgroup label="Modèle brouillon">
<option value="draft-mtp">MTP (imposé)</option>
<option value="draft-eagle3">EAGLE-3</option>
<option value="draft-simple">modèle brouillon séparé</option>
<option value="draft-dflash">dFlash</option>
<option value="draft-dspark">dSpark</option>
</optgroup>
<optgroup label="N-grammes (sans modèle brouillon)">
<option value="ngram-simple">n-grammes</option>
<option value="ngram-mod">n-grammes (mod)</option>
<option value="ngram-cache">n-grammes (cache)</option>
<option value="ngram-map-k">n-grammes (map-k)</option>
<option value="ngram-map-k4v">n-grammes (map-k4v)</option>
</optgroup>
</select></span></span>
</div>
<div class="pe-row" id="s-spec-n-row" style="display:none">
<span class="pe-row-l">Jetons anticipés<span class="pe-sub">par étape — défaut 3</span></span>
<span class="pe-row-c"><input id="s-spec-n" class="pe-val" type="number" min="1" step="1" placeholder="3" oninput="eaSetValued('--spec-draft-n-max', this.value)"></span>
</div>
<!-- Tête MTP publiée à part (mtp-*.gguf) ou petit modèle brouillon :
clé MODEL_DRAFT, résolue au lancement comme le projecteur vision. -->
<div class="pe-row" id="s-draft-row" style="display:none">
<span class="pe-row-l">Brouillon<span class="pe-sub">tête MTP à part ou petit modèle</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="m-draft" onchange="onPickDraft()"></select></span></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-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':'off')"><span class="pe-sl"></span></label></span>
</div>
<div class="pe-row" id="s-reasoning-effort-row">
<span class="pe-row-l">Intensité du raisonnement<span class="pe-sub" id="s-reasoning-effort-sub">défaut de ce preset — ajustable à la volée depuis la barre de saisie</span></span>
<span class="pe-row-c">
<select id="s-reasoning-effort" class="pe-val" onchange="cfgWriteKey('REASONING_EFFORT', this.value)">
<option value="">auto</option>
<option value="none">aucune</option>
<option value="low">basse</option>
<option value="medium">moyenne</option>
<option value="high">haute</option>
<option value="xhigh">maximale</option>
</select>
</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>
<!-- Chargement du modèle : --load-mode de llama.cpp (remplace --mlock et
--no-mmap). Pour un moteur ancien, loki serve retraduit au lancement
(normalizeLoadFlags). -->
<div class="pe-row">
<span class="pe-row-l">Chargement<span class="pe-sub" id="s-loadmode-sub"></span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-loadmode" onchange="eaSetLoadMode(this.value)">
<option value="">auto</option>
<option value="mmap">mmap</option>
<option value="none">none</option>
<option value="mlock">mlock</option>
<option value="mmap+mlock">mmap+mlock</option>
<option value="dio">dio</option>
</select></span></span>
</div>
</div>
</div>
<!-- Échantillonnage (repris de l'amont AJEAN v0.9.5/v0.9.6) : ces valeurs
partent dans CHAQUE requête de complétion, pas sur la ligne de commande
du moteur — c'est le seul endroit où elles prennent effet. Un champ vide
n'envoie rien, llama-server garde son défaut. Les valeurs recommandées
dépendent du modèle (Qwen3.8 en réflexion : temp 1.0, top_p 0.95,
top_k 20, min_p 0). -->
<div id="m-sampling-row" class="pe-group" style="display:none">
<div class="pe-gh">Échantillonnage</div>
<div class="pe-list">
<div class="pe-row">
<span class="pe-row-l">Température<span class="pe-sub">vide = défaut du modèle</span></span>
<span class="pe-row-c"><input id="s-temp" class="pe-val" type="number" min="0" step="0.05" placeholder="—" oninput="cfgWriteKey('TEMP', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">top_p<span class="pe-sub">noyau de probabilité</span></span>
<span class="pe-row-c"><input id="s-topp" class="pe-val" type="number" min="0" max="1" step="0.01" placeholder="—" oninput="cfgWriteKey('TOP_P', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">top_k<span class="pe-sub">0 = désactivé</span></span>
<span class="pe-row-c"><input id="s-topk" class="pe-val" type="number" min="0" step="1" placeholder="—" oninput="cfgWriteKey('TOP_K', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">min_p<span class="pe-sub">seuil de probabilité</span></span>
<span class="pe-row-c"><input id="s-minp" class="pe-val" type="number" min="0" max="1" step="0.01" placeholder="—" oninput="cfgWriteKey('MIN_P', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Pénalité de présence<span class="pe-sub">pousse vers de nouveaux mots (0 = off)</span></span>
<span class="pe-row-c"><input id="s-presp" class="pe-val" type="number" min="0" max="2" step="0.1" placeholder="—" oninput="cfgWriteKey('PRESENCE_PENALTY', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Pénalité de répétition<span class="pe-sub">anti-répétition (1 = neutre)</span></span>
<span class="pe-row-c"><input id="s-reppen" class="pe-val" type="number" min="0" step="0.05" placeholder="—" oninput="cfgWriteKey('REPEAT_PENALTY', this.value)"></span>
</div>
</div>
</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 de configuration<span class="pe-sub">texte brut</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" oninput="syncKVSub()"></textarea>
</div>
</div>
</div>
</div>
<!-- Pied compact, sur UNE ligne : la suppression est une icône (action rare et
destructrice, elle n'a pas à peser autant que « enregistrer »), et sa case
« supprimer aussi le .gguf » se réduit à l'extension, le libellé complet
restant en infobulle. -->
<div class="pe-foot">
<div class="foot-l">
<button id="m-del" class="pe-del" onclick="delItem()" title="supprimer" aria-label="supprimer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
</div>
<div class="foot-r">
<button class="pe-btn" onclick="closeModal()">annuler</button>
<button class="pe-btn primary" onclick="saveItem()">enregistrer</button>
</div>
</div>
</div>
</div>
<script src="marked.min.js"></script>
<script>
@@JS@@
</script></body></html>