Un seul jeu d'icônes pour toute l'interface

Les deux icônes de la carte de saisie venaient de la maquette ; tout le reste
était encore des GLYPHES DE TEXTE — ↻ × ☰ ⌫ 🗑 ✎ ↓ ▸ ▣ ·. C'est le système
d'exploitation qui les dessinait : le même bouton sortait fin sur macOS, gras et
bleuté sur Windows (émojis en couleur), et absent d'une machine sans police
d'émojis. Aucune cohérence possible, et rien à régler depuis la feuille de style.

Elles passent en SVG, tracées comme celles de la maquette : boîte de 24, trait de
1,8, extrémités arrondies, couleur héritée. Un seul fichier les décrit
(03-icons.js) — menu, fermer, rafraîchir, crayon, corbeille, téléchargement,
flèche, dossier, fichier, image, plus, chevron.

Touchées : le ☰ du téléphone, les croix de modale, les deux rafraîchissements,
le « + » des sections, la flèche de retour en bas, le téléchargement Hugging
Face, « vider la discussion », les lignes du panneau Fichiers (dossier, image,
fichier, télécharger, supprimer), renommer/supprimer une discussion, éditer un
serveur MCP.

Deux pièges rencontrés, notés dans la feuille :

- Une règle de centrage qui pose `display:inline-flex` sur ces boutons les rend
  VISIBLES en permanence : plusieurs se masquent justement par `display:none`
  (le ☰ hors téléphone, le « + » d'une section fermée, la flèche de retour en
  bas). Le ☰ s'est ainsi retrouvé par-dessus l'en-tête sur grand écran. Le
  centrage passe donc par le rembourrage pour ceux-là.
- Les titres de panneau (.stitle) et l'intertitre « Paramètres » gardaient le
  monospace en capitales de la charte précédente ; ils suivent maintenant le
  seul style de titre de la maquette, celui de « Performance ».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
This commit is contained in:
Claude committed 2026-08-16 19:16:43 +00:00
1 parent 82136d6e0d
commit fef8cca2c4
7 files changed
+236 -50

No files matched your search

+118 -25
View File
@@ -1748,8 +1748,12 @@ html[data-files="1"] #files-btn{color:var(--accent)}
La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du
texte courant (« Performance »). Le monospace en capitales de la charte texte courant (« Performance »). Le monospace en capitales de la charte
précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */ précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */
.side .stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em; /* Le panneau Fichiers porte les mêmes titres que la barre latérale : un seul
style de titre dans toute l'application, celui de la maquette. */
.stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em;
font-size:13.5px;font-weight:600;color:var(--text)} font-size:13.5px;font-weight:600;color:var(--text)}
.side-sep{font-family:var(--font);text-transform:none;letter-spacing:0;
font-size:12px;font-weight:600;color:var(--dim)}
#side-scroll>details>summary,#side-settings>summary{padding:0 14px} #side-scroll>details>summary,#side-settings>summary{padding:0 14px}
#side-settings{margin-top:0;border-top:1px solid var(--border)} #side-settings{margin-top:0;border-top:1px solid var(--border)}
#side-settings>.sidegroup{padding:0 12px} #side-settings>.sidegroup{padding:0 12px}
@@ -1790,12 +1794,43 @@ html[data-files="1"] #files-btn{color:var(--accent)}
.side .preset .conv-when{font-size:13px} .side .preset .conv-when{font-size:13px}
.perf-head{font-size:16px;font-weight:700} .perf-head{font-size:16px;font-weight:700}
.perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500} .perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500}
.side .stitle{font-size:14px;font-weight:600} .stitle{font-size:14px;font-weight:600}
/* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */ /* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */
#brand svg{height:20px} #brand svg{height:20px}
#ver{font-size:13px;font-weight:500;margin-left:8px} #ver{font-size:13px;font-weight:500;margin-left:8px}
/* --- Boutons-icônes : une seule géométrie --------------------------
Toutes ces commandes portaient un GLYPHE DE TEXTE, donc une taille et une
assiette décidées par la police du système. En SVG, elles se centrent
normalement — encore faut-il que leur bouton soit une boîte flex et non une
ligne de texte. */
/* ⚠️ On NE touche PAS à `display` ici : plusieurs de ces boutons s'affichent et
se masquent par ce biais (#menubtn et .preset-add sont en display:none au
repos, #scrollbtn aussi). Une règle de centrage qui pose display:inline-flex
les rendrait visibles en permanence — le ☰ s'est ainsi retrouvé par-dessus
l'en-tête sur grand écran. Le centrage passe donc par le rembourrage, que la
taille connue de chaque bouton rend exact. */
.modal-x,.mcp-edit,#files-refresh,#files-close,
.files-acts .iconbtn,.conv-acts .iconbtn{
display:inline-flex;align-items:center;justify-content:center;line-height:1}
#menubtn{padding:8px;line-height:0}
#scrollbtn{padding:9px;line-height:0}
.preset-add{padding:5px;line-height:0}
#menubtn svg,#scrollbtn svg,.preset-add svg{display:block;margin:0 auto}
.modal-x svg,.mcp-edit svg,#files-refresh svg,#files-close svg,.iconbtn svg{
display:block;width:auto;height:auto}
/* La pastille du fil d'Ariane des fichiers n'est plus un caractère : elle a la
largeur de son dessin. */
.files-ic{width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center}
.files-ic svg{display:block}
.files-row.isdir .files-ic{color:var(--accent)}
/* « Vider la discussion » : icône + libellé, alignés comme un seul bloc. */
#chat-actions-clear,.row>button>svg{vertical-align:-2px}
.row>button:has(svg){display:inline-flex;align-items:center;gap:7px}
/* Le « + » des sections : même carré que les autres commandes de la barre. */
.preset-add{width:26px;height:26px;border-radius:8px}
</style></head><body> </style></head><body>
<button id="menubtn" onclick="toggleSide()">☰</button> <button id="menubtn" onclick="toggleSide()" title="menu" aria-label="ouvrir le menu"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg></button>
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est <!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
fermé sur téléphone. Sans ce rappel, un rechargement de page donnait fermé sur téléphone. Sans ce rappel, un rechargement de page donnait
l'impression que l'installation avait disparu. --> l'impression que l'installation avait disparu. -->
@@ -1821,7 +1856,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
La recherche filtre CÔTÉ CLIENT : la liste est déjà entièrement chargée La recherche filtre CÔTÉ CLIENT : la liste est déjà entièrement chargée
(métadonnées seules, sans les messages), un aller-retour serveur par (métadonnées seules, sans les messages), un aller-retour serveur par
frappe n'apporterait rien. --> frappe n'apporterait rien. -->
<details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion">+</button></summary> <details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion" aria-label="nouvelle discussion"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5.5v13"/><path d="M5.5 12h13"/></svg></button></summary>
<input id="conv-search" type="search" placeholder="rechercher une discussion…" oninput="renderConversations()" aria-label="rechercher une discussion"> <input id="conv-search" type="search" placeholder="rechercher une discussion…" oninput="renderConversations()" aria-label="rechercher une discussion">
<div id="conv-list" class="slist"></div> <div id="conv-list" class="slist"></div>
</details> </details>
@@ -1843,7 +1878,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="sidegroup"> <div class="sidegroup">
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary> <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"> <div class="row" style="justify-content:flex-end;align-items:center">
<button onclick="loadSvcLog()" title="rafraîchir">↻</button> <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> </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> <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 <!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
@@ -1856,7 +1891,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<details open id="cfg-details"><summary><span class="stitle">Config active</span></summary> <details open id="cfg-details"><summary><span class="stitle">Config active</span></summary>
<div id="cfg" class="slist">…</div> <div id="cfg" class="slist">…</div>
</details> </details>
<details><summary><span class="stitle">Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary> <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> <div class="slist"><div id="presets" class="rowlist"></div></div>
</details> </details>
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary> <details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
@@ -2101,7 +2136,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). --> <!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
<div class="row" style="margin-bottom:0"> <div class="row" style="margin-bottom:0">
<button id="btn-bench" onclick="runBenchUI()">bench</button> <button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="resetChat()">⌫ clear chat</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></div> </div></div>
</details> </details>
@@ -2145,7 +2180,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
pas de civilité ni d'accord qui obligerait à deviner. --> pas de civilité ni d'accord qui obligerait à deviner. -->
<div id="ce-title">On commence par quoi ?</div> <div id="ce-title">On commence par quoi ?</div>
</div> </div>
<button id="scrollbtn" onclick="jumpBottom()" title="aller en bas">↓</button> <button id="scrollbtn" onclick="jumpBottom()" title="aller en bas" aria-label="aller en bas"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M6 13l6 6 6-6"/></svg></button>
<!-- Voile de dépôt : révélé pendant qu'on survole la fenêtre avec un fichier. --> <!-- Voile de dépôt : révélé pendant qu'on survole la fenêtre avec un fichier. -->
<div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div> <div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div>
<div id="composer"> <div id="composer">
@@ -2202,8 +2237,8 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<!-- Titre court : le panneau est déjà nommé par son contexte, et le libellé <!-- Titre court : le panneau est déjà nommé par son contexte, et le libellé
complet reste porté par l'aria-label de la section. --> complet reste porté par l'aria-label de la section. -->
<span class="stitle">Fichiers</span> <span class="stitle">Fichiers</span>
<button id="files-refresh" onclick="loadFiles()" title="rafraîchir">↻</button> <button id="files-refresh" onclick="loadFiles()" title="rafraîchir" aria-label="rafraîchir"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg></button>
<button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer">×</button> <button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Fil d'Ariane : le dossier de la discussion a des sous-dossiers (uploads, <!-- Fil d'Ariane : le dossier de la discussion a des sous-dossiers (uploads,
captures), il faut pouvoir y descendre et en remonter. --> captures), il faut pouvoir y descendre et en remonter. -->
@@ -2216,7 +2251,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column"> <div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column">
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center"> <div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<strong>Benchmark</strong> <strong>Benchmark</strong>
<button onclick="closeBenchModal()" style="margin:0">×</button> <button onclick="closeBenchModal()" style="margin:0" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div> <div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div>
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px"> <div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px">
@@ -2233,7 +2268,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="modal-card" style="max-width:460px"> <div class="modal-card" style="max-width:460px">
<div class="modal-head"> <div class="modal-head">
<strong>Exporter la conversation</strong> <strong>Exporter la conversation</strong>
<button class="modal-x" onclick="closeExportModal()" title="fermer">×</button> <button class="modal-x" onclick="closeExportModal()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Conversation vide : pas d'options à régler, un seul message. Proposer <!-- Conversation vide : pas d'options à régler, un seul message. Proposer
des réglages pour un fichier qui ne contiendrait rien n'a aucun sens. --> des réglages pour un fichier qui ne contiendrait rien n'a aucun sens. -->
@@ -2306,10 +2341,10 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
<strong>Backends personnalisés</strong> <strong>Backends personnalisés</strong>
<button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div class="modal-body mform"> <div class="modal-body mform">
<div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton ✎ → section Moteur → « backend détecté »).</div> <div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton crayon → section Moteur → « backend détecté »).</div>
<div class="fld"> <div class="fld">
<label class="fld-l">Installés</label> <label class="fld-l">Installés</label>
<div id="lc-custom-list" class="mcp-list"></div> <div id="lc-custom-list" class="mcp-list"></div>
@@ -2336,7 +2371,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
<strong id="mcp-modal-title">Serveur MCP</strong> <strong id="mcp-modal-title">Serveur MCP</strong>
<button class="modal-x" onclick="closeMcp()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeMcp()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div class="modal-body mform"> <div class="modal-body mform">
<div class="fld"> <div class="fld">
@@ -2395,7 +2430,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<div class="modal-card"> <div class="modal-card">
<div class="modal-head"> <div class="modal-head">
<strong id="modal-title">Preset</strong> <strong id="modal-title">Preset</strong>
<button class="modal-x" onclick="closeModal()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeModal()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Chargement : la modale s'ouvre tout de suite, mais son contenu n'arrive <!-- Chargement : la modale s'ouvre tout de suite, mais son contenu n'arrive
qu'après (jusqu'à 3 requêtes en accès distant). Ce voile se pose PAR-DESSUS qu'après (jusqu'à 3 requêtes en accès distant). Ce voile se pose PAR-DESSUS
@@ -2443,7 +2478,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">ou un projecteur mmproj — lien direct vers un .gguf</span></span> <span class="pe-row-l">Télécharger un modèle <span class="pe-sub">ou un projecteur mmproj — lien direct vers un .gguf</span></span>
<div class="pe-inpbtn"> <div class="pe-inpbtn">
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf"> <input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">↓</button> <button id="m-hf-btn" onclick="startDownload()" title="Télécharger" aria-label="Télécharger"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/><path d="M4.5 19.5h15"/></svg></button>
</div> </div>
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div> <div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
<div id="m-hf-progress" class="pe-note" style="display:none"></div> <div id="m-hf-progress" class="pe-note" style="display:none"></div>
@@ -2771,6 +2806,57 @@ async function loadPrefs(){
}catch(e){} }catch(e){}
} }
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('loki.sys') || ''; loadSys(); restoreChat(); }); document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('loki.sys') || ''; loadSys(); restoreChat(); });
// ─── Jeu d'icônes ────────────────────────────────────────────────────────────
// Un seul trait pour toute l'interface : 24 unités de côté, 1,8 d'épaisseur,
// extrémités et jonctions arrondies — celui des deux icônes de la maquette
// (dépôt et envoi), étendu au reste de l'application.
//
// Elles étaient jusqu'ici des GLYPHES DE TEXTE (↻ × ☰ 🗑 ✎ ↓ ▸). C'est le
// système d'exploitation qui les dessinait : le même bouton sortait fin sur
// macOS, gras et bleuté sur Windows (émojis en couleur), et carrément absent sur
// un serveur sans police d'émojis. Aucune cohérence possible, et rien à régler
// depuis la feuille de style. En SVG, elles suivent currentColor et la taille
// qu'on leur donne.
//
// Ajouter une icône : une entrée ici, et rien d'autre — surtout pas un second
// dessin ailleurs, c'est ainsi que le trait finit par diverger.
const ICONS = {
menu: '<path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/>',
close: '<path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/>',
refresh: '<path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/>',
pencil: '<path d="M4 20h4L19 9a2.1 2.1 0 0 0-3-3L5 17z"/><path d="M14.5 7.5l3 3"/>',
trash: '<path d="M4 7h16"/><path d="M9.5 7V4.5h5V7"/><path d="M6.5 7l1 12.5h9l1-12.5"/>',
download: '<path d="M12 4v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/><path d="M4.5 19.5h15"/>',
arrowDown: '<path d="M12 5v14"/><path d="M6 13l6 6 6-6"/>',
folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
file: '<path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5z"/><path d="M13.5 3.5V8.5h5"/>',
image: '<rect x="3.5" y="5" width="17" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M4.5 17.5 10 12l3.5 3.5L16 13l3.5 3.5"/>',
plus: '<path d="M12 5.5v13"/><path d="M5.5 12h13"/>',
chevron: '<path d="M9.5 5.5 16 12l-6.5 6.5"/>',
search: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.4-4.4"/>',
sun: '<circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2"/><path d="M12 19.3v2.2"/><path d="M4.2 4.2l1.6 1.6"/><path d="M18.2 18.2l1.6 1.6"/><path d="M2.5 12h2.2"/><path d="M19.3 12h2.2"/><path d="M4.2 19.8l1.6-1.6"/><path d="M18.2 5.8l1.6-1.6"/>',
};
// iconHTML rend l'icône en chaîne, pour les endroits qui composent déjà du
// balisage. `aria-hidden` : le sens est porté par le title/aria-label du bouton,
// jamais par le dessin.
function iconHTML(name, size){
const d = ICONS[name];
if(!d) return '';
const n = size || 18;
return '<svg viewBox="0 0 24 24" width="' + n + '" height="' + n + '" fill="none" '
+ 'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
+ 'stroke-linejoin="round" aria-hidden="true">' + d + '</svg>';
}
// icon rend l'icône en ÉLÉMENT, pour les listes construites en DOM (fichiers,
// discussions) où l'on ne veut pas d'innerHTML.
function icon(name, size){
const span = document.createElement('span');
span.className = 'icn';
span.innerHTML = iconHTML(name, size); // contenu constant, jamais de donnée
return span.firstElementChild || span;
}
// Pastille d'état d'une section du menu (mode agent, internet, MCP, distant). // Pastille d'état d'une section du menu (mode agent, internet, MCP, distant).
// `state` : true = actif (pilule pleine), false = inactif (contour), 'warn'/'err' // `state` : true = actif (pilule pleine), false = inactif (contour), 'warn'/'err'
// = état signalé, null = pastille masquée. Un seul endroit décide de leur allure. // = état signalé, null = pastille masquée. Un seul endroit décide de leur allure.
@@ -5930,7 +6016,8 @@ function renderMCP(r){
sw.appendChild(cb); sw.appendChild(sl); sw.appendChild(cb); sw.appendChild(sl);
const edit = document.createElement('button'); const edit = document.createElement('button');
edit.className = 'mcp-edit'; edit.title = 'éditer'; edit.textContent = '✎'; edit.className = 'mcp-edit'; edit.title = 'éditer';
edit.setAttribute('aria-label', 'éditer ce serveur'); edit.appendChild(icon('pencil', 15));
edit.onclick = (e)=>{ e.stopPropagation(); openMcp(s.name); }; edit.onclick = (e)=>{ e.stopPropagation(); openMcp(s.name); };
row.appendChild(dot); row.appendChild(info); row.appendChild(sw); row.appendChild(edit); row.appendChild(dot); row.appendChild(info); row.appendChild(sw); row.appendChild(edit);
@@ -6524,7 +6611,7 @@ function fileRow(e){
const ic = document.createElement('span'); const ic = document.createElement('span');
ic.className = 'files-ic'; ic.className = 'files-ic';
ic.textContent = e.dir ? '▸' : (e.image ? '▣' : '·'); ic.appendChild(icon(e.dir ? 'folder' : (e.image ? 'image' : 'file'), 16));
row.appendChild(ic); row.appendChild(ic);
const mid = document.createElement('span'); const mid = document.createElement('span');
@@ -6553,12 +6640,14 @@ function fileRow(e){
acts.className = 'files-acts'; acts.className = 'files-acts';
if(!e.dir){ if(!e.dir){
const dl = document.createElement('button'); const dl = document.createElement('button');
dl.className = 'iconbtn'; dl.textContent = '↓'; dl.title = 'télécharger'; dl.className = 'iconbtn'; dl.appendChild(icon('download', 16));
dl.title = 'télécharger'; dl.setAttribute('aria-label', 'télécharger');
dl.onclick = ev=>{ ev.stopPropagation(); downloadWorkspaceFile(e.path, e.name, row); }; dl.onclick = ev=>{ ev.stopPropagation(); downloadWorkspaceFile(e.path, e.name, row); };
acts.appendChild(dl); acts.appendChild(dl);
} }
const rm = document.createElement('button'); const rm = document.createElement('button');
rm.className = 'iconbtn'; rm.textContent = '🗑'; rm.title = 'supprimer'; rm.className = 'iconbtn'; rm.appendChild(icon('trash', 16));
rm.title = 'supprimer'; rm.setAttribute('aria-label', 'supprimer');
rm.onclick = ev=>{ ev.stopPropagation(); removeFile(e); }; rm.onclick = ev=>{ ev.stopPropagation(); removeFile(e); };
acts.appendChild(rm); acts.appendChild(rm);
row.appendChild(acts); row.appendChild(acts);
@@ -6662,10 +6751,14 @@ function renderConversations(){
// Renommer / supprimer : stopPropagation, sinon le clic bascule aussi. // Renommer / supprimer : stopPropagation, sinon le clic bascule aussi.
const acts = document.createElement('div'); acts.className = 'conv-acts'; const acts = document.createElement('div'); acts.className = 'conv-acts';
const ren = document.createElement('button'); ren.className = 'iconbtn'; ren.textContent = '✎'; const ren = document.createElement('button'); ren.className = 'iconbtn';
ren.title = 'renommer'; ren.onclick = e => { e.stopPropagation(); convRename(c.id, title); }; ren.appendChild(icon('pencil', 15)); ren.title = 'renommer';
const del = document.createElement('button'); del.className = 'iconbtn'; del.textContent = '🗑'; ren.setAttribute('aria-label', 'renommer la discussion');
del.title = 'supprimer'; del.onclick = e => { e.stopPropagation(); convDelete(c.id, title); }; ren.onclick = e => { e.stopPropagation(); convRename(c.id, title); };
const del = document.createElement('button'); del.className = 'iconbtn';
del.appendChild(icon('trash', 15)); del.title = 'supprimer';
del.setAttribute('aria-label', 'supprimer la discussion');
del.onclick = e => { e.stopPropagation(); convDelete(c.id, title); };
acts.appendChild(ren); acts.appendChild(del); acts.appendChild(ren); acts.appendChild(del);
row.append(info, when, acts); row.append(info, when, acts);
+15 -15
View File
@@ -31,7 +31,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<style> <style>
@@CSS@@ @@CSS@@
</style></head><body> </style></head><body>
<button id="menubtn" onclick="toggleSide()">☰</button> <button id="menubtn" onclick="toggleSide()" title="menu" aria-label="ouvrir le menu"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/></svg></button>
<!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est <!-- Installation d'un moteur en cours : le détail vit dans le panneau, qui est
fermé sur téléphone. Sans ce rappel, un rechargement de page donnait fermé sur téléphone. Sans ce rappel, un rechargement de page donnait
l'impression que l'installation avait disparu. --> l'impression que l'installation avait disparu. -->
@@ -57,7 +57,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
La recherche filtre CÔTÉ CLIENT : la liste est déjà entièrement chargée La recherche filtre CÔTÉ CLIENT : la liste est déjà entièrement chargée
(métadonnées seules, sans les messages), un aller-retour serveur par (métadonnées seules, sans les messages), un aller-retour serveur par
frappe n'apporterait rien. --> frappe n'apporterait rien. -->
<details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion">+</button></summary> <details id="conv-details" open><summary><span class="stitle">Discussions</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();convNew()" title="nouvelle discussion" aria-label="nouvelle discussion"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5.5v13"/><path d="M5.5 12h13"/></svg></button></summary>
<input id="conv-search" type="search" placeholder="rechercher une discussion…" oninput="renderConversations()" aria-label="rechercher une discussion"> <input id="conv-search" type="search" placeholder="rechercher une discussion…" oninput="renderConversations()" aria-label="rechercher une discussion">
<div id="conv-list" class="slist"></div> <div id="conv-list" class="slist"></div>
</details> </details>
@@ -79,7 +79,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="sidegroup"> <div class="sidegroup">
<details id="svc-log-box" ontoggle="if(this.open){loadSvcLog();showPaths()}"><summary><span class="stitle">Journal du moteur</span></summary> <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"> <div class="row" style="justify-content:flex-end;align-items:center">
<button onclick="loadSvcLog()" title="rafraîchir">↻</button> <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> </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> <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 <!-- Emplacements : au même endroit que le journal, c'est le panneau où l'on
@@ -92,7 +92,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<details open id="cfg-details"><summary><span class="stitle">Config active</span></summary> <details open id="cfg-details"><summary><span class="stitle">Config active</span></summary>
<div id="cfg" class="slist">…</div> <div id="cfg" class="slist">…</div>
</details> </details>
<details><summary><span class="stitle">Presets</span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary> <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> <div class="slist"><div id="presets" class="rowlist"></div></div>
</details> </details>
<details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary> <details><summary><span class="stitle">Mode agent</span><span id="agent-badge" class="sbadge"></span></summary>
@@ -337,7 +337,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). --> <!-- Ligne 2 : bench + clear chat (seule action destructive, gardée en dernier). -->
<div class="row" style="margin-bottom:0"> <div class="row" style="margin-bottom:0">
<button id="btn-bench" onclick="runBenchUI()">bench</button> <button id="btn-bench" onclick="runBenchUI()">bench</button>
<button onclick="resetChat()">⌫ clear chat</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></div> </div></div>
</details> </details>
@@ -381,7 +381,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
pas de civilité ni d'accord qui obligerait à deviner. --> pas de civilité ni d'accord qui obligerait à deviner. -->
<div id="ce-title">On commence par quoi ?</div> <div id="ce-title">On commence par quoi ?</div>
</div> </div>
<button id="scrollbtn" onclick="jumpBottom()" title="aller en bas">↓</button> <button id="scrollbtn" onclick="jumpBottom()" title="aller en bas" aria-label="aller en bas"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M6 13l6 6 6-6"/></svg></button>
<!-- Voile de dépôt : révélé pendant qu'on survole la fenêtre avec un fichier. --> <!-- Voile de dépôt : révélé pendant qu'on survole la fenêtre avec un fichier. -->
<div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div> <div id="dropveil" aria-hidden="true"><div>Déposer pour joindre au message</div></div>
<div id="composer"> <div id="composer">
@@ -438,8 +438,8 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<!-- Titre court : le panneau est déjà nommé par son contexte, et le libellé <!-- Titre court : le panneau est déjà nommé par son contexte, et le libellé
complet reste porté par l'aria-label de la section. --> complet reste porté par l'aria-label de la section. -->
<span class="stitle">Fichiers</span> <span class="stitle">Fichiers</span>
<button id="files-refresh" onclick="loadFiles()" title="rafraîchir">↻</button> <button id="files-refresh" onclick="loadFiles()" title="rafraîchir" aria-label="rafraîchir"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/></svg></button>
<button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer">×</button> <button id="files-close" onclick="toggleFiles()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Fil d'Ariane : le dossier de la discussion a des sous-dossiers (uploads, <!-- Fil d'Ariane : le dossier de la discussion a des sous-dossiers (uploads,
captures), il faut pouvoir y descendre et en remonter. --> captures), il faut pouvoir y descendre et en remonter. -->
@@ -452,7 +452,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column"> <div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column">
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center"> <div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<strong>Benchmark</strong> <strong>Benchmark</strong>
<button onclick="closeBenchModal()" style="margin:0">×</button> <button onclick="closeBenchModal()" style="margin:0" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div> <div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div>
<div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px"> <div style="padding:12px 16px;border-top:1px solid var(--border);display:flex;justify-content:flex-end;gap:6px">
@@ -469,7 +469,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="modal-card" style="max-width:460px"> <div class="modal-card" style="max-width:460px">
<div class="modal-head"> <div class="modal-head">
<strong>Exporter la conversation</strong> <strong>Exporter la conversation</strong>
<button class="modal-x" onclick="closeExportModal()" title="fermer">×</button> <button class="modal-x" onclick="closeExportModal()" title="fermer" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Conversation vide : pas d'options à régler, un seul message. Proposer <!-- Conversation vide : pas d'options à régler, un seul message. Proposer
des réglages pour un fichier qui ne contiendrait rien n'a aucun sens. --> des réglages pour un fichier qui ne contiendrait rien n'a aucun sens. -->
@@ -542,10 +542,10 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
<strong>Backends personnalisés</strong> <strong>Backends personnalisés</strong>
<button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div class="modal-body mform"> <div class="modal-body mform">
<div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton ✎ → section Moteur → « backend détecté »).</div> <div class="muted" style="font-size:12px;line-height:1.5">Compile un fork de llama.cpp depuis un dépôt Git — pour les modèles qui exigent un moteur spécial (quant ternaire, etc.). Un backend personnalisé <b>ne remplace pas</b> le moteur : vous le choisissez ensuite <b>par modèle</b> (bouton crayon → section Moteur → « backend détecté »).</div>
<div class="fld"> <div class="fld">
<label class="fld-l">Installés</label> <label class="fld-l">Installés</label>
<div id="lc-custom-list" class="mcp-list"></div> <div id="lc-custom-list" class="mcp-list"></div>
@@ -572,7 +572,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="modal-card" style="max-width:560px"> <div class="modal-card" style="max-width:560px">
<div class="modal-head"> <div class="modal-head">
<strong id="mcp-modal-title">Serveur MCP</strong> <strong id="mcp-modal-title">Serveur MCP</strong>
<button class="modal-x" onclick="closeMcp()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeMcp()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<div class="modal-body mform"> <div class="modal-body mform">
<div class="fld"> <div class="fld">
@@ -631,7 +631,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<div class="modal-card"> <div class="modal-card">
<div class="modal-head"> <div class="modal-head">
<strong id="modal-title">Preset</strong> <strong id="modal-title">Preset</strong>
<button class="modal-x" onclick="closeModal()" aria-label="Fermer">×</button> <button class="modal-x" onclick="closeModal()" aria-label="Fermer"><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/></svg></button>
</div> </div>
<!-- Chargement : la modale s'ouvre tout de suite, mais son contenu n'arrive <!-- Chargement : la modale s'ouvre tout de suite, mais son contenu n'arrive
qu'après (jusqu'à 3 requêtes en accès distant). Ce voile se pose PAR-DESSUS qu'après (jusqu'à 3 requêtes en accès distant). Ce voile se pose PAR-DESSUS
@@ -679,7 +679,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">ou un projecteur mmproj — lien direct vers un .gguf</span></span> <span class="pe-row-l">Télécharger un modèle <span class="pe-sub">ou un projecteur mmproj — lien direct vers un .gguf</span></span>
<div class="pe-inpbtn"> <div class="pe-inpbtn">
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf"> <input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">↓</button> <button id="m-hf-btn" onclick="startDownload()" title="Télécharger" aria-label="Télécharger"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 4v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/><path d="M4.5 19.5h15"/></svg></button>
</div> </div>
<div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div> <div id="m-hf-bar" class="pe-bar" style="display:none"><i></i></div>
<div id="m-hf-progress" class="pe-note" style="display:none"></div> <div id="m-hf-progress" class="pe-note" style="display:none"></div>
+51
View File
@@ -0,0 +1,51 @@
// ─── Jeu d'icônes ────────────────────────────────────────────────────────────
// Un seul trait pour toute l'interface : 24 unités de côté, 1,8 d'épaisseur,
// extrémités et jonctions arrondies — celui des deux icônes de la maquette
// (dépôt et envoi), étendu au reste de l'application.
//
// Elles étaient jusqu'ici des GLYPHES DE TEXTE (↻ × ☰ 🗑 ✎ ↓ ▸). C'est le
// système d'exploitation qui les dessinait : le même bouton sortait fin sur
// macOS, gras et bleuté sur Windows (émojis en couleur), et carrément absent sur
// un serveur sans police d'émojis. Aucune cohérence possible, et rien à régler
// depuis la feuille de style. En SVG, elles suivent currentColor et la taille
// qu'on leur donne.
//
// Ajouter une icône : une entrée ici, et rien d'autre — surtout pas un second
// dessin ailleurs, c'est ainsi que le trait finit par diverger.
const ICONS = {
menu: '<path d="M4 7h16"/><path d="M4 12h16"/><path d="M4 17h16"/>',
close: '<path d="M6.5 6.5l11 11"/><path d="M17.5 6.5l-11 11"/>',
refresh: '<path d="M20.5 12a8.5 8.5 0 1 1-2.5-6"/><path d="M20.5 3.5V9H15"/>',
pencil: '<path d="M4 20h4L19 9a2.1 2.1 0 0 0-3-3L5 17z"/><path d="M14.5 7.5l3 3"/>',
trash: '<path d="M4 7h16"/><path d="M9.5 7V4.5h5V7"/><path d="M6.5 7l1 12.5h9l1-12.5"/>',
download: '<path d="M12 4v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/><path d="M4.5 19.5h15"/>',
arrowDown: '<path d="M12 5v14"/><path d="M6 13l6 6 6-6"/>',
folder: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
file: '<path d="M13.5 3.5H7A1.5 1.5 0 0 0 5.5 5v14A1.5 1.5 0 0 0 7 20.5h10a1.5 1.5 0 0 0 1.5-1.5V8.5z"/><path d="M13.5 3.5V8.5h5"/>',
image: '<rect x="3.5" y="5" width="17" height="14" rx="2"/><circle cx="8.5" cy="10" r="1.4"/><path d="M4.5 17.5 10 12l3.5 3.5L16 13l3.5 3.5"/>',
plus: '<path d="M12 5.5v13"/><path d="M5.5 12h13"/>',
chevron: '<path d="M9.5 5.5 16 12l-6.5 6.5"/>',
search: '<circle cx="11" cy="11" r="6.5"/><path d="M20 20l-4.4-4.4"/>',
sun: '<circle cx="12" cy="12" r="4.2"/><path d="M12 2.5v2.2"/><path d="M12 19.3v2.2"/><path d="M4.2 4.2l1.6 1.6"/><path d="M18.2 18.2l1.6 1.6"/><path d="M2.5 12h2.2"/><path d="M19.3 12h2.2"/><path d="M4.2 19.8l1.6-1.6"/><path d="M18.2 5.8l1.6-1.6"/>',
};
// iconHTML rend l'icône en chaîne, pour les endroits qui composent déjà du
// balisage. `aria-hidden` : le sens est porté par le title/aria-label du bouton,
// jamais par le dessin.
function iconHTML(name, size){
const d = ICONS[name];
if(!d) return '';
const n = size || 18;
return '<svg viewBox="0 0 24 24" width="' + n + '" height="' + n + '" fill="none" '
+ 'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '
+ 'stroke-linejoin="round" aria-hidden="true">' + d + '</svg>';
}
// icon rend l'icône en ÉLÉMENT, pour les listes construites en DOM (fichiers,
// discussions) où l'on ne veut pas d'innerHTML.
function icon(name, size){
const span = document.createElement('span');
span.className = 'icn';
span.innerHTML = iconHTML(name, size); // contenu constant, jamais de donnée
return span.firstElementChild || span;
}
+2 -1
View File
@@ -63,7 +63,8 @@ function renderMCP(r){
sw.appendChild(cb); sw.appendChild(sl); sw.appendChild(cb); sw.appendChild(sl);
const edit = document.createElement('button'); const edit = document.createElement('button');
edit.className = 'mcp-edit'; edit.title = 'éditer'; edit.textContent = '✎'; edit.className = 'mcp-edit'; edit.title = 'éditer';
edit.setAttribute('aria-label', 'éditer ce serveur'); edit.appendChild(icon('pencil', 15));
edit.onclick = (e)=>{ e.stopPropagation(); openMcp(s.name); }; edit.onclick = (e)=>{ e.stopPropagation(); openMcp(s.name); };
row.appendChild(dot); row.appendChild(info); row.appendChild(sw); row.appendChild(edit); row.appendChild(dot); row.appendChild(info); row.appendChild(sw); row.appendChild(edit);
+5 -3
View File
@@ -142,7 +142,7 @@ function fileRow(e){
const ic = document.createElement('span'); const ic = document.createElement('span');
ic.className = 'files-ic'; ic.className = 'files-ic';
ic.textContent = e.dir ? '▸' : (e.image ? '▣' : '·'); ic.appendChild(icon(e.dir ? 'folder' : (e.image ? 'image' : 'file'), 16));
row.appendChild(ic); row.appendChild(ic);
const mid = document.createElement('span'); const mid = document.createElement('span');
@@ -171,12 +171,14 @@ function fileRow(e){
acts.className = 'files-acts'; acts.className = 'files-acts';
if(!e.dir){ if(!e.dir){
const dl = document.createElement('button'); const dl = document.createElement('button');
dl.className = 'iconbtn'; dl.textContent = '↓'; dl.title = 'télécharger'; dl.className = 'iconbtn'; dl.appendChild(icon('download', 16));
dl.title = 'télécharger'; dl.setAttribute('aria-label', 'télécharger');
dl.onclick = ev=>{ ev.stopPropagation(); downloadWorkspaceFile(e.path, e.name, row); }; dl.onclick = ev=>{ ev.stopPropagation(); downloadWorkspaceFile(e.path, e.name, row); };
acts.appendChild(dl); acts.appendChild(dl);
} }
const rm = document.createElement('button'); const rm = document.createElement('button');
rm.className = 'iconbtn'; rm.textContent = '🗑'; rm.title = 'supprimer'; rm.className = 'iconbtn'; rm.appendChild(icon('trash', 16));
rm.title = 'supprimer'; rm.setAttribute('aria-label', 'supprimer');
rm.onclick = ev=>{ ev.stopPropagation(); removeFile(e); }; rm.onclick = ev=>{ ev.stopPropagation(); removeFile(e); };
acts.appendChild(rm); acts.appendChild(rm);
row.appendChild(acts); row.appendChild(acts);
+8 -4
View File
@@ -60,10 +60,14 @@ function renderConversations(){
// Renommer / supprimer : stopPropagation, sinon le clic bascule aussi. // Renommer / supprimer : stopPropagation, sinon le clic bascule aussi.
const acts = document.createElement('div'); acts.className = 'conv-acts'; const acts = document.createElement('div'); acts.className = 'conv-acts';
const ren = document.createElement('button'); ren.className = 'iconbtn'; ren.textContent = '✎'; const ren = document.createElement('button'); ren.className = 'iconbtn';
ren.title = 'renommer'; ren.onclick = e => { e.stopPropagation(); convRename(c.id, title); }; ren.appendChild(icon('pencil', 15)); ren.title = 'renommer';
const del = document.createElement('button'); del.className = 'iconbtn'; del.textContent = '🗑'; ren.setAttribute('aria-label', 'renommer la discussion');
del.title = 'supprimer'; del.onclick = e => { e.stopPropagation(); convDelete(c.id, title); }; ren.onclick = e => { e.stopPropagation(); convRename(c.id, title); };
const del = document.createElement('button'); del.className = 'iconbtn';
del.appendChild(icon('trash', 15)); del.title = 'supprimer';
del.setAttribute('aria-label', 'supprimer la discussion');
del.onclick = e => { e.stopPropagation(); convDelete(c.id, title); };
acts.appendChild(ren); acts.appendChild(del); acts.appendChild(ren); acts.appendChild(del);
row.append(info, when, acts); row.append(info, when, acts);
+37 -2
View File
@@ -1717,8 +1717,12 @@ html[data-files="1"] #files-btn{color:var(--accent)}
La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du La maquette n'a qu'un style de titre : casse normale, demi-gras, taille du
texte courant (« Performance »). Le monospace en capitales de la charte texte courant (« Performance »). Le monospace en capitales de la charte
précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */ précédente y jurait, et faisait crier des libellés qui n'ont rien à dire. */
.side .stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em; /* Le panneau Fichiers porte les mêmes titres que la barre latérale : un seul
style de titre dans toute l'application, celui de la maquette. */
.stitle{font-family:var(--font);text-transform:none;letter-spacing:-.01em;
font-size:13.5px;font-weight:600;color:var(--text)} font-size:13.5px;font-weight:600;color:var(--text)}
.side-sep{font-family:var(--font);text-transform:none;letter-spacing:0;
font-size:12px;font-weight:600;color:var(--dim)}
#side-scroll>details>summary,#side-settings>summary{padding:0 14px} #side-scroll>details>summary,#side-settings>summary{padding:0 14px}
#side-settings{margin-top:0;border-top:1px solid var(--border)} #side-settings{margin-top:0;border-top:1px solid var(--border)}
#side-settings>.sidegroup{padding:0 12px} #side-settings>.sidegroup{padding:0 12px}
@@ -1759,7 +1763,38 @@ html[data-files="1"] #files-btn{color:var(--accent)}
.side .preset .conv-when{font-size:13px} .side .preset .conv-when{font-size:13px}
.perf-head{font-size:16px;font-weight:700} .perf-head{font-size:16px;font-weight:700}
.perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500} .perf-l,.perf-l .perf-pct{font-size:14px;font-weight:500}
.side .stitle{font-size:14px;font-weight:600} .stitle{font-size:14px;font-weight:600}
/* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */ /* Marque : « Loki v0.9.4 » en tête de colonne, à l'échelle des titres. */
#brand svg{height:20px} #brand svg{height:20px}
#ver{font-size:13px;font-weight:500;margin-left:8px} #ver{font-size:13px;font-weight:500;margin-left:8px}
/* --- Boutons-icônes : une seule géométrie --------------------------
Toutes ces commandes portaient un GLYPHE DE TEXTE, donc une taille et une
assiette décidées par la police du système. En SVG, elles se centrent
normalement — encore faut-il que leur bouton soit une boîte flex et non une
ligne de texte. */
/* ⚠️ On NE touche PAS à `display` ici : plusieurs de ces boutons s'affichent et
se masquent par ce biais (#menubtn et .preset-add sont en display:none au
repos, #scrollbtn aussi). Une règle de centrage qui pose display:inline-flex
les rendrait visibles en permanence — le ☰ s'est ainsi retrouvé par-dessus
l'en-tête sur grand écran. Le centrage passe donc par le rembourrage, que la
taille connue de chaque bouton rend exact. */
.modal-x,.mcp-edit,#files-refresh,#files-close,
.files-acts .iconbtn,.conv-acts .iconbtn{
display:inline-flex;align-items:center;justify-content:center;line-height:1}
#menubtn{padding:8px;line-height:0}
#scrollbtn{padding:9px;line-height:0}
.preset-add{padding:5px;line-height:0}
#menubtn svg,#scrollbtn svg,.preset-add svg{display:block;margin:0 auto}
.modal-x svg,.mcp-edit svg,#files-refresh svg,#files-close svg,.iconbtn svg{
display:block;width:auto;height:auto}
/* La pastille du fil d'Ariane des fichiers n'est plus un caractère : elle a la
largeur de son dessin. */
.files-ic{width:16px;height:16px;display:inline-flex;align-items:center;justify-content:center}
.files-ic svg{display:block}
.files-row.isdir .files-ic{color:var(--accent)}
/* « Vider la discussion » : icône + libellé, alignés comme un seul bloc. */
#chat-actions-clear,.row>button>svg{vertical-align:-2px}
.row>button:has(svg){display:inline-flex;align-items:center;gap:7px}
/* Le « + » des sections : même carré que les autres commandes de la barre. */
.preset-add{width:26px;height:26px;border-radius:8px}