Files
Loki/internal/loki/ui/src/index.tmpl.html
T
MichaelandClaude Fable 5 69bf8d985d Vérification du moteur de retour ; boutons MCP au gabarit ; « Paramètres »
- Le bouton « vérifier les mises à jour » revient dans la carte Moteur : il
  ne fait QUE regarder, là où « mettre à jour » télécharge et bascule après
  confirmation. Deux gestes qui n'engagent pas pareil, deux boutons.
- Les boutons « catalogue » / « + ajouter un serveur » (MCP) étaient des
  pilules arrondies au milieu de boutons rectangulaires : même gabarit que
  leurs voisins (4 px, 12 px, mêmes marges).
- « Réglages » devient « Paramètres » dans la barre latérale et en tête de
  la modale.

Version 0.12.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 08:13:21 +02:00

1148 lines
85 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">
<!-- 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">
<!-- 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>
<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">
<div class="perf-head"><span>Performance</span></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. -->
<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 (whisper.cpp
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) — 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 onclick="closeBenchModal()">fermer</button>
<button id="bench-rerun" onclick="runBenchUI()" style="border-color:var(--accent);color:var(--accent)">relancer</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="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>
<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>
<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();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>
<!-- 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</span>
<select id="mem-mode" onchange="setMemMode()" class="sctl">
<option value="always">auto</option>
<option value="ondemand">sur demande</option>
<option value="off">désactivée</option>
</select>
</label>
<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>
</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>
<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-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>
</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 trois 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. -->
<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).</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>
</div>
</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">
<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>
<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>
<div class="pe-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">
<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>
</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>
<!-- 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>
<div class="pe-group">
<div class="pe-gh">Moteur</div>
<!-- « Image » n'apparaît qu'en conteneur (moteur fourni), où
« Précompilé » et « Compilé » sont au contraire masqués : ils
désignent des installations que Loki ne fait pas ici. -->
<div class="pe-seg">
<label id="be-lab-image" style="display:none"><input type="radio" name="m-be" value="image" onchange="onBackendMode('image')"><span>Image</span><span class="be-note" id="be-image-note"></span></label>
<label id="be-lab-fast"><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Précompilé</span><span class="be-note" id="be-fast-note"></span></label>
<label id="be-lab-opt"><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Compilé</span><span class="be-note" id="be-opt-note"></span></label>
<label id="be-lab-custom"><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
</div>
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
<div class="pe-row">
<span class="pe-row-l">Backend</span>
<span class="pe-row-c"><span class="pe-selc"><select id="m-backend-detected" onchange="onPickDetected()"></select></span></span>
</div>
<div class="pe-row stack">
<input id="m-backend-path" class="pe-inp" oninput="onCustomPath()" placeholder="ou un chemin vers llama-server(.exe)">
<div class="pe-note" id="be-drop-hint"></div>
</div>
</div>
</div>
<!-- Cartes graphiques : la liste vient du moteur choisi juste au-dessus,
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>
</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 = tout sur le GPU · auto = ce qui tient en VRAM</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</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">0 = auto</span></span>
<span class="pe-row-c"><input id="s-threads" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Threads (batch)<span class="pe-sub">0 = auto</span></span>
<span class="pe-row-c"><input id="s-tbatch" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS_BATCH', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">BATCH</span>
<span class="pe-row-c"><input id="s-batch" class="pe-val" type="number" min="0" step="256" placeholder="2048" oninput="cfgWriteKey('BATCH', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">UBATCH</span>
<span class="pe-row-c"><input id="s-ubatch" class="pe-val" type="number" min="0" step="128" placeholder="512" oninput="cfgWriteKey('UBATCH', this.value)"></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Cache KV<span class="pe-sub">compression du contexte</span></span>
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value)">
<option value="">f16 (max qualité)</option>
<option value="q8_0">q8_0 (léger)</option>
<option value="q4_0">q4_0 (très léger)</option>
<option value="q5_1">q5_1</option>
<option value="bf16">bf16</option>
</select></span></span>
</div>
<!-- Décodage spéculatif : le type vient de la liste que llama-server
accepte pour --spec-type. 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>
<optgroup label="Modèle brouillon">
<option value="draft-mtp">MTP (intégré au modèle)</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>
<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>
<div class="pe-row">
<span class="pe-row-l">Garder en RAM<span class="pe-sub">évite le swap (mlock)</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-mlock" onchange="eaToggleFlag('--mlock', this.checked)"><span class="pe-sl"></span></label></span>
</div>
<div class="pe-row">
<span class="pe-row-l">Charger tout en mémoire<span class="pe-sub">désactive le mmap</span></span>
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-nommap" onchange="eaToggleFlag('--no-mmap', this.checked)"><span class="pe-sl"></span></label></span>
</div>
</div>
</div>
<!-- É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"></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>