mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 11:49:43 +02:00
Installer un modèle demandait d'aller sur huggingface.co, de naviguer dans l'arborescence d'un dépôt et de coller un lien à la main. Rien ne disait si le fichier tiendrait en mémoire, et surtout rien ne reliait un modèle à SON projecteur vision : c'est ainsi qu'un mmproj-Qwen3VL-8B s'est retrouvé configuré pour un Qwen3.8-27B — deux modèles sans rapport, moteur qui démarre et ne voit rien. La recherche Hugging Face arrive dans l'éditeur de preset. Elle ne remonte que les dépôts GGUF ; déplier un dépôt montre ses quantifications avec leur taille et un verdict mémoire, et propose le projecteur vision DU MÊME DÉPÔT — le seul qui corresponde. Quand le dépôt n'en publie pas, Loki le dit au lieu d'aller en chercher un ailleurs. Le nouveau code ne télécharge rien : il produit des URL que le chemin existant consomme tel quel (normalizeHFURL, shardURLSet, sonde d'espace disque, reprise et annulation). Une file d'attente enchaîne modèle puis projecteur — le serveur ne mène qu'un transfert à la fois, et le champ Vision ne se remplit que si le modèle est déjà sélectionné. Trois familles de .gguf cohabitent dans un dépôt et ne veulent pas dire la même chose : mmproj-* (projecteur), mtp-* (poids de décodage spéculatif) et le modèle. Les deux premières ressemblent à un modèle ; les proposer en vrac, ce serait offrir de lancer llama-server sur un encodeur d'images. Les tranches d'une famille sont repliées en une entrée de taille TOTALE : annoncer 15 Go pour un modèle qui en occupe 45 promet une place qui n'existe pas. Le verdict mémoire s'appuie enfin sur le GPU. detectHardware() ne renvoyait que la RAM système alors que detectGPUs() existait déjà : sur un serveur à carte NVIDIA, le verdict se prononçait sur la mauvaise grandeur. Le coût du cache KV reste une estimation assumée — l'exact demanderait de parser l'en-tête GGUF — et l'interface l'annonce comme telle plutôt que d'afficher un chiffre faussement sûr. Le catalogue ajean.link disparaît. Sa route n'avait aucun consommateur (l'écran d'accueil qu'elle attendait n'a jamais existé), son repli embarqué proposait du Qwen2.5 de 2024, et un fork qui laisse le serveur de l'amont décider de ce qu'il propose n'est pas vraiment un fork. Une piste écartée en cours de route : marquer les dépôts « vision » d'après les tags Hugging Face. Ils mentent — des deux dépôts GGUF de Qwen3.8-27B qui publient tous deux un mmproj, seul ggml-org est taggé image-text-to-text. Une pastille sur l'un et pas sur l'autre aurait été pire que rien. La vision est donc déduite de la seule source qui ne se trompe pas : la présence d'un mmproj-*.gguf dans l'arborescence. Vérifié : 8 tests unitaires sur les arborescences réelles des deux dépôts, puis au navigateur contre le vrai Hugging Face — 25 dépôts trouvés, 3 quants listés sans aucun mtp ni mmproj, projecteur Q8_0 proposé et coché, verdicts affichés avec leur explication, modale dans l'écran. L'ordre de la file (modèle puis projecteur) est testé en interceptant les appels, sans transfert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VueWA9xcYadaYq65tBisix
859 lines
62 KiB
HTML
859 lines
62 KiB
HTML
<!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='%23000'/><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,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABBUlEQVR42u3SAQkAAAwCQfuX3lI4GNxF0E8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIArU2NbcYhDHOIQhzgQB+JAHIgDcSAOxIE4xCEOcYhDHIgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQBziEIc4xCEOcYgDcSAOxIE4EAfiQByIQxziEIc4xIE4EAfiQByIA3EgDsQhDnGIQxziEIc4EAfiQByIAxciDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAc4hCHOMQhDnGIA3EgDsSBOBAH4kAciEMc4hCHOMQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADfLBHMhi4TnG6EAAAAAElFTkSuQmCC">
|
||
<!-- 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-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('loki-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||
<style>
|
||
@@CSS@@
|
||
</style></head><body>
|
||
<button id="menubtn" onclick="toggleSide()">☰</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">
|
||
<!-- sidecard : regroupe la marque et la mention du fork en UNE carte (direction
|
||
Terre). Sans ce conteneur, les deux blocs flottaient séparément. -->
|
||
<div class="sidecard">
|
||
<div class="sidehead">
|
||
<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>
|
||
<span id="status-svc" class="statuspill" onclick="toggleSvcLog()" title="voir le journal du moteur">…</span>
|
||
</div>
|
||
<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><!-- /sidecard -->
|
||
<!-- 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. -->
|
||
<details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion">+</button></summary>
|
||
<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:8px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn) 45%,transparent);background:color-mix(in srgb,var(--warn) 12%,transparent)"></div>
|
||
<div id="model-err" style="display:none;font-size:11px;line-height:1.5;margin:0 0 10px;padding:8px 10px;border-radius:8px;color:var(--err);border:1px solid color-mix(in srgb,var(--err) 45%,transparent);background:color-mix(in srgb,var(--err) 12%,transparent)"></div>
|
||
<!-- Journal du moteur : replié par défaut, ouvert au clic sur la pastille d'état
|
||
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. -->
|
||
<div class="sidegroup">
|
||
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
||
<div class="row" style="justify-content:flex-end;align-items:center">
|
||
<button onclick="loadSvcLog()" title="rafraîchir">↻</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>
|
||
</details>
|
||
<!-- GPU et RAM décrivent la même chose (les ressources de la machine) :
|
||
une seule section, deux blocs de statistique dans la même carte. -->
|
||
<details open><summary><span class="stitle">Machine</span></summary>
|
||
<div class="slist">
|
||
<div id="vram">…</div>
|
||
<div id="ram-details"><div id="ram"></div></div>
|
||
</div>
|
||
</details>
|
||
<details open id="cfg-details"><summary><span class="stitle">Config active</span></summary>
|
||
<div id="cfg" class="slist">…</div>
|
||
</details>
|
||
<details><summary><span class="stitle">Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
|
||
<div class="slist"><div id="presets" class="rowlist"></div></div>
|
||
</details>
|
||
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
|
||
<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>
|
||
<button class="addbtn" style="margin-top:6px" onclick="openMcp('')">+ ajouter un serveur</button>
|
||
</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>
|
||
</details>
|
||
<details><summary><span class="stitle">System prompt</span></summary>
|
||
<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>
|
||
</details>
|
||
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
||
<!-- 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 ne s'applique
|
||
(rien à installer, et l'image n'embarque ni cmake ni compilateur). -->
|
||
<div id="lc-provided" class="rowbody" style="display:none">
|
||
<div style="font-size:12px;line-height:1.5">Moteur <b>fourni par l'image Docker</b> — llama.cpp officiel, déjà compilé.
|
||
<div class="muted" style="margin-top:4px">Rien à installer ni à mettre à jour ici : le moteur suit l'image.
|
||
Pour en changer (version épinglée, CPU, Vulkan), reconstruis l'image avec le build-arg <code>LLAMACPP_IMAGE</code>.</div>
|
||
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></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>
|
||
</details>
|
||
</div><!-- /sidegroup -->
|
||
<!-- ── 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. -->
|
||
<div class="side-sep">Paramètres</div>
|
||
<!-- 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. -->
|
||
<div class="sidegroup">
|
||
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||
<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>
|
||
</details>
|
||
<details><summary><span class="stitle">Apparence</span></summary>
|
||
<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-side" onchange="toggleView('hide-side')"><span class="slider"></span></span> barre latérale escamotable<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">La barre latérale devient un tiroir <b>même sur ordinateur</b> : elle reste fermée et s'ouvre avec le bouton ☰. Toute la largeur pour la conversation.</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>
|
||
</details>
|
||
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
||
<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…) sur ces adresses. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer <clé></code>.</span></span></div>
|
||
<!-- UNE carte pour tout l'endpoint : adresse locale, bascule « public » et
|
||
adresse publique sont un seul sujet — trois cartes accolées donnaient
|
||
trois îlots pour un même réglage. -->
|
||
<div class="slist">
|
||
<div class="rowbody">
|
||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — 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>
|
||
<!-- L'adresse ci-dessus ne sert à RIEN si le moteur n'écoute que sur la
|
||
boucle locale (le défaut sous Windows) : le chat du navigateur marche,
|
||
mais aucun logiciel du réseau ne peut s'y brancher. Cet interrupteur
|
||
règle HOST et pose la règle de pare-feu, voir sys_network.go. -->
|
||
<label class="switchrow"><span class="switch"><input type="checkbox" id="lan-toggle" onchange="toggleLAN()"><span class="slider"></span></span> joignable depuis le réseau local<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Éteint, le moteur n'écoute que sur <b>cette machine</b> : l'adresse ci-dessus ne répond qu'ici. Allumez-le pour brancher un logiciel depuis un autre ordinateur du réseau (sous Windows, la règle de pare-feu est posée en même temps). <b>Redémarre le moteur.</b></span></span></label>
|
||
<div id="lan-warn" class="rowbody" style="display:none"></div>
|
||
<label class="switchrow"><span class="switch"><input type="checkbox" id="oai-public-toggle" onchange="toggleOAIPublic()"><span class="slider"></span></span> exposer en public (ajean.link)<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Rend votre IA joignable depuis <b>n'importe où</b> via <code>https://<machine>.oai.ajean.link/v1</code> (pour brancher un SaaS). Le trafic est chiffré <b>de bout en bout</b> jusqu'à ce serveur — le relais ajean.link ne voit rien. Nécessite l'accès distant actif.</span></span></label>
|
||
<div id="oai-public-wrap" class="rowbody" style="display:none">
|
||
<div class="muted" style="margin:0 0 6px">Adresse publique — chiffrée de bout en bout, accessible partout</div>
|
||
<div class="row" style="margin:0">
|
||
<input id="oai-public-url" readonly class="io-input">
|
||
<button class="iconbtn" onclick="copyText(document.getElementById('oai-public-url').value,'adresse publique 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>
|
||
</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 (llama-server). Indispensable avant d'exposer le serveur sur internet. Modifier la clé <b>redémarre</b> le service.</span></span></div>
|
||
<div class="slist"><div class="rowbody">
|
||
<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>
|
||
</details>
|
||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||
<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()">⌫ clear chat</button>
|
||
</div>
|
||
</div></div>
|
||
</details>
|
||
</div><!-- /sidegroup -->
|
||
</div>
|
||
<div class="main" style="position:relative">
|
||
<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">↓</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">
|
||
<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>
|
||
<!-- Postes distants : l'icône dit SUR QUOI l'IA agit. Écran = ce serveur
|
||
(cible locale) ; ondes wifi = un poste distant est la cible active.
|
||
Un clic ouvre la modale de gestion (cible, liste, appairage). -->
|
||
<button id="node-btn" onclick="openNodeHub()" title="Postes distants" aria-label="Postes distants">
|
||
<svg class="ic-here" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||
<svg class="ic-remote" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 8.82a15 15 0 0 1 20 0"/><path d="M5 12.55a11 11 0 0 1 14 0"/><path d="M8.5 16.1a6 6 0 0 1 7 0"/><path d="M12 20h.01"/></svg>
|
||
</button>
|
||
<input type="file" id="attach-input" multiple style="display:none" onchange="onAttachPick(event)">
|
||
<button id="attach" onclick="document.getElementById('attach-input').click()" title="joindre un fichier" aria-label="joindre un fichier">
|
||
<!-- Trombone dessiné en SVG : pas de dépendance à une police d'emoji,
|
||
dont le rendu varie énormément d'un système à l'autre. -->
|
||
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.4 11.05 12.25 20.2a5.5 5.5 0 0 1-7.78-7.78l9.19-9.19a3.67 3.67 0 1 1 5.18 5.18l-9.2 9.2a1.83 1.83 0 1 1-2.59-2.6l8.49-8.48"/></svg>
|
||
</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…" onkeydown="onKey(event)" oninput="autoGrow(this)" onpaste="onPaste(event)"></textarea>
|
||
<button id="send" onclick="send()">send</button>
|
||
<button id="stop" onclick="stopGen()" style="display:none;background:var(--danger-bg);border-color:var(--err);color:var(--err)">stop</button>
|
||
</div>
|
||
<div id="sendhint" class="muted">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
|
||
</div>
|
||
</div>
|
||
<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:8px;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">×</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. -->
|
||
<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">×</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">×</button>
|
||
</div>
|
||
<div class="modal-body mform">
|
||
<div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton ✎ → section Moteur → « backend détecté »).</div>
|
||
<div class="fld">
|
||
<label class="fld-l">Installés</label>
|
||
<div id="lc-custom-list" class="mcp-list"></div>
|
||
</div>
|
||
<div class="fld">
|
||
<label class="fld-l" for="lc-custom-url">Installer depuis un dépôt Git</label>
|
||
<input id="lc-custom-url" class="ctl" placeholder="https://github.com/owner/llama.cpp-fork">
|
||
</div>
|
||
<div class="fld">
|
||
<label class="fld-l" for="lc-custom-name">Nom <span class="opt">(optionnel — déduit de l'URL sinon)</span></label>
|
||
<input id="lc-custom-name" class="ctl" placeholder="llama.cpp-prism">
|
||
</div>
|
||
</div>
|
||
<div class="modal-foot">
|
||
<div class="foot-l"></div>
|
||
<div class="foot-r">
|
||
<button onclick="closeCustomBackends()">fermer</button>
|
||
<button class="btn-save" onclick="lcInstallCustom()">Installer & compiler</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="mcp-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeMcp()">
|
||
<div class="modal-card" style="max-width:560px">
|
||
<div class="modal-head">
|
||
<strong id="mcp-modal-title">Serveur MCP</strong>
|
||
<button class="modal-x" onclick="closeMcp()" aria-label="Fermer">×</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 @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="node-hub-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)hideModal('node-hub-modal')">
|
||
<div class="modal-card">
|
||
<div class="modal-head">
|
||
<strong>Postes distants</strong>
|
||
<button class="modal-x" onclick="hideModal('node-hub-modal')" aria-label="Fermer">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="muted" style="font-size:12px;line-height:1.5;margin-bottom:10px">
|
||
Pilote un <b>autre PC</b> depuis l'IA. Choisis sur quelle machine l'IA agit
|
||
(ses outils shell/fichiers) et appaire de nouveaux postes. Tu décides des
|
||
capacités et du dossier autorisé pour chaque poste.
|
||
</div>
|
||
<div id="node-hub-list" class="mcp-list"></div>
|
||
<button class="addbtn" style="margin-top:10px" onclick="openNodePair()">+ appairer un poste</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="node-pair-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)hideModal('node-pair-modal')">
|
||
<div class="modal-card">
|
||
<div class="modal-head">
|
||
<strong>Appairer un poste distant</strong>
|
||
<button class="modal-x" onclick="hideModal('node-pair-modal')" aria-label="Fermer">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="fld">
|
||
<label class="fld-l">Capacités autorisées <span class="opt">(ce que l'IA pourra faire sur ce poste)</span></label>
|
||
<div id="node-pair-caps" class="node-caps"></div>
|
||
</div>
|
||
<div class="fld">
|
||
<label class="fld-l" for="node-pair-root">Dossier autorisé <span class="opt">(confine lecture/écriture ; laisser vide = dossier de travail par défaut du poste)</span></label>
|
||
<input id="node-pair-root" class="ctl" placeholder="C:\\Users\\moi\\loki ou /home/moi/loki" spellcheck="false">
|
||
</div>
|
||
<div id="node-pair-out" style="display:none">
|
||
<div class="subhead" style="margin-top:6px">Code d'appairage</div>
|
||
<div class="node-code" id="node-pair-code">——</div>
|
||
<div class="muted" style="font-size:12px;line-height:1.5">
|
||
Sur le PC à piloter, installe <b>loki</b> (ajean.link/download) puis lance — <b>accès à distance</b>, chiffré de bout en bout :
|
||
<pre class="node-cmd" id="node-pair-cmd"></pre>
|
||
<span style="opacity:.8">Ou en <b>réseau local</b> (connexion directe) :</span>
|
||
<pre class="node-cmd" id="node-pair-cmd-lan"></pre>
|
||
Le code expire dans <span id="node-pair-ttl">10</span> min et ne sert qu'une fois.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-foot">
|
||
<div class="foot-l"></div>
|
||
<div class="foot-r">
|
||
<button onclick="hideModal('node-pair-modal')">fermer</button>
|
||
<button class="btn-save" onclick="genNodeCode()">générer le code</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">×</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">↓</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>
|
||
</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><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">
|
||
<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>
|
||
|
||
<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>
|