Files
Loki/internal/jean/ui/src/index.tmpl.html
T
nathaninline ef5b036d99 backend llama.cpp : choix du moteur PAR MODELE (3 options), la barre laterale n'installe plus que
Corrige la confusion : deux endroits ecrivaient la ligne BIN et se battaient
(la barre laterale changeait le BIN global + redemarrait, puis selectionner un
preset remettait le BIN du preset). Desormais :
- la section laterale 'Backend llama.cpp' sert UNIQUEMENT a installer / mettre
  a jour les moteurs (rapide / optimisee), elle ne change plus le BIN actif
- le choix du moteur se fait PAR MODELE, dans l'editeur de preset, avec 3
  options claires : Rapide (precompile) / Optimise (compile) / Personnalise
  (chemin libre ou backend depose dans le dossier backends de jean, detecte)
- detection auto de l'option a l'ouverture selon le BIN du preset ; l'astuce
  indique le dossier ou deposer un binaire custom
- /api/llamacpp expose backends_dir ; l'endpoint /use n'est plus utilise par l'UI

v0.4.11
2026-07-22 20:17:23 +02:00

252 lines
22 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/jean) — 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="Jean">
<title>Jean</title>
<!-- Favicon (même marque « ajean » 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='%231f6feb'/><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,iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAABa0lEQVR42u3dsQ2AIBBAUSCOYA8b4iiMSI9D2FpYGTEheW8AisvPUV7MdQR4kowAcSAOxIE4EAfiQByIA3EgDhAH4kAciANxIA7EgTgQB+IAcSAOxIE4EAfiQByIA3EgDhAH4kAciANxIA7EgTgQB+JAHCAOxMEHNiO4622f9HI5TpsD3wriQBwgDsSBOBAH4kAciANxIA7EAeJAHIgDcSAOxIE4EAfiQBwgDsSBOBAH4kAciANxIA7EgThAHIgDcSAOxIE4EAfiQByIA8SBOBAH4kAciANxIA7EgThAHIgDcSAO/rLkLXsX520OxIE4EAfiQByIA8SBOBAH4kAciANxIA7EgThAHIgDcSAOxIE4EAfiQByIA8SBOHgl5jpMAZsDcSAOxIE4EAfiQByIA3GAOBAH4kAciANxIA7EgTgQB4gDcSAOxIE4EAfiQByIA3EgDhAH4kAciANxIA7EgTgQB+IAcSAOxME0F8YvCl24O02KAAAAAElFTkSuQmCC">
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
<script>try{document.documentElement.setAttribute('data-theme',localStorage.getItem('jean-theme')||'dark');document.documentElement.setAttribute('data-display',localStorage.getItem('jean-display')||'full')}catch(e){document.documentElement.setAttribute('data-theme','dark');document.documentElement.setAttribute('data-display','full')}</script>
<style>
@@CSS@@
</style></head><body>
<button id="menubtn" onclick="toggleSide()">☰</button>
<div id="backdrop" onclick="toggleSide()"></div>
<div class="side" id="side">
<div class="sidehead">
<h1 id="brand"><span id="brand-a">a</span>jean<span id="ver"></span></h1>
<span id="status-svc" class="statuspill">…</span>
</div>
<details open><summary>GPU</summary>
<div id="vram" class="muted">…</div>
</details>
<details open id="cfg-details"><summary>Config active</summary>
<div id="cfg" class="muted">…</div>
</details>
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
<div id="presets"></div>
</details>
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
<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, 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>
<!-- Mémoire / Accès internet / Paramètres : sous-réglages du mode agent, en cartes. Grisés/inertes quand l'agent est off (voir setAgentGate). -->
<div id="mem-block" class="gated-block">
<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>
<label class="switchrow" style="justify-content:flex-start;gap:12px"><span>mode</span>
<select id="mem-mode" onchange="setMemMode()" style="flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px">
<option value="always">auto (proactive)</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-caret">▸</span>
<span class="pages-title">Pages <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>
<input id="mem-search" class="mem-search" type="search" placeholder="filtrer la mémoire…" oninput="renderMemList()" style="display:none">
<div id="mem-list" class="mem-list muted" style="font-size:12px"></div>
</div>
</div>
<div id="net-block" class="gated-block">
<div class="subhead">Accès internet<span id="internet-badge" style="margin-left:6px;font-size:10px"></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> via un serveur <b>Crawl4AI</b>. Actifs seulement si le serveur répond.</span></span></div>
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
<div class="row" style="margin-top:6px">
<input id="crawl-url" type="text" placeholder="http://localhost:11235" style="flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit" onchange="saveCrawlUrl()">
<button onclick="saveCrawlUrl()">enregistrer</button>
</div>
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
</div>
<div id="param-block" class="gated-block">
<div class="subhead">Paramètres</div>
<label class="switchrow"><span class="switch"><input type="checkbox" id="toollimit-toggle" onchange="toggleToolLimit()"><span class="slider"></span></span> limiter les appels d'outils par tour<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">activé par défaut : coupe l'IA après ~8 appels d'outils (message <code>[stop: trop d'appels d'outils]</code>). Désactive-le si tu veux la laisser enchaîner sans plafond (l'anti-boucle reste actif).</span></span></label>
<label class="switchrow" style="margin-top:8px"><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>
</details>
<details><summary>Apparence</summary>
<label class="switchrow" style="justify-content:space-between;gap:12px"><span>thème</span>
<select id="theme-select" onchange="applyTheme(this.value);savePrefs()" style="flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px"></select>
</label>
<label class="switchrow" style="justify-content:space-between;gap:12px;margin-top:8px"><span>affichage<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip"><b>Complet</b> : tout est visible (raisonnement, appels d'outils, vitesse, config active). <b>Simple</b> : masque le raisonnement, les appels d'outils, les détails de vitesse sur les bulles et la config active à gauche.</span></span></span>
<select id="display-select" onchange="applyDisplay(this.value);savePrefs()" style="flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:5px 8px;font:inherit;font-size:12px">
<option value="full">complet</option>
<option value="simple">simple</option>
</select>
</label>
</details>
<details><summary>System prompt</summary>
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;min-height:70px;resize:vertical"></textarea>
</details>
<details><summary>Accès OpenAI</summary>
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branche n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor…) sur cette URL. Modèle : <code>jean</code>. Si une clé est définie, le client doit envoyer <code>Authorization: Bearer &lt;clé&gt;</code>.</span></span></div>
<label class="switchrow" style="margin-top:6px"><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 ton IA joignable depuis <b>n'importe où</b> via <code>https://&lt;machine&gt;.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 un abonnement ajean.link actif.</span></span></label>
<div id="oai-public-wrap" style="display:none;margin-top:4px">
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 public (ajean.link) — accessible partout, TLS de bout en bout jusqu'à ce serveur</div>
<div class="row">
<input id="oai-public-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
<button onclick="copyText(document.getElementById('oai-public-url').value,'URL publique copiée')" title="copier">⧉</button>
</div>
</div>
<div id="oai-local-wrap" style="margin-top:4px">
<div class="muted" style="font-size:11px;margin-bottom:2px">🏠 local (réseau)</div>
<div class="row">
<input id="oai-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
<button onclick="copyText(document.getElementById('oai-url').value,'URL copiée')" title="copier">⧉</button>
</div>
</div>
<div class="muted" style="font-size:11px;margin-top:3px">modèle : <code>jean</code></div>
<div class="subhead" style="margin-top:12px">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="row" style="margin-top:4px">
<input id="oai-key" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
<button id="oai-key-eye" onclick="toggleKeyReveal()" title="afficher / masquer">👁</button>
<button onclick="copyApiKey()" title="copier la clé">⧉</button>
</div>
<div class="row" style="margin-top:6px">
<button onclick="apiKeyAction('generate')">générer</button>
<button onclick="apiKeySet()">définir…</button>
<button onclick="apiKeyAction('clear')">retirer</button>
</div>
</details>
<details id="lc-details"><summary>Backend llama.cpp</summary>
<div id="lc-status" class="lc-head muted">…</div>
<div class="lc-modes">
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
<div class="lc-mode-t">⚡ Version rapide <span class="lc-reco">conseillé</span></div>
<div class="lc-mode-d">prête à l'emploi, installée 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">🔧 Version optimisée</div>
<div class="lc-mode-d">compilée pour ta machine, plus longue</div>
<div class="lc-mode-state" id="lc-opt-state"></div>
</div>
</div>
<div id="lc-job" style="display:none;margin-top:10px">
<div id="lc-job-phase" style="font-size:12px;display:flex;align-items:center;gap:6px"></div>
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
<pre id="lc-log" hidden></pre>
</div>
</details>
<details><summary>Service</summary>
<div class="row">
<button onclick="act('start')">start</button>
<button onclick="act('restart')">restart</button>
<button onclick="act('stop')">stop</button>
</div>
</details>
<details id="upd-details"><summary>Actions</summary>
<div class="row"><button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button></div>
<div id="upd-msg" class="muted" style="font-size:12px;margin-top:6px;line-height:1.5"></div>
<div class="row" style="margin-top:6px">
<button onclick="loadAll()">↻ refresh</button>
<button onclick="resetChat()">⌫ clear chat</button>
<button id="btn-bench" onclick="runBenchUI()">⚡ bench</button>
</div>
</details>
</div>
<div class="main" style="position:relative">
<div id="chat"></div>
<button id="scrollbtn" onclick="jumpBottom()" title="aller en bas">↓</button>
<div id="composer">
<div id="ctxtrack"><div id="ctx-fill"></div></div>
<div id="ctxmeta">
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
<span style="display:flex;align-items:center;gap:10px;margin-left:auto">
<span id="ctx-text" class="muted">contexte 0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
</span>
</div>
<div id="inputbar">
<textarea id="input" placeholder="message…" onkeydown="onKey(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" style="padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7">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,.6);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>
<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">
<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="modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:30;align-items:center;justify-content:center;padding:20px">
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:700px;max-height:90vh;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 id="modal-title">Preset</strong>
<button onclick="closeModal()" style="margin:0">×</button>
</div>
<div style="padding:14px 16px;display:flex;flex-direction:column;gap:10px;overflow:auto;flex:1">
<label class="muted">Nom <span style="opacity:.6">(espaces et accents OK, sauf / et \)</span></label>
<input id="m-name" style="background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit">
<div id="m-model-row" style="display:none">
<label class="muted">Moteur (backend) <span style="opacity:.6">— quel llama.cpp fait tourner ce modèle</span></label>
<div class="be-opts">
<label class="be-opt"><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>⚡ Rapide</span><span class="be-note" id="be-fast-note"></span></label>
<label class="be-opt"><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>🔧 Optimisé</span><span class="be-note" id="be-opt-note"></span></label>
<label class="be-opt"><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>📁 Personnalisé</span></label>
</div>
<div id="m-backend-custom" style="display:none;margin:0 0 10px">
<input id="m-backend-path" oninput="onCustomPath()" placeholder="chemin vers llama-server(.exe)" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit">
<select id="m-backend-detected" onchange="onPickDetected()" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;margin-top:6px"></select>
<div class="muted" id="be-drop-hint" style="font-size:11px;margin-top:4px"></div>
</div>
<label class="muted">Modèle <span style="opacity:.6">(remplace la ligne MODEL=)</span></label>
<select id="m-model" onchange="onPickModel()" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;margin-top:6px"></select>
<label class="muted" style="margin-top:10px;display:block">Télécharger depuis Hugging Face <span style="opacity:.6">(lien vers un .gguf)</span></label>
<div style="display:flex;gap:6px;margin-top:6px">
<input id="m-hf-url" placeholder="https://huggingface.co/…/resolve/main/model.gguf" style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit">
<button id="m-hf-btn" onclick="startDownload()" style="flex-shrink:0">⬇ télécharger</button>
</div>
<div id="m-hf-progress" class="muted" style="display:none;font-size:11px;margin-top:6px"></div>
<label class="muted" style="margin-top:10px;display:block">Quantization (tag) <span style="opacity:.6">(vide = détection auto depuis le nom du modèle)</span></label>
<input id="m-quant" oninput="applyQuant()" placeholder="ex: Q4_K_M, IQ3_XXS, BF16…" style="width:100%;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;margin-top:6px">
</div>
<label class="muted">Contenu</label>
<textarea id="m-content" spellcheck="false" style="background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:8px;font:inherit;min-height:280px;resize:vertical"></textarea>
</div>
<div style="padding:14px 16px;border-top:1px solid var(--border);display:flex;justify-content:space-between;gap:6px;flex-wrap:wrap">
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
<button id="m-del" onclick="delItem()" style="background:var(--danger-bg);border-color:var(--err);color:var(--err);margin:0">supprimer</button>
<label id="m-del-model-wrap" style="display:none;align-items:center;gap:5px;font-size:11px;color:var(--dim)"><input type="checkbox" id="m-del-model"> + le modèle .gguf</label>
</div>
<div style="display:flex;gap:6px">
<button onclick="closeModal()">annuler</button>
<button onclick="saveItem()" style="border-color:var(--accent);color:var(--accent)">enregistrer</button>
</div>
</div>
</div>
</div>
<script src="marked.min.js"></script>
<script>
@@JS@@
</script></body></html>