mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
- Le bouton « vérifier les mises à jour » revient dans la carte Moteur : il ne fait QUE regarder, là où « mettre à jour » télécharge et bascule après confirmation. Deux gestes qui n'engagent pas pareil, deux boutons. - Les boutons « catalogue » / « + ajouter un serveur » (MCP) étaient des pilules arrondies au milieu de boutons rectangulaires : même gabarit que leurs voisins (4 px, 12 px, mêmes marges). - « Réglages » devient « Paramètres » dans la barre latérale et en tête de la modale. Version 0.12.3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1148 lines
85 KiB
HTML
1148 lines
85 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>
|
||
<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>
|
||
<!-- ── 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 = tout sur le GPU · auto = ce qui tient en VRAM</span></span>
|
||
<span class="pe-row-c"><input id="s-ngl" class="pe-val" type="text" inputmode="numeric" placeholder="999" oninput="cfgWriteKey('NGL', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Experts MoE sur CPU<span class="pe-sub">vide = non</span></span>
|
||
<span class="pe-row-c"><input id="s-moe" class="pe-val" type="number" min="0" step="1" placeholder="—" oninput="eaSetValued('--n-cpu-moe', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Threads CPU<span class="pe-sub">0 = auto</span></span>
|
||
<span class="pe-row-c"><input id="s-threads" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Threads (batch)<span class="pe-sub">0 = auto</span></span>
|
||
<span class="pe-row-c"><input id="s-tbatch" class="pe-val" type="number" min="0" step="1" placeholder="0" oninput="cfgWriteKey('THREADS_BATCH', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">BATCH</span>
|
||
<span class="pe-row-c"><input id="s-batch" class="pe-val" type="number" min="0" step="256" placeholder="2048" oninput="cfgWriteKey('BATCH', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">UBATCH</span>
|
||
<span class="pe-row-c"><input id="s-ubatch" class="pe-val" type="number" min="0" step="128" placeholder="512" oninput="cfgWriteKey('UBATCH', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Cache KV<span class="pe-sub">compression du contexte</span></span>
|
||
<span class="pe-row-c"><span class="pe-selc"><select id="s-kv" onchange="cfgWriteKey('KV_TYPE', this.value)">
|
||
<option value="">f16 (max qualité)</option>
|
||
<option value="q8_0">q8_0 (léger)</option>
|
||
<option value="q4_0">q4_0 (très léger)</option>
|
||
<option value="q5_1">q5_1</option>
|
||
<option value="bf16">bf16</option>
|
||
</select></span></span>
|
||
</div>
|
||
<!-- Décodage spéculatif : le type vient de la liste que llama-server
|
||
accepte pour --spec-type. Le nombre de jetons anticipés
|
||
(--spec-draft-n-max) n'a de sens qu'une fois un type choisi, donc
|
||
sa ligne n'apparaît qu'à ce moment. -->
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Décodage spéculatif<span class="pe-sub">anticipe des jetons pour accélérer</span></span>
|
||
<span class="pe-row-c"><span class="pe-selc"><select id="s-spec" onchange="onSpecType()">
|
||
<option value="">non</option>
|
||
<optgroup label="Modèle brouillon">
|
||
<option value="draft-mtp">MTP (intégré au modèle)</option>
|
||
<option value="draft-eagle3">EAGLE-3</option>
|
||
<option value="draft-simple">modèle brouillon séparé</option>
|
||
<option value="draft-dflash">dFlash</option>
|
||
<option value="draft-dspark">dSpark</option>
|
||
</optgroup>
|
||
<optgroup label="N-grammes (sans modèle brouillon)">
|
||
<option value="ngram-simple">n-grammes</option>
|
||
<option value="ngram-mod">n-grammes (mod)</option>
|
||
<option value="ngram-cache">n-grammes (cache)</option>
|
||
<option value="ngram-map-k">n-grammes (map-k)</option>
|
||
<option value="ngram-map-k4v">n-grammes (map-k4v)</option>
|
||
</optgroup>
|
||
</select></span></span>
|
||
</div>
|
||
<div class="pe-row" id="s-spec-n-row" style="display:none">
|
||
<span class="pe-row-l">Jetons anticipés<span class="pe-sub">par étape — défaut 3</span></span>
|
||
<span class="pe-row-c"><input id="s-spec-n" class="pe-val" type="number" min="1" step="1" placeholder="3" oninput="eaSetValued('--spec-draft-n-max', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Raisonnement<span class="pe-sub" id="s-reasoning-sub">réflexion étape par étape</span></span>
|
||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-reasoning" onchange="cfgWriteKey('REASONING', this.checked?'on':'off')"><span class="pe-sl"></span></label></span>
|
||
</div>
|
||
<div class="pe-row" id="s-reasoning-effort-row">
|
||
<span class="pe-row-l">Intensité du raisonnement<span class="pe-sub" id="s-reasoning-effort-sub">défaut de ce preset — ajustable à la volée depuis la barre de saisie</span></span>
|
||
<span class="pe-row-c">
|
||
<select id="s-reasoning-effort" class="pe-val" onchange="cfgWriteKey('REASONING_EFFORT', this.value)">
|
||
<option value="">auto</option>
|
||
<option value="none">aucune</option>
|
||
<option value="low">basse</option>
|
||
<option value="medium">moyenne</option>
|
||
<option value="high">haute</option>
|
||
<option value="xhigh">maximale</option>
|
||
</select>
|
||
</span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Flash attention<span class="pe-sub">plus rapide, moins de VRAM</span></span>
|
||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-flash" onchange="eaSetValued('--flash-attn', this.checked?'on':'')"><span class="pe-sl"></span></label></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Garder en RAM<span class="pe-sub">évite le swap (mlock)</span></span>
|
||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-mlock" onchange="eaToggleFlag('--mlock', this.checked)"><span class="pe-sl"></span></label></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Charger tout en mémoire<span class="pe-sub">désactive le mmap</span></span>
|
||
<span class="pe-row-c"><label class="pe-switch"><input type="checkbox" id="s-nommap" onchange="eaToggleFlag('--no-mmap', this.checked)"><span class="pe-sl"></span></label></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Échantillonnage (repris de l'amont AJEAN v0.9.5/v0.9.6) : ces valeurs
|
||
partent dans CHAQUE requête de complétion, pas sur la ligne de commande
|
||
du moteur — c'est le seul endroit où elles prennent effet. Un champ vide
|
||
n'envoie rien, llama-server garde son défaut. Les valeurs recommandées
|
||
dépendent du modèle (Qwen3.8 en réflexion : temp 1.0, top_p 0.95,
|
||
top_k 20, min_p 0). -->
|
||
<div id="m-sampling-row" class="pe-group" style="display:none">
|
||
<div class="pe-gh">Échantillonnage</div>
|
||
<div class="pe-list">
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Température<span class="pe-sub">vide = défaut du modèle</span></span>
|
||
<span class="pe-row-c"><input id="s-temp" class="pe-val" type="number" min="0" step="0.05" placeholder="—" oninput="cfgWriteKey('TEMP', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">top_p<span class="pe-sub">noyau de probabilité</span></span>
|
||
<span class="pe-row-c"><input id="s-topp" class="pe-val" type="number" min="0" max="1" step="0.01" placeholder="—" oninput="cfgWriteKey('TOP_P', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">top_k<span class="pe-sub">0 = désactivé</span></span>
|
||
<span class="pe-row-c"><input id="s-topk" class="pe-val" type="number" min="0" step="1" placeholder="—" oninput="cfgWriteKey('TOP_K', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">min_p<span class="pe-sub">seuil de probabilité</span></span>
|
||
<span class="pe-row-c"><input id="s-minp" class="pe-val" type="number" min="0" max="1" step="0.01" placeholder="—" oninput="cfgWriteKey('MIN_P', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Pénalité de présence<span class="pe-sub">pousse vers de nouveaux mots (0 = off)</span></span>
|
||
<span class="pe-row-c"><input id="s-presp" class="pe-val" type="number" min="0" max="2" step="0.1" placeholder="—" oninput="cfgWriteKey('PRESENCE_PENALTY', this.value)"></span>
|
||
</div>
|
||
<div class="pe-row">
|
||
<span class="pe-row-l">Pénalité de répétition<span class="pe-sub">anti-répétition (1 = neutre)</span></span>
|
||
<span class="pe-row-c"><input id="s-reppen" class="pe-val" type="number" min="0" step="0.05" placeholder="—" oninput="cfgWriteKey('REPEAT_PENALTY', this.value)"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="m-raw" class="pe-group">
|
||
<div class="pe-gh" id="m-raw-head">Configuration</div>
|
||
<div class="pe-list">
|
||
<div id="m-raw-toggle" class="pe-row" style="cursor:pointer" onclick="toggleRaw()">
|
||
<span class="pe-row-l">Éditer le fichier de configuration<span class="pe-sub">texte brut</span></span>
|
||
<span class="pe-row-c"><span id="m-raw-caret" class="pe-caret"></span></span>
|
||
</div>
|
||
<div id="m-raw-body" class="pe-row stack" style="display:none">
|
||
<textarea id="m-content" class="pe-inp" spellcheck="false"></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Pied compact, sur UNE ligne : la suppression est une icône (action rare et
|
||
destructrice, elle n'a pas à peser autant que « enregistrer »), et sa case
|
||
« supprimer aussi le .gguf » se réduit à l'extension, le libellé complet
|
||
restant en infobulle. -->
|
||
<div class="pe-foot">
|
||
<div class="foot-l">
|
||
<button id="m-del" class="pe-del" onclick="delItem()" title="supprimer" aria-label="supprimer"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a1 1 0 011-1h6a1 1 0 011 1v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg></button>
|
||
</div>
|
||
<div class="foot-r">
|
||
<button class="pe-btn" onclick="closeModal()">annuler</button>
|
||
<button class="pe-btn primary" onclick="saveItem()">enregistrer</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script src="marked.min.js"></script>
|
||
<script>
|
||
@@JS@@
|
||
</script></body></html>
|