mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Réglages en modale à deux volets ; discussions triées par activité
La barre latérale ne garde que les discussions et le moniteur machine : tout le bloc « Réglages » (11 sections empilées en details) devient une modale avec sa nav de sections à gauche et LE panneau choisi à droite — chaque section a enfin toute la largeur, et la barre latérale respire. Les panneaux réutilisent tels quels les gabarits existants (.slist, .switchrow, .subhead) ; les ids historiques (svc-log-box, lc-details, tasks-details…) sont conservés sur les panneaux, et openDetails() les résout vers openSettings(panneau) : pastille d'état du moteur, pastille d'installation et jobs llama.cpp rouvrent la modale au bon endroit sans changer leurs appelants. Le chargement paresseux du journal du moteur (ancien ontoggle) devient un hook d'affichage du panneau. Discussions : la plus récente en tête. Le serveur triait déjà par date d'activité, mais son tri stable laissait une discussion toute neuve SOUS celle qu'on venait de quitter (touchées dans la même seconde) : l'UI départage par date de création puis par ordre d'index. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f4090059f1
commit
a333603ca6
6 files changed
+862
-592
No files matched your search
+431
-296
@@ -2052,6 +2052,44 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
#criteria-list li.crit-pending{color:var(--dim)}
|
||||
|
||||
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
|
||||
|
||||
/* ── Réglages en modale ─────────────────────────────────────────────────────
|
||||
Deux volets : nav des sections à gauche, LE panneau choisi à droite. Les
|
||||
panneaux réutilisent tels quels les gabarits de la barre latérale (.slist,
|
||||
.switchrow, .subhead…), seuls la coque et le routage sont neufs. */
|
||||
#side-settings-btn{display:flex;align-items:center;gap:8px;width:100%;margin:8px 0 0;padding:10px 12px;
|
||||
background:none;border:0;border-top:1px solid var(--border);border-radius:0;box-shadow:none;
|
||||
font-size:13px;color:var(--text);cursor:pointer;text-align:left}
|
||||
#side-settings-btn:hover{background:var(--bubble)}
|
||||
#side-settings-btn svg{flex:none;opacity:.7}
|
||||
.set-card{max-width:860px;height:min(660px,92vh)}
|
||||
.set-shell{display:flex;flex:1;min-height:0}
|
||||
.set-nav{flex:0 0 200px;border-right:1px solid var(--border);overflow-y:auto;
|
||||
padding:10px 8px;display:flex;flex-direction:column;gap:2px}
|
||||
.set-nav-h{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin:12px 8px 4px}
|
||||
.set-nav-h:first-child{margin-top:2px}
|
||||
.set-nav button{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:7px 10px;
|
||||
background:none;border:0;border-radius:3px;box-shadow:none;
|
||||
font-size:12.5px;color:var(--text);cursor:pointer;text-align:left}
|
||||
.set-nav button:hover{background:var(--bubble)}
|
||||
.set-nav button.active{background:var(--bubble);font-weight:600}
|
||||
.set-nav button .sbadge{margin-left:auto}
|
||||
.set-body{flex:1;min-width:0;overflow-y:auto;padding:16px}
|
||||
.set-pane{display:none}
|
||||
.set-pane.active{display:block}
|
||||
.set-pane-h{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin:0 0 12px}
|
||||
.set-pane-h>span:first-child{flex:1;min-width:0}
|
||||
/* Téléphone : la nav passe en rangée défilante au-dessus du panneau, et la
|
||||
modale prend tout l'écran — un panneau de réglages ne se lit pas en timbre. */
|
||||
@media(max-width:720px){
|
||||
.set-ov{padding:0}
|
||||
.set-card{max-width:none;height:100vh;height:100dvh;max-height:none;border-radius:0;border:0}
|
||||
.set-shell{flex-direction:column}
|
||||
.set-nav{flex:none;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;
|
||||
border-right:0;border-bottom:1px solid var(--border);padding:8px;white-space:nowrap}
|
||||
.set-nav-h{display:none}
|
||||
.set-nav button{width:auto;flex:none}
|
||||
}
|
||||
/* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris,
|
||||
pas de capitales forcées) et borné : un nom de .gguf peut être très long,
|
||||
il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */
|
||||
@@ -2155,302 +2193,14 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
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. -->
|
||||
<details id="side-settings"><summary><span class="stitle">Réglages</span></summary>
|
||||
<div class="sidegroup">
|
||||
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
||||
<div class="row" style="justify-content:flex-end;align-items:center">
|
||||
<button onclick="loadSvcLog()" title="rafraîchir" 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>
|
||||
</details>
|
||||
<details open id="cfg-details"><summary><span class="stitle">Config active</span></summary>
|
||||
<div id="cfg" class="slist">…</div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter" 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></summary>
|
||||
<div class="slist"><div id="presets" class="rowlist"></div></div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
|
||||
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, écrire/lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label></div>
|
||||
<!-- Sous-réglages du mode agent. MÊME gabarit que « Machine » / « Apparence » :
|
||||
étiquette de groupe nue, puis UNE carte de lignes (.slist). Le groupe
|
||||
entier est grisé/inerte quand l'agent est off (voir setAgentGate). -->
|
||||
<div id="mem-block" class="gated-group">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span>mode</span>
|
||||
<select id="mem-mode" onchange="setMemMode()" class="sctl">
|
||||
<option value="always">auto</option>
|
||||
<option value="ondemand">sur demande</option>
|
||||
<option value="off">désactivée</option>
|
||||
</select>
|
||||
</label>
|
||||
<div id="mem-pages-bar" class="pages-bar" onclick="toggleMemPages()">
|
||||
<span class="pages-title">Voir la mémoire <span id="mem-count" class="mem-count"></span></span>
|
||||
<button class="addbtn" onclick="event.stopPropagation();openMem('')" title="ajouter une page mémoire">+ ajouter</button>
|
||||
</div>
|
||||
<div id="mem-pages-body" hidden>
|
||||
<!-- Le champ de filtre garde son rembourrage de ligne ; la liste, elle,
|
||||
est à fleur de carte : ses pages sont des LIGNES séparées par un
|
||||
filet, pas une pile de petites cartes. -->
|
||||
<div id="mem-search-row" class="rowbody" style="display:none">
|
||||
<input id="mem-search" class="mem-search" type="search" placeholder="filtrer la mémoire…" oninput="renderMemList()" style="margin:0">
|
||||
</div>
|
||||
<div id="mem-list" class="mem-list rowlist muted" style="font-size:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="net-block" class="gated-group">
|
||||
<div class="subhead">Accès internet<span id="internet-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Ajoute les outils <b>web_search / web_open / web_read / web_grep</b>. Le moteur <b>intégré</b> ne demande aucune installation. Le moteur <b>Crawl4AI</b> exige un serveur que tu héberges toi-même (ex. <code>docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest</code>) mais exécute le JavaScript des pages. Actifs seulement si le <b>mode agent</b> est actif.</span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
||||
<!-- Moteur web : « go » = intégré au binaire (rien à installer, pas de rendu
|
||||
JavaScript) · « crawl4ai » = serveur externe avec Chromium headless.
|
||||
Réglages propres à Crawl4AI masqués avec le moteur intégré ; l'URL et
|
||||
la clé s'enregistrent en quittant le champ (onchange). -->
|
||||
<div class="rowbody">
|
||||
<div class="row" style="margin:0">
|
||||
<select id="web-engine" class="sctl" onchange="saveWebEngine()">
|
||||
<option value="go">Moteur Intégré</option>
|
||||
<option value="crawl4ai">Crawl4AI</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Masqués AU DÉPART : le moteur par défaut est l'intégré, et ces champs
|
||||
ne concernent que Crawl4AI. Affichés d'emblée puis repliés par le
|
||||
chargement de l'état, ils faisaient sauter la section sous les yeux. -->
|
||||
<div id="crawl-fields" style="display:none">
|
||||
<div class="row">
|
||||
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
||||
</div>
|
||||
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
||||
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
||||
qu'un indice sur les 4 derniers caractères. -->
|
||||
<div class="row">
|
||||
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
||||
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
||||
</div>
|
||||
<div class="muted" style="font-size:11px;margin-top:4px">enregistré automatiquement en quittant le champ</div>
|
||||
<div id="crawl-key-state" class="muted" style="font-size:11px;margin-top:3px"></div>
|
||||
</div>
|
||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
<div id="mcp-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun serveur — ajoute un serveur fichiers, une base de données, une API…)</div></div>
|
||||
<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>
|
||||
</details>
|
||||
<details id="tasks-details"><summary><span class="stitle">Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></summary>
|
||||
<!-- 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>
|
||||
</details>
|
||||
<details><summary><span class="stitle">System prompt</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
||||
</div></div>
|
||||
</details>
|
||||
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
||||
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
||||
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
||||
étaient des cartes dans une carte). -->
|
||||
<div class="slist">
|
||||
<!-- Moteur fourni par l'image Docker : aucun des trois modes 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">
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<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>
|
||||
</details>
|
||||
</div><!-- /sidegroup -->
|
||||
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||
l'application elle-même. -->
|
||||
<div class="side-sep">Paramètres</div>
|
||||
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||
<div class="sidegroup">
|
||||
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
|
||||
<!-- autocomplete/data-*-ignore : sans ça, les gestionnaires de mots de
|
||||
passe prennent ce champ pour un identifiant de connexion et
|
||||
recouvrent la saisie de comptes enregistrés. -->
|
||||
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi"
|
||||
autocomplete="off" name="loki-nickname" data-1p-ignore data-lpignore="true" data-form-type="other"
|
||||
oninput="saveIdentity()"></span>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<span class="pe-row-l">Ton avatar</span>
|
||||
<div id="p-avatar-user" class="avpick"></div>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<span class="pe-row-l">Avatar de Loki</span>
|
||||
<div id="p-avatar-ai" class="avpick"></div>
|
||||
</div>
|
||||
</div></div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Apparence</span></summary>
|
||||
<div class="slist">
|
||||
<label class="switchrow rev"><span>mode sombre</span>
|
||||
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="subhead">Affichage</div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-reasoning" onchange="toggleView('hide-reasoning')"><span class="slider"></span></span> masquer le raisonnement</label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-tools" onchange="toggleView('hide-tools')"><span class="slider"></span></span> masquer les appels d'outils</label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-fold-tools" onchange="toggleView('fold-tools')"><span class="slider"></span></span> garder les bulles repliées<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Les bulles de raisonnement et d'outil arrivent <b>déjà fermées</b> au lieu de s'ouvrir puis se refermer. Un clic sur leur étiquette les déplie.</span></span></label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
||||
</div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor, 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>
|
||||
</details>
|
||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||
dans loki.db (bbolt, verrouillé tant que le service tourne) : sans le
|
||||
bouton exporter, plus personne ne peut relire/archiver ses échanges. -->
|
||||
<div class="row" style="margin:0">
|
||||
<button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button>
|
||||
<button onclick="openExportModal()" title="choisir le format et ce qu'on emporte">⤓ exporter…</button>
|
||||
</div>
|
||||
<div id="upd-msg" class="muted"></div>
|
||||
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
|
||||
<div class="row" style="margin-bottom:0">
|
||||
<button id="btn-bench" onclick="runBenchUI()">bench</button>
|
||||
<button onclick="resetChat()" 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>
|
||||
</details>
|
||||
</div><!-- /sidegroup -->
|
||||
</details><!-- /#side-settings -->
|
||||
<!-- Réglages : tout vit désormais dans une modale (nav de sections à gauche,
|
||||
panneau à droite) — la barre latérale ne garde que les discussions et le
|
||||
moniteur machine. Voir js/22-settings-modal.js ; les ids historiques des
|
||||
sections (svc-log-box, lc-details…) sont conservés sur les panneaux. -->
|
||||
<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>Réglages</span>
|
||||
</button>
|
||||
</div><!-- /#side-scroll -->
|
||||
<!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en
|
||||
jauges fines — visible en permanence sans jamais disputer la place au
|
||||
@@ -2591,6 +2341,340 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
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>Réglages</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">
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<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">
|
||||
@@ -7978,6 +8062,14 @@ async function loadConversations(){
|
||||
let r;
|
||||
try{ r = await jget('/api/conversations'); }catch(_){ return; }
|
||||
CONVS = r.conversations || [];
|
||||
// La plus récente EN TÊTE. Le serveur trie déjà par date d'activité, mais son
|
||||
// tri STABLE laissait une discussion toute neuve SOUS celle qu'on vient de
|
||||
// quitter (les deux sont touchées dans la même seconde) : on départage par la
|
||||
// date de création, puis par l'ordre du fichier d'index (création croissante).
|
||||
const ord = new Map(CONVS.map((c, i) => [c.id, i]));
|
||||
CONVS.sort((a, b) => (b.updated || 0) - (a.updated || 0)
|
||||
|| (b.created || 0) - (a.created || 0)
|
||||
|| ord.get(b.id) - ord.get(a.id));
|
||||
CONV_ACTIVE = r.active || '';
|
||||
CONV_BUSY = !!r.busy;
|
||||
renderConversations();
|
||||
@@ -8287,6 +8379,12 @@ function openDetails(el){
|
||||
if(typeof el === 'string') el = document.getElementById(el);
|
||||
while(el){
|
||||
if(el.tagName === 'DETAILS') el.open = true;
|
||||
// La cible vit dans un panneau de la modale de réglages : ouvrir la modale
|
||||
// sur ce panneau. C'est ce qui garde tous les appelants historiques
|
||||
// (pastille d'état, jobs moteur) valides depuis la refonte en modale.
|
||||
if(el.classList && el.classList.contains('set-pane') && typeof openSettings === 'function'){
|
||||
openSettings(el.dataset.pane);
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
@@ -8921,4 +9019,41 @@ async function runTaskNow(){
|
||||
st.style.color = 'var(--err)';
|
||||
}
|
||||
}
|
||||
// ─── Réglages en modale ──────────────────────────────────────────────────────
|
||||
// Toutes les sections de réglages vivent dans une modale à deux volets : la nav
|
||||
// des sections à gauche, LE panneau choisi à droite. La barre latérale ne garde
|
||||
// que les discussions et le moniteur machine — visuellement plus simple, et
|
||||
// chaque section dispose enfin de toute la largeur.
|
||||
//
|
||||
// Les ids historiques des sections (svc-log-box, lc-details, tasks-details…)
|
||||
// sont posés sur les .set-pane : openDetails() (18-shell.js) les résout vers le
|
||||
// bon panneau, si bien que tous les chemins existants — pastille d'état du
|
||||
// moteur, pastille d'installation, jobs llama.cpp — rouvrent la modale au bon
|
||||
// endroit sans changer une ligne de leurs appelants.
|
||||
let SET_LAST = 'config';
|
||||
// Panneaux à contenu paresseux : chargés à l'affichage — l'équivalent de
|
||||
// l'ancien ontoggle des <details>.
|
||||
const SET_HOOKS = {
|
||||
'engine-log': () => { loadSvcLog(); showPaths(); },
|
||||
};
|
||||
function openSettings(pane){
|
||||
showModal('settings-modal');
|
||||
settingsShow(pane || SET_LAST);
|
||||
}
|
||||
function closeSettings(){ hideModal('settings-modal'); }
|
||||
function settingsShow(pane){
|
||||
SET_LAST = pane;
|
||||
document.querySelectorAll('#set-nav [data-pane]').forEach(b => b.classList.toggle('active', b.dataset.pane === pane));
|
||||
document.querySelectorAll('#set-body .set-pane').forEach(s => s.classList.toggle('active', s.dataset.pane === pane));
|
||||
const body = document.getElementById('set-body');
|
||||
if(body) body.scrollTop = 0;
|
||||
const h = SET_HOOKS[pane]; if(h) h();
|
||||
}
|
||||
// Échap ferme la modale — capture:false, donc les modales à confirmation
|
||||
// (ask-modal, qui écoute en capture) gardent la main quand elles sont ouvertes.
|
||||
document.addEventListener('keydown', e => {
|
||||
if(e.key !== 'Escape') return;
|
||||
const m = document.getElementById('settings-modal');
|
||||
if(m && m.classList.contains('show')) closeSettings();
|
||||
});
|
||||
</script></body></html>
|
||||
@@ -75,302 +75,14 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
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. -->
|
||||
<details id="side-settings"><summary><span class="stitle">Réglages</span></summary>
|
||||
<div class="sidegroup">
|
||||
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary>
|
||||
<div class="row" style="justify-content:flex-end;align-items:center">
|
||||
<button onclick="loadSvcLog()" title="rafraîchir" 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>
|
||||
</details>
|
||||
<details open id="cfg-details"><summary><span class="stitle">Config active</span></summary>
|
||||
<div id="cfg" class="slist">…</div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter" 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></summary>
|
||||
<div class="slist"><div id="presets" class="rowlist"></div></div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
|
||||
<div class="slist"><label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, écrire/lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label></div>
|
||||
<!-- Sous-réglages du mode agent. MÊME gabarit que « Machine » / « Apparence » :
|
||||
étiquette de groupe nue, puis UNE carte de lignes (.slist). Le groupe
|
||||
entier est grisé/inerte quand l'agent est off (voir setAgentGate). -->
|
||||
<div id="mem-block" class="gated-group">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span>mode</span>
|
||||
<select id="mem-mode" onchange="setMemMode()" class="sctl">
|
||||
<option value="always">auto</option>
|
||||
<option value="ondemand">sur demande</option>
|
||||
<option value="off">désactivée</option>
|
||||
</select>
|
||||
</label>
|
||||
<div id="mem-pages-bar" class="pages-bar" onclick="toggleMemPages()">
|
||||
<span class="pages-title">Voir la mémoire <span id="mem-count" class="mem-count"></span></span>
|
||||
<button class="addbtn" onclick="event.stopPropagation();openMem('')" title="ajouter une page mémoire">+ ajouter</button>
|
||||
</div>
|
||||
<div id="mem-pages-body" hidden>
|
||||
<!-- Le champ de filtre garde son rembourrage de ligne ; la liste, elle,
|
||||
est à fleur de carte : ses pages sont des LIGNES séparées par un
|
||||
filet, pas une pile de petites cartes. -->
|
||||
<div id="mem-search-row" class="rowbody" style="display:none">
|
||||
<input id="mem-search" class="mem-search" type="search" placeholder="filtrer la mémoire…" oninput="renderMemList()" style="margin:0">
|
||||
</div>
|
||||
<div id="mem-list" class="mem-list rowlist muted" style="font-size:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="net-block" class="gated-group">
|
||||
<div class="subhead">Accès internet<span id="internet-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Ajoute les outils <b>web_search / web_open / web_read / web_grep</b>. Le moteur <b>intégré</b> ne demande aucune installation. Le moteur <b>Crawl4AI</b> exige un serveur que tu héberges toi-même (ex. <code>docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest</code>) mais exécute le JavaScript des pages. Actifs seulement si le <b>mode agent</b> est actif.</span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
||||
<!-- Moteur web : « go » = intégré au binaire (rien à installer, pas de rendu
|
||||
JavaScript) · « crawl4ai » = serveur externe avec Chromium headless.
|
||||
Réglages propres à Crawl4AI masqués avec le moteur intégré ; l'URL et
|
||||
la clé s'enregistrent en quittant le champ (onchange). -->
|
||||
<div class="rowbody">
|
||||
<div class="row" style="margin:0">
|
||||
<select id="web-engine" class="sctl" onchange="saveWebEngine()">
|
||||
<option value="go">Moteur Intégré</option>
|
||||
<option value="crawl4ai">Crawl4AI</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Masqués AU DÉPART : le moteur par défaut est l'intégré, et ces champs
|
||||
ne concernent que Crawl4AI. Affichés d'emblée puis repliés par le
|
||||
chargement de l'état, ils faisaient sauter la section sous les yeux. -->
|
||||
<div id="crawl-fields" style="display:none">
|
||||
<div class="row">
|
||||
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
||||
</div>
|
||||
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
||||
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
||||
qu'un indice sur les 4 derniers caractères. -->
|
||||
<div class="row">
|
||||
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
||||
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
||||
</div>
|
||||
<div class="muted" style="font-size:11px;margin-top:4px">enregistré automatiquement en quittant le champ</div>
|
||||
<div id="crawl-key-state" class="muted" style="font-size:11px;margin-top:3px"></div>
|
||||
</div>
|
||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-block" class="gated-group">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" class="sbadge"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="slist">
|
||||
<div class="rowbody">
|
||||
<!-- Même texte que celui rendu après chargement quand il n'y a aucun
|
||||
serveur : sinon la carte partait vide puis grandissait d'une ligne. -->
|
||||
<div id="mcp-list" class="mcp-list"><div class="muted" style="font-size:12px">(aucun serveur — ajoute un serveur fichiers, une base de données, une API…)</div></div>
|
||||
<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>
|
||||
</details>
|
||||
<details id="tasks-details"><summary><span class="stitle">Tâches planifiées</span><span id="tasks-badge" class="sbadge"></span></summary>
|
||||
<!-- 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>
|
||||
</details>
|
||||
<details><summary><span class="stitle">System prompt</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<textarea id="sysprompt" placeholder="(optionnel) ex: tu es un assistant concis en français" oninput="saveSys()" class="sctl"></textarea>
|
||||
</div></div>
|
||||
</details>
|
||||
<details id="lc-details"><summary><span class="stitle">Moteur</span></summary>
|
||||
<!-- Une seule carte : état, choix du mode et journal d'installation sont le
|
||||
même sujet. Les trois modes sont des LIGNES de cette carte (elles
|
||||
étaient des cartes dans une carte). -->
|
||||
<div class="slist">
|
||||
<!-- Moteur fourni par l'image Docker : aucun des trois modes 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">
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<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>
|
||||
</details>
|
||||
</div><!-- /sidegroup -->
|
||||
<!-- ── PARAMÈTRES DE L'APPLICATION ────────────────────────────────────
|
||||
Séparés des réglages d'IA (Machine, Presets, Mode agent, System
|
||||
prompt, Moteur), qui restent au-dessus : ce qui suit ne concerne que
|
||||
l'application elle-même. -->
|
||||
<div class="side-sep">Paramètres</div>
|
||||
<!-- sidegroup : plusieurs sections dans UNE carte. Une carte par section
|
||||
empilait 11 arrondis et autant de gouttières : la barre latérale ne
|
||||
tenait plus dans un écran, alors que ces lignes sont des titres repliés. -->
|
||||
<div class="sidegroup">
|
||||
<details id="ident-details"><summary><span class="stitle">Identité</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<div class="pe-row">
|
||||
<span class="pe-row-l">Ton prénom<span class="pe-sub">affiché sur tes messages</span></span>
|
||||
<!-- autocomplete/data-*-ignore : sans ça, les gestionnaires de mots de
|
||||
passe prennent ce champ pour un identifiant de connexion et
|
||||
recouvrent la saisie de comptes enregistrés. -->
|
||||
<span class="pe-row-c"><input id="p-username" class="pe-inp" maxlength="24" placeholder="toi"
|
||||
autocomplete="off" name="loki-nickname" data-1p-ignore data-lpignore="true" data-form-type="other"
|
||||
oninput="saveIdentity()"></span>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<span class="pe-row-l">Ton avatar</span>
|
||||
<div id="p-avatar-user" class="avpick"></div>
|
||||
</div>
|
||||
<div class="pe-row stack">
|
||||
<span class="pe-row-l">Avatar de Loki</span>
|
||||
<div id="p-avatar-ai" class="avpick"></div>
|
||||
</div>
|
||||
</div></div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Apparence</span></summary>
|
||||
<div class="slist">
|
||||
<label class="switchrow rev"><span>mode sombre</span>
|
||||
<span class="switch"><input type="checkbox" id="theme-dark" onchange="toggleThemeDark()"><span class="slider"></span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="subhead">Affichage</div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-reasoning" onchange="toggleView('hide-reasoning')"><span class="slider"></span></span> masquer le raisonnement</label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-tools" onchange="toggleView('hide-tools')"><span class="slider"></span></span> masquer les appels d'outils</label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-fold-tools" onchange="toggleView('fold-tools')"><span class="slider"></span></span> garder les bulles repliées<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Les bulles de raisonnement et d'outil arrivent <b>déjà fermées</b> au lieu de s'ouvrir puis se refermer. Un clic sur leur étiquette les déplie.</span></span></label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-stats" onchange="toggleView('hide-stats')"><span class="slider"></span></span> masquer la vitesse de génération<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Sous chaque réponse : <b>prefill</b> (lecture du contexte) et <b>decode</b> (écriture), en tokens et en tokens/seconde. Coche pour ne plus les afficher.</span></span></label>
|
||||
</div>
|
||||
</details>
|
||||
<details><summary><span class="stitle">Accès OpenAI</span></summary>
|
||||
<div class="subhead">Endpoint compatible OpenAI<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Branchez n'importe quel client compatible OpenAI (OpenCode, Continue, Cursor, 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>
|
||||
</details>
|
||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<!-- Ligne 1 : mise à jour + export du fil. Depuis la 0.8 la conversation vit
|
||||
dans loki.db (bbolt, verrouillé tant que le service tourne) : sans le
|
||||
bouton exporter, plus personne ne peut relire/archiver ses échanges. -->
|
||||
<div class="row" style="margin:0">
|
||||
<button id="upd-check" onclick="checkUpdate()">↥ Vérifier les mises à jour</button>
|
||||
<button onclick="openExportModal()" title="choisir le format et ce qu'on emporte">⤓ exporter…</button>
|
||||
</div>
|
||||
<div id="upd-msg" class="muted"></div>
|
||||
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
|
||||
<div class="row" style="margin-bottom:0">
|
||||
<button id="btn-bench" onclick="runBenchUI()">bench</button>
|
||||
<button onclick="resetChat()" 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>
|
||||
</details>
|
||||
</div><!-- /sidegroup -->
|
||||
</details><!-- /#side-settings -->
|
||||
<!-- Réglages : tout vit désormais dans une modale (nav de sections à gauche,
|
||||
panneau à droite) — la barre latérale ne garde que les discussions et le
|
||||
moniteur machine. Voir js/22-settings-modal.js ; les ids historiques des
|
||||
sections (svc-log-box, lc-details…) sont conservés sur les panneaux. -->
|
||||
<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>Réglages</span>
|
||||
</button>
|
||||
</div><!-- /#side-scroll -->
|
||||
<!-- Moniteur machine : charge GPU, VRAM et mémoire vive. Posé en pied, en
|
||||
jauges fines — visible en permanence sans jamais disputer la place au
|
||||
@@ -511,6 +223,340 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
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>Réglages</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">
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<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">
|
||||
|
||||
@@ -24,6 +24,14 @@ async function loadConversations(){
|
||||
let r;
|
||||
try{ r = await jget('/api/conversations'); }catch(_){ return; }
|
||||
CONVS = r.conversations || [];
|
||||
// La plus récente EN TÊTE. Le serveur trie déjà par date d'activité, mais son
|
||||
// tri STABLE laissait une discussion toute neuve SOUS celle qu'on vient de
|
||||
// quitter (les deux sont touchées dans la même seconde) : on départage par la
|
||||
// date de création, puis par l'ordre du fichier d'index (création croissante).
|
||||
const ord = new Map(CONVS.map((c, i) => [c.id, i]));
|
||||
CONVS.sort((a, b) => (b.updated || 0) - (a.updated || 0)
|
||||
|| (b.created || 0) - (a.created || 0)
|
||||
|| ord.get(b.id) - ord.get(a.id));
|
||||
CONV_ACTIVE = r.active || '';
|
||||
CONV_BUSY = !!r.busy;
|
||||
renderConversations();
|
||||
|
||||
@@ -38,6 +38,12 @@ function openDetails(el){
|
||||
if(typeof el === 'string') el = document.getElementById(el);
|
||||
while(el){
|
||||
if(el.tagName === 'DETAILS') el.open = true;
|
||||
// La cible vit dans un panneau de la modale de réglages : ouvrir la modale
|
||||
// sur ce panneau. C'est ce qui garde tous les appelants historiques
|
||||
// (pastille d'état, jobs moteur) valides depuis la refonte en modale.
|
||||
if(el.classList && el.classList.contains('set-pane') && typeof openSettings === 'function'){
|
||||
openSettings(el.dataset.pane);
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// ─── Réglages en modale ──────────────────────────────────────────────────────
|
||||
// Toutes les sections de réglages vivent dans une modale à deux volets : la nav
|
||||
// des sections à gauche, LE panneau choisi à droite. La barre latérale ne garde
|
||||
// que les discussions et le moniteur machine — visuellement plus simple, et
|
||||
// chaque section dispose enfin de toute la largeur.
|
||||
//
|
||||
// Les ids historiques des sections (svc-log-box, lc-details, tasks-details…)
|
||||
// sont posés sur les .set-pane : openDetails() (18-shell.js) les résout vers le
|
||||
// bon panneau, si bien que tous les chemins existants — pastille d'état du
|
||||
// moteur, pastille d'installation, jobs llama.cpp — rouvrent la modale au bon
|
||||
// endroit sans changer une ligne de leurs appelants.
|
||||
let SET_LAST = 'config';
|
||||
// Panneaux à contenu paresseux : chargés à l'affichage — l'équivalent de
|
||||
// l'ancien ontoggle des <details>.
|
||||
const SET_HOOKS = {
|
||||
'engine-log': () => { loadSvcLog(); showPaths(); },
|
||||
};
|
||||
function openSettings(pane){
|
||||
showModal('settings-modal');
|
||||
settingsShow(pane || SET_LAST);
|
||||
}
|
||||
function closeSettings(){ hideModal('settings-modal'); }
|
||||
function settingsShow(pane){
|
||||
SET_LAST = pane;
|
||||
document.querySelectorAll('#set-nav [data-pane]').forEach(b => b.classList.toggle('active', b.dataset.pane === pane));
|
||||
document.querySelectorAll('#set-body .set-pane').forEach(s => s.classList.toggle('active', s.dataset.pane === pane));
|
||||
const body = document.getElementById('set-body');
|
||||
if(body) body.scrollTop = 0;
|
||||
const h = SET_HOOKS[pane]; if(h) h();
|
||||
}
|
||||
// Échap ferme la modale — capture:false, donc les modales à confirmation
|
||||
// (ask-modal, qui écoute en capture) gardent la main quand elles sont ouvertes.
|
||||
document.addEventListener('keydown', e => {
|
||||
if(e.key !== 'Escape') return;
|
||||
const m = document.getElementById('settings-modal');
|
||||
if(m && m.classList.contains('show')) closeSettings();
|
||||
});
|
||||
@@ -2021,6 +2021,44 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
#criteria-list li.crit-pending{color:var(--dim)}
|
||||
|
||||
.rolebadge{display:inline-block;margin-left:6px;padding:0 6px;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font-size:9px;letter-spacing:.05em;text-transform:uppercase;vertical-align:middle}
|
||||
|
||||
/* ── Réglages en modale ─────────────────────────────────────────────────────
|
||||
Deux volets : nav des sections à gauche, LE panneau choisi à droite. Les
|
||||
panneaux réutilisent tels quels les gabarits de la barre latérale (.slist,
|
||||
.switchrow, .subhead…), seuls la coque et le routage sont neufs. */
|
||||
#side-settings-btn{display:flex;align-items:center;gap:8px;width:100%;margin:8px 0 0;padding:10px 12px;
|
||||
background:none;border:0;border-top:1px solid var(--border);border-radius:0;box-shadow:none;
|
||||
font-size:13px;color:var(--text);cursor:pointer;text-align:left}
|
||||
#side-settings-btn:hover{background:var(--bubble)}
|
||||
#side-settings-btn svg{flex:none;opacity:.7}
|
||||
.set-card{max-width:860px;height:min(660px,92vh)}
|
||||
.set-shell{display:flex;flex:1;min-height:0}
|
||||
.set-nav{flex:0 0 200px;border-right:1px solid var(--border);overflow-y:auto;
|
||||
padding:10px 8px;display:flex;flex-direction:column;gap:2px}
|
||||
.set-nav-h{font-size:10px;text-transform:uppercase;letter-spacing:.12em;color:var(--dim);margin:12px 8px 4px}
|
||||
.set-nav-h:first-child{margin-top:2px}
|
||||
.set-nav button{display:flex;align-items:center;gap:8px;width:100%;margin:0;padding:7px 10px;
|
||||
background:none;border:0;border-radius:3px;box-shadow:none;
|
||||
font-size:12.5px;color:var(--text);cursor:pointer;text-align:left}
|
||||
.set-nav button:hover{background:var(--bubble)}
|
||||
.set-nav button.active{background:var(--bubble);font-weight:600}
|
||||
.set-nav button .sbadge{margin-left:auto}
|
||||
.set-body{flex:1;min-width:0;overflow-y:auto;padding:16px}
|
||||
.set-pane{display:none}
|
||||
.set-pane.active{display:block}
|
||||
.set-pane-h{display:flex;align-items:center;gap:8px;font-size:14px;font-weight:600;margin:0 0 12px}
|
||||
.set-pane-h>span:first-child{flex:1;min-width:0}
|
||||
/* Téléphone : la nav passe en rangée défilante au-dessus du panneau, et la
|
||||
modale prend tout l'écran — un panneau de réglages ne se lit pas en timbre. */
|
||||
@media(max-width:720px){
|
||||
.set-ov{padding:0}
|
||||
.set-card{max-width:none;height:100vh;height:100dvh;max-height:none;border-radius:0;border:0}
|
||||
.set-shell{flex-direction:column}
|
||||
.set-nav{flex:none;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;
|
||||
border-right:0;border-bottom:1px solid var(--border);padding:8px;white-space:nowrap}
|
||||
.set-nav-h{display:none}
|
||||
.set-nav button{width:auto;flex:none}
|
||||
}
|
||||
/* Nom du modèle qui a produit la réponse, à côté de « Loki ». Discret (gris,
|
||||
pas de capitales forcées) et borné : un nom de .gguf peut être très long,
|
||||
il s'ellipse au lieu de pousser l'étiquette hors de la bulle. */
|
||||
|
||||
Reference in new issue
Block a user