mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 02:38:20 +02:00
Le choix du matériel et du modèle n'existait nulle part : la dictée prenait le seul modèle codé en dur et le seul chemin possible. Sur une machine à plusieurs GPU, rien ne permettait de dire lequel prêter à whisper. Le panneau liste les GPU avec leur VRAM libre (/api/vram donne déjà tout), et chaque modèle avec sa taille de téléchargement et sa mémoire. Un modèle absent le dit dans sa propre étiquette : le choisir n'est pas un réglage instantané mais un transfert de plusieurs centaines de mégaoctets, et le taire ferait passer l'attente pour une panne. Le test du micro capte trois secondes, affiche le niveau mesuré puis la transcription, sans toucher au champ de saisie. Vu de l'extérieur, un micro muet, un niveau trop faible et un moteur en panne se ressemblent tous — il fallait bricoler ce diagnostic à la main pour les distinguer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3ewMAsXhkw9RY11kb9Dc9
1204 lines
88 KiB
HTML
1204 lines
88 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='%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>
|
||
<button data-pane="dictee" onclick="settingsShow('dictee')">Dictée</button>
|
||
<div class="set-nav-h">Application</div>
|
||
<button data-pane="identity" onclick="settingsShow('identity')">Identité</button>
|
||
<button data-pane="appearance" onclick="settingsShow('appearance')">Apparence</button>
|
||
<button data-pane="openai" onclick="settingsShow('openai')">Accès OpenAI</button>
|
||
<button data-pane="actions" onclick="settingsShow('actions')">Actions</button>
|
||
</nav>
|
||
<div class="set-body" id="set-body">
|
||
<section class="set-pane" data-pane="engine-log" id="svc-log-box">
|
||
<div class="set-pane-h"><span>Journal du moteur</span></div>
|
||
<div class="row" style="justify-content:flex-end;align-items:center">
|
||
<button onclick="loadSvcLog()" title="rafraîchir" aria-label="rafraîchir le journal"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg></button>
|
||
</div>
|
||
<pre id="svc-log" class="muted" style="font-size:10px;line-height:1.45;max-height:220px;overflow:auto;white-space:pre-wrap;word-break:break-word;margin:6px 0 0"></pre>
|
||
<!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
|
||
va quand on cherche à comprendre l'état de son installation. -->
|
||
<div class="row" style="justify-content:space-between;align-items:center;margin-top:12px">
|
||
<span class="io-cap">Emplacements</span>
|
||
</div>
|
||
<div id="paths-msg" class="muted" style="font-size:10px;line-height:1.5"></div>
|
||
</section>
|
||
<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 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>
|
||
<!-- Dictée vocale. Réglages de SERVEUR : le modèle chargé et le GPU occupé
|
||
sont des propriétés de la machine, identiques pour tous les onglets.
|
||
Voir js/24-dictate-settings.js et internal/loki/dictate_*.go. -->
|
||
<section class="set-pane" data-pane="dictee">
|
||
<div class="set-pane-h"><span>Dictée</span></div>
|
||
<div class="subhead">Matériel<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">whisper tourne dans un processus séparé, allumé au premier clic sur le micro et éteint après <b>dix minutes</b> sans dictée — il ne garde pas la VRAM occupée pour rien. Le GPU choisi est transmis par <code>CUDA_VISIBLE_DEVICES</code>.</span></span></div>
|
||
<div class="slist">
|
||
<label class="switchrow"><span>processeur ou GPU</span>
|
||
<select id="dic-device" class="sctl"></select>
|
||
</label>
|
||
</div>
|
||
<div class="subhead">Modèle<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Les modèles ne sont <b>pas</b> dans l'image : ils sont téléchargés à la demande dans <code>/data/whisper/</code> et survivent aux recréations du conteneur. Plus le modèle est gros, mieux il comprend — et plus il occupe de mémoire.</span></span></div>
|
||
<div class="slist">
|
||
<label class="switchrow"><span>modèle</span>
|
||
<select id="dic-model" class="sctl"></select>
|
||
</label>
|
||
<div class="rowbody">
|
||
<button id="dic-dl" class="btn" onclick="dictateDownload()">Télécharger ce modèle</button>
|
||
<span id="dic-dl-state" class="dim"></span>
|
||
</div>
|
||
</div>
|
||
<div class="subhead">Reconnaissance</div>
|
||
<div class="slist">
|
||
<label class="switchrow"><span>langue</span>
|
||
<select id="dic-lang" class="sctl">
|
||
<option value="fr">français</option>
|
||
<option value="en">anglais</option>
|
||
<option value="de">allemand</option>
|
||
<option value="es">espagnol</option>
|
||
<option value="it">italien</option>
|
||
<option value="auto">détection automatique</option>
|
||
</select>
|
||
</label>
|
||
<label class="switchrow"><span>réactivité<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Longueur des tranches transcrites au fil de la dictée. Court = le texte arrive plus tôt, avec moins de contexte donc plus d'erreurs. Long = mieux reconnu, mais plus d'attente.</span></span></span>
|
||
<select id="dic-react" class="sctl">
|
||
<option value="court">court</option>
|
||
<option value="moyen">moyen</option>
|
||
<option value="long">long</option>
|
||
</select>
|
||
</label>
|
||
<div class="rowbody">
|
||
<button class="btn primary" onclick="dictateSave()">Enregistrer</button>
|
||
<span id="dic-save-state" class="dim"></span>
|
||
</div>
|
||
</div>
|
||
<div class="subhead">Vérifier</div>
|
||
<div class="slist">
|
||
<div class="rowbody">
|
||
<button id="dic-test" class="btn" onclick="dictateTestMic()">Tester le micro (3 s)</button>
|
||
</div>
|
||
<div id="dic-test-out" class="dim"></div>
|
||
<div id="dic-state" class="dim"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||
l'application elle-même. -->
|
||
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||
<section class="set-pane" data-pane="identity" id="ident-details">
|
||
<div class="set-pane-h"><span>Identité</span></div>
|
||
<div class="slist"><div class="rowbody">
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
|
||
<!-- autocomplete/data-*-ignore : sans ça, les gestionnaires de mots de
|
||
passe prennent ce champ pour un identifiant de connexion et
|
||
recouvrent la saisie de comptes enregistrés. -->
|
||
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi"
|
||
autocomplete="off" name="loki-nickname" data-1p-ignore data-lpignore="true" data-form-type="other"
|
||
oninput="saveIdentity()"></span>
|
||
</div>
|
||
<div class="pe-row stack">
|
||
<span class="pe-row-l">Ton avatar</span>
|
||
<div id="p-avatar-user" class="avpick"></div>
|
||
</div>
|
||
<div class="pe-row stack">
|
||
<span class="pe-row-l">Avatar de Loki</span>
|
||
<div id="p-avatar-ai" class="avpick"></div>
|
||
</div>
|
||
</div></div>
|
||
</section>
|
||
<section class="set-pane" data-pane="appearance">
|
||
<div class="set-pane-h"><span>Apparence</span></div>
|
||
<div class="slist">
|
||
<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 <clé></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 & 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 @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. 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 ou auto = ce qui tient en VRAM · all = tout, quitte à saturer</span></span>
|
||
<span class="pe-row-c"><input id="s-ngl" class="pe-val" type="text" inputmode="numeric" placeholder="999" oninput="cfgWriteKey('NGL', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Experts MoE sur CPU<span class="pe-sub">vide = non</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>
|