mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 07:28:13 +02:00
v0.6.4 : serveurs MCP connectes en parallele et prechauffes, vitesse de generation de retour, retouches du menu
MCP : les serveurs actifs se connectent en parallele au lieu de la file indienne (quatre serveurs npx n'additionnent plus leurs demarrages) et sont prechauffes au lancement du service, donc le premier message ne paie plus le handshake. Une session est publiee dans le pool avant d'aboutir : deux appelants simultanes partagent la meme tentative au lieu de lancer deux process. Interface : la vitesse de generation (prefill/decode) reapparait sous chaque reponse, en fondu, avec un interrupteur pour la masquer ; infobulles " ? " qui n'etaient plus rognees ; espace superflu en haut des cartes du mode agent ; pastilles d'etat redessinees et plaquees a droite ; moteurs renommes en llama.cpp precompile / compile / personnalise ; badge conseille lisible en sombre ; section Service fondue dans Moteur ; plus qu'un enregistrement automatique pour l'acces internet. Documentation : le README dit desormais que Crawl4AI est un serveur a heberger soi-meme, avec la commande Docker.
This commit is contained in:
1 parent
92c350bdb8
commit
1360eb1943
22 files changed
+409
-135
No files matched your search
@@ -220,6 +220,14 @@ La clé est stockée dans `$JEAN_HOME/.web_key`, distincte de `.api_key` (pilota
|
|||||||
|
|
||||||
Par défaut l'IA n'a pas accès au web. En branchant un serveur [Crawl4AI](https://github.com/unclecode/crawl4ai) (Chrome headless), elle gagne quatre outils : `web_search` (DuckDuckGo), `web_open` (récupère une page → métadonnées + plan), `web_read` (lit une plage de lignes) et `web_grep` (regex dans une page ouverte).
|
Par défaut l'IA n'a pas accès au web. En branchant un serveur [Crawl4AI](https://github.com/unclecode/crawl4ai) (Chrome headless), elle gagne quatre outils : `web_search` (DuckDuckGo), `web_open` (récupère une page → métadonnées + plan), `web_read` (lit une plage de lignes) et `web_grep` (regex dans une page ouverte).
|
||||||
|
|
||||||
|
**AJEAN ne fournit pas le serveur Crawl4AI, il s'y branche** — c'est à toi de l'héberger (sur la même machine ou ailleurs), typiquement en Docker :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis côté AJEAN :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
jean internet url http://localhost:11235 # URL de ton serveur Crawl4AI
|
jean internet url http://localhost:11235 # URL de ton serveur Crawl4AI
|
||||||
jean internet on # active l'accès web
|
jean internet on # active l'accès web
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
|||||||
"FileVersion": {
|
"FileVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 6,
|
"Minor": 6,
|
||||||
"Patch": 3,
|
"Patch": 4,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 0,
|
"Major": 0,
|
||||||
"Minor": 6,
|
"Minor": 6,
|
||||||
"Patch": 3,
|
"Patch": 4,
|
||||||
"Build": 0
|
"Build": 0
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
||||||
"OriginalFilename": "jean.exe",
|
"OriginalFilename": "jean.exe",
|
||||||
"ProductName": "AJEAN",
|
"ProductName": "AJEAN",
|
||||||
"ProductVersion": "0.6.3",
|
"ProductVersion": "0.6.4",
|
||||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
+77
-21
@@ -18,10 +18,15 @@ import (
|
|||||||
|
|
||||||
// Pool de sessions MCP + exposition des outils au moteur de chat.
|
// Pool de sessions MCP + exposition des outils au moteur de chat.
|
||||||
//
|
//
|
||||||
// Une session est établie paresseusement au premier besoin (premier tour de chat
|
// Une session est gardée ouverte entre les tours de chat : un serveur stdio garde
|
||||||
// avec le mode agent actif) et gardée ouverte pour les tours suivants. Un serveur
|
// son process vivant d'un message à l'autre — c'est voulu, desktop-commander & co
|
||||||
// stdio garde donc son process vivant entre les messages — c'est voulu :
|
// ont un coût de démarrage non négligeable.
|
||||||
// desktop-commander & co ont un coût de démarrage non négligeable.
|
//
|
||||||
|
// Les connexions sont établies EN PARALLÈLE (mcpEnsureAll) : en séquence, quatre
|
||||||
|
// serveurs `npx` faisaient attendre la somme de leurs démarrages (~20 s), là où
|
||||||
|
// les autres clients MCP paient le plus lent. Elles sont aussi pré-chauffées au
|
||||||
|
// démarrage du service web/link (MCPPrewarm) pour que le premier message ne paie
|
||||||
|
// pas le handshake.
|
||||||
//
|
//
|
||||||
// Le namespacing des outils suit la convention de l'écosystème :
|
// Le namespacing des outils suit la convention de l'écosystème :
|
||||||
// mcp__<serveur>__<outil>. On maintient un registre nom-exposé → (serveur, outil)
|
// mcp__<serveur>__<outil>. On maintient un registre nom-exposé → (serveur, outil)
|
||||||
@@ -39,12 +44,17 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// mcpSession est une connexion vivante à un serveur MCP.
|
// mcpSession est une connexion vivante à un serveur MCP.
|
||||||
|
//
|
||||||
|
// L'entrée est publiée dans le pool AVANT que la connexion aboutisse, pour qu'un
|
||||||
|
// second appelant se mette en attente au lieu de lancer un deuxième process. Les
|
||||||
|
// champs sess/tools/err ne doivent donc être lus qu'une fois `ready` fermé.
|
||||||
type mcpSession struct {
|
type mcpSession struct {
|
||||||
name string
|
name string
|
||||||
cfg MCPServerConfig
|
cfg MCPServerConfig
|
||||||
sess *mcpsdk.ClientSession
|
sess *mcpsdk.ClientSession
|
||||||
tools []*mcpsdk.Tool
|
tools []*mcpsdk.Tool
|
||||||
err error // dernière erreur de connexion/liste, pour l'UI
|
err error // dernière erreur de connexion/liste, pour l'UI
|
||||||
|
ready chan struct{} // fermé quand la tentative de connexion est terminée
|
||||||
}
|
}
|
||||||
|
|
||||||
// mcpManager détient le pool de sessions.
|
// mcpManager détient le pool de sessions.
|
||||||
@@ -79,6 +89,9 @@ func mcpInvalidate(name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mcpMgr.mu.Unlock()
|
mcpMgr.mu.Unlock()
|
||||||
|
// Une session encore en cours de connexion a sess == nil : c'est sa goroutine
|
||||||
|
// qui refermera ce qu'elle vient d'ouvrir, en constatant qu'elle n'est plus
|
||||||
|
// dans le pool.
|
||||||
if s != nil && s.sess != nil {
|
if s != nil && s.sess != nil {
|
||||||
_ = s.sess.Close()
|
_ = s.sess.Close()
|
||||||
}
|
}
|
||||||
@@ -145,16 +158,22 @@ func mcpConnect(ctx context.Context, name string, cfg MCPServerConfig) (*mcpsdk.
|
|||||||
|
|
||||||
// ensure renvoie une session vivante pour un serveur activé, en la (re)créant si
|
// ensure renvoie une session vivante pour un serveur activé, en la (re)créant si
|
||||||
// besoin. Doit être appelé sans détenir mgr.mu (il gère le lock lui-même).
|
// besoin. Doit être appelé sans détenir mgr.mu (il gère le lock lui-même).
|
||||||
|
//
|
||||||
|
// L'entrée est réservée dans le pool avant la connexion : deux appelants
|
||||||
|
// simultanés (le pré-chauffage et un tour de chat, par exemple) partagent la même
|
||||||
|
// tentative au lieu de lancer deux process stdio.
|
||||||
func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
|
func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
if s, ok := m.sessions[name]; ok {
|
if s, ok := m.sessions[name]; ok {
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
<-s.ready // connexion peut-être encore en cours : on attend son issue
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
s := &mcpSession{name: name, cfg: cfg, ready: make(chan struct{})}
|
||||||
|
m.sessions[name] = s
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
// Connexion hors-lock (peut être lente).
|
// Connexion hors-lock (peut être lente).
|
||||||
s := &mcpSession{name: name, cfg: cfg}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), mcpConnectTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
sess, err := mcpConnect(ctx, name, cfg)
|
sess, err := mcpConnect(ctx, name, cfg)
|
||||||
@@ -171,28 +190,63 @@ func (m *mcpManager) ensure(name string, cfg MCPServerConfig) *mcpSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
// Un autre appelant a pu connecter entretemps : garder la première.
|
// La session a pu être invalidée pendant la connexion (changement de config,
|
||||||
if existing, ok := m.sessions[name]; ok {
|
// arrêt du service) : dans ce cas on ne la republie pas, on referme.
|
||||||
m.mu.Unlock()
|
stale := m.sessions[name] != s
|
||||||
if s.sess != nil {
|
if !stale {
|
||||||
_ = s.sess.Close()
|
// (Re)peuple le registre pour ce serveur.
|
||||||
|
for exposed, ref := range m.registry {
|
||||||
|
if ref.server == name {
|
||||||
|
delete(m.registry, exposed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return existing
|
for _, t := range s.tools {
|
||||||
}
|
m.registry[mcpExposedName(name, t.Name)] = mcpToolRef{server: name, tool: t.Name}
|
||||||
m.sessions[name] = s
|
|
||||||
// (Re)peuple le registre pour ce serveur.
|
|
||||||
for exposed, ref := range m.registry {
|
|
||||||
if ref.server == name {
|
|
||||||
delete(m.registry, exposed)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, t := range s.tools {
|
|
||||||
m.registry[mcpExposedName(name, t.Name)] = mcpToolRef{server: name, tool: t.Name}
|
|
||||||
}
|
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
close(s.ready)
|
||||||
|
if stale && s.sess != nil {
|
||||||
|
_ = s.sess.Close()
|
||||||
|
s.sess = nil
|
||||||
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mcpEnsureAll connecte en parallèle tous les serveurs activés et attend qu'ils
|
||||||
|
// soient tous fixés (connectés ou en erreur). C'est ce qui évite d'additionner
|
||||||
|
// les temps de démarrage : le coût total est celui du serveur le plus lent.
|
||||||
|
func mcpEnsureAll(servers map[string]MCPServerConfig) {
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for _, name := range sortedServerNames(servers) {
|
||||||
|
cfg := servers[name]
|
||||||
|
if !cfg.Enabled || cfg.Transport() == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(name string, cfg MCPServerConfig) {
|
||||||
|
defer wg.Done()
|
||||||
|
mcpMgr.ensure(name, cfg)
|
||||||
|
}(name, cfg)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCPPrewarm établit les sessions MCP en tâche de fond, au démarrage du service
|
||||||
|
// web/link. Sans ça le handshake était payé par le premier message de chat, ce
|
||||||
|
// qui donnait l'impression d'une IA très lente à démarrer. Sans mode agent les
|
||||||
|
// outils MCP ne sont de toute façon pas exposés : inutile de lancer les process.
|
||||||
|
func MCPPrewarm() {
|
||||||
|
if !agentEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
servers, err := LoadMCPConfig()
|
||||||
|
if err != nil || len(servers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go mcpEnsureAll(servers)
|
||||||
|
}
|
||||||
|
|
||||||
var mcpSanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
|
var mcpSanitizeRe = regexp.MustCompile(`[^a-zA-Z0-9_-]+`)
|
||||||
|
|
||||||
// mcpSanitize rend une chaîne compatible avec les noms d'outils (^[A-Za-z0-9_-]+$).
|
// mcpSanitize rend une chaîne compatible avec les noms d'outils (^[A-Za-z0-9_-]+$).
|
||||||
@@ -213,6 +267,7 @@ func mcpTools() []Tool {
|
|||||||
if err != nil || len(servers) == 0 {
|
if err != nil || len(servers) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
mcpEnsureAll(servers) // connexions en parallèle, puis lecture du pool (instantanée)
|
||||||
var out []Tool
|
var out []Tool
|
||||||
for _, name := range sortedServerNames(servers) {
|
for _, name := range sortedServerNames(servers) {
|
||||||
cfg := servers[name]
|
cfg := servers[name]
|
||||||
@@ -419,6 +474,7 @@ func MCPStatus() ([]MCPServerStatus, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
mcpEnsureAll(servers) // en parallèle : le panneau MCP de l'UI n'attend plus la somme
|
||||||
var out []MCPServerStatus
|
var out []MCPServerStatus
|
||||||
for _, name := range sortedServerNames(servers) {
|
for _, name := range sortedServerNames(servers) {
|
||||||
cfg := servers[name]
|
cfg := servers[name]
|
||||||
|
|||||||
@@ -3,9 +3,49 @@ package jean
|
|||||||
import (
|
import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TestMCPEnsureDeduplicatesConcurrentConnects vérifie que N appelants simultanés
|
||||||
|
// (pré-chauffage + tour de chat + panneau MCP de l'UI) partagent une seule
|
||||||
|
// tentative de connexion : sans ça, connecter en parallèle multiplierait les
|
||||||
|
// process stdio lancés pour un même serveur.
|
||||||
|
func TestMCPEnsureDeduplicatesConcurrentConnects(t *testing.T) {
|
||||||
|
t.Setenv("JEAN_HOME", t.TempDir())
|
||||||
|
t.Cleanup(mcpCloseAll)
|
||||||
|
|
||||||
|
// Commande inexistante : la connexion échoue, mais l'entrée du pool est bien
|
||||||
|
// partagée — c'est ce qu'on teste.
|
||||||
|
cfg := MCPServerConfig{Command: "jean-binaire-inexistant-pour-test", Enabled: true}
|
||||||
|
|
||||||
|
const callers = 8
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
got := make([]*mcpSession, callers)
|
||||||
|
for i := range got {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
got[i] = mcpMgr.ensure("bidon", cfg)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for i, s := range got {
|
||||||
|
if s != got[0] {
|
||||||
|
t.Fatalf("appelant %d a obtenu une session différente : %p vs %p", i, s, got[0])
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.ready:
|
||||||
|
default:
|
||||||
|
t.Fatalf("appelant %d a reçu une session pas encore prête", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got[0].err == nil {
|
||||||
|
t.Fatal("une commande inexistante devrait produire une erreur de connexion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestMCPEndToEnd connecte le serveur MCP de référence (@modelcontextprotocol/
|
// TestMCPEndToEnd connecte le serveur MCP de référence (@modelcontextprotocol/
|
||||||
// server-everything via npx), vérifie la découverte d'outils namespacés puis un
|
// server-everything via npx), vérifie la découverte d'outils namespacés puis un
|
||||||
// appel réel (echo). Skippé si npx est absent (CI sans Node).
|
// appel réel (echo). Skippé si npx est absent (CI sans Node).
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const Version = "0.6.3"
|
const Version = "0.6.4"
|
||||||
|
|
||||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
||||||
func Main() {
|
func Main() {
|
||||||
|
|||||||
+138
-55
@@ -22,7 +22,7 @@
|
|||||||
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
||||||
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
||||||
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
||||||
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||||
<style>
|
<style>
|
||||||
/* ===== Palette =====================================================
|
/* ===== Palette =====================================================
|
||||||
Un seul thème (« Clean ») : noir et blanc, sobre. Deux variantes — claire par
|
Un seul thème (« Clean ») : noir et blanc, sobre. Deux variantes — claire par
|
||||||
@@ -173,7 +173,10 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
les confondre avec les traits des sections principales. */
|
les confondre avec les traits des sections principales. */
|
||||||
/* Bloc du mode agent = carte de liste : le rembourrage est porté par les
|
/* Bloc du mode agent = carte de liste : le rembourrage est porté par les
|
||||||
lignes (comme .slist), pas par la carte, sinon les retraits se cumulent. */
|
lignes (comme .slist), pas par la carte, sinon les retraits se cumulent. */
|
||||||
.gated-block{margin:6px 0;border:1px solid var(--border);border-radius:10px;padding:0 0 10px;background:var(--panel);overflow:hidden;transition:opacity .15s}
|
/* ⚠️ PAS d'overflow:hidden ici : les bulles d'aide (« ? ») sont positionnées en
|
||||||
|
absolu par rapport à la ligne, donc un débordement caché les TRONQUE — c'est
|
||||||
|
ce qui rendait plusieurs infobulles illisibles. */
|
||||||
|
.gated-block{margin:6px 0;border:1px solid var(--border);border-radius:10px;padding:0 0 10px;background:var(--panel);overflow:visible;transition:opacity .15s}
|
||||||
.gated-block>.switchrow:first-of-type{padding-top:0}
|
.gated-block>.switchrow:first-of-type{padding-top:0}
|
||||||
.gated-block.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
.gated-block.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
||||||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||||
@@ -329,7 +332,24 @@ details>summary{display:flex;align-items:center;gap:8px;height:34px;font-size:13
|
|||||||
couleur distinguent les deux niveaux. */
|
couleur distinguent les deux niveaux. */
|
||||||
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
||||||
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
|
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
|
||||||
.sbadge{flex:none;font-size:10px}
|
/* Pastille d'état d'une section (mode agent, accès internet, MCP, accès distant).
|
||||||
|
Un point + un mot, dans une pilule — l'ancien couple « on / off » en étiquette
|
||||||
|
carrée se lisait mal et jurait avec le reste du menu. Actif = pilule pleine
|
||||||
|
(inversée), inactif = contour discret. */
|
||||||
|
.sbadge{flex:none;display:inline-flex;align-items:center;gap:5px;
|
||||||
|
font-family:var(--font);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
|
||||||
|
padding:2px 8px;border-radius:999px;border:1px solid var(--border);color:var(--dim);background:transparent;white-space:nowrap}
|
||||||
|
.sbadge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.55}
|
||||||
|
.sbadge.on{background:var(--text);border-color:var(--text);color:var(--bg)}
|
||||||
|
.sbadge.on::before{opacity:1}
|
||||||
|
.sbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
|
||||||
|
.sbadge.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
|
||||||
|
.sbadge:empty{display:none}
|
||||||
|
/* Dans une étiquette de groupe, la pastille se plaque à DROITE (comme dans les
|
||||||
|
titres de section, où .stitle occupe la place) au lieu de coller au libellé.
|
||||||
|
L'ordre visuel reste : libellé · « ? » · … · pastille. */
|
||||||
|
.subhead .sbadge{margin-left:auto;order:2}
|
||||||
|
.subhead .help{order:1}
|
||||||
details>summary::-webkit-details-marker{display:none}
|
details>summary::-webkit-details-marker{display:none}
|
||||||
details>summary:hover{color:var(--dim)}
|
details>summary:hover{color:var(--dim)}
|
||||||
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
|
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
|
||||||
@@ -343,11 +363,20 @@ details[open]>summary::after{transform:rotate(45deg)}
|
|||||||
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
|
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
|
||||||
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
|
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
|
||||||
.io-cap{margin:0 2px 5px}
|
.io-cap{margin:0 2px 5px}
|
||||||
|
/* Premier élément d'une carte : sa marge haute s'ajoutait au rembourrage de la
|
||||||
|
carte (18 px de vide au-dessus de « Mémoire », « Accès internet », etc.). */
|
||||||
|
.gated-block>:first-child{margin-top:0}
|
||||||
|
/* Une étiquette de groupe qui suit immédiatement le titre de section n'a pas
|
||||||
|
besoin de reprendre de l'air : le titre en a déjà donné. */
|
||||||
|
details>summary+.subhead{margin-top:4px}
|
||||||
.io-group{margin-top:8px}
|
.io-group{margin-top:8px}
|
||||||
|
|
||||||
/* --- Carte de liste --- */
|
/* --- Carte de liste --- */
|
||||||
|
/* overflow VISIBLE : cf. la note sur .gated-block — les infobulles doivent
|
||||||
|
pouvoir déborder de la carte. Les lignes n'ont pas de fond propre, l'arrondi
|
||||||
|
n'a donc rien à rogner. */
|
||||||
.slist,.gated-block{background:var(--panel);border:1px solid var(--border);border-radius:9px;
|
.slist,.gated-block{background:var(--panel);border:1px solid var(--border);border-radius:9px;
|
||||||
overflow:hidden;margin:0 0 8px}
|
overflow:visible;margin:0 0 8px}
|
||||||
.slist:last-child,.gated-block:last-child{margin-bottom:0}
|
.slist:last-child,.gated-block:last-child{margin-bottom:0}
|
||||||
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
|
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
|
||||||
.slist.pad,.gated-block{padding:8px 10px}
|
.slist.pad,.gated-block{padding:8px 10px}
|
||||||
@@ -490,7 +519,9 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
|||||||
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
|
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
|
||||||
.lc-mode:hover{border-color:var(--dim)}
|
.lc-mode:hover{border-color:var(--dim)}
|
||||||
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
|
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
|
||||||
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border:1px solid var(--accent);border-radius:8px;padding:1px 6px}
|
/* « conseillé » : pilule PLEINE (texte inversé). En contour, elle héritait du
|
||||||
|
gris atténué du thème monochrome et devenait illisible en mode sombre. */
|
||||||
|
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:8px;padding:1px 6px}
|
||||||
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
|
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
|
||||||
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
.lc-mode-go{color:var(--accent)}
|
.lc-mode-go{color:var(--accent)}
|
||||||
@@ -694,6 +725,19 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
|||||||
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
||||||
#chat .msg .label{display:none}
|
#chat .msg .label{display:none}
|
||||||
#chat .msg.collapsible .label{display:block}
|
#chat .msg.collapsible .label{display:block}
|
||||||
|
/* Mesures de génération sous la réponse (prefill / decode). Assez discrètes pour
|
||||||
|
ne pas concurrencer le texte, assez lisibles pour être utiles. Escamotables
|
||||||
|
depuis Apparence → Affichage. */
|
||||||
|
#chat .msg .statline{margin-top:10px;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;
|
||||||
|
color:var(--dim);opacity:.75}
|
||||||
|
#chat .msg .statline:empty{display:none}
|
||||||
|
html[data-hide-stats="1"] #chat .msg .statline{display:none}
|
||||||
|
/* Arrivée en fondu montant plutôt qu'un surgissement sec. L'opacité finale (.75)
|
||||||
|
est celle de la règle ci-dessus : l'animation doit y ATTERRIR, sinon la ligne
|
||||||
|
clignote en repassant à sa valeur de repos. */
|
||||||
|
#chat .msg .statline-in{animation:statin .28s cubic-bezier(.2,.7,.3,1) both}
|
||||||
|
@keyframes statin{from{opacity:0;transform:translateY(3px)}to{opacity:.75;transform:none}}
|
||||||
|
@media (prefers-reduced-motion:reduce){#chat .msg .statline-in{animation:none}}
|
||||||
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
|
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
|
||||||
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
|
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
|
||||||
#chat .msg.reasoning{color:var(--dim)}
|
#chat .msg.reasoning{color:var(--dim)}
|
||||||
@@ -757,7 +801,7 @@ button,.addbtn,.mem-search{border-radius:10px}
|
|||||||
.preset{background:var(--panel);border-color:var(--border)}
|
.preset{background:var(--panel);border-color:var(--border)}
|
||||||
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
|
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
|
||||||
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
|
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
|
||||||
.subhead,.lc-reco,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
|
.subhead,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
|
||||||
button:hover{border-color:var(--dim);color:var(--text)}
|
button:hover{border-color:var(--dim);color:var(--text)}
|
||||||
|
|
||||||
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
|
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
|
||||||
@@ -832,20 +876,21 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="net-block" class="gated-block">
|
<div id="net-block" class="gated-block">
|
||||||
<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> via un serveur <b>Crawl4AI</b>. Actifs seulement si le serveur répond.</span></span></div>
|
<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> via un serveur <b>Crawl4AI</b> que tu héberges toi-même (ex. <code>docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest</code>). Actifs seulement si le <b>mode agent</b> est actif et que le serveur répond.</span></span></div>
|
||||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
||||||
|
<!-- URL et clé s'enregistrent en quittant le champ (onchange) : deux boutons
|
||||||
|
« enregistrer » côte à côte n'apportaient rien et brouillaient la carte. -->
|
||||||
<div class="row" style="margin-top:6px">
|
<div class="row" style="margin-top:6px">
|
||||||
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
||||||
<button onclick="saveCrawlUrl()">enregistrer</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
||||||
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
||||||
qu'un indice sur les 4 derniers caractères. -->
|
qu'un indice sur les 4 derniers caractères. -->
|
||||||
<div class="row" style="margin-top:6px">
|
<div class="row" style="margin-top:6px">
|
||||||
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
||||||
<button onclick="saveCrawlKey()">enregistrer</button>
|
|
||||||
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
||||||
</div>
|
</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 id="crawl-key-state" class="muted" style="font-size:11px;margin-top:3px"></div>
|
||||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -871,6 +916,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<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-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-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-side" onchange="toggleView('hide-side')"><span class="slider"></span></span> barre latérale escamotable<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">La barre latérale devient un tiroir <b>même sur ordinateur</b> : elle reste fermée et s'ouvre avec le bouton ☰. Toute la largeur pour la conversation.</span></span></label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-side" onchange="toggleView('hide-side')"><span class="slider"></span></span> barre latérale escamotable<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">La barre latérale devient un tiroir <b>même sur ordinateur</b> : elle reste fermée et s'ouvre avec le bouton ☰. Toute la largeur pour la conversation.</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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary><span class="stitle">System prompt</span></summary>
|
<details><summary><span class="stitle">System prompt</span></summary>
|
||||||
@@ -943,18 +989,18 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<div id="lc-status" class="lc-head muted">…</div>
|
<div id="lc-status" class="lc-head muted">…</div>
|
||||||
<div class="lc-modes">
|
<div class="lc-modes">
|
||||||
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
||||||
<div class="lc-mode-t">Version rapide <span class="lc-reco">conseillé</span></div>
|
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco">conseillé</span></div>
|
||||||
<div class="lc-mode-d">prête à l'emploi, installée en ~2 min</div>
|
<div class="lc-mode-d">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
||||||
<div class="lc-mode-state" id="lc-fast-state"></div>
|
<div class="lc-mode-state" id="lc-fast-state"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
||||||
<div class="lc-mode-t">Version optimisée</div>
|
<div class="lc-mode-t">llama.cpp compilé</div>
|
||||||
<div class="lc-mode-d">compilée pour votre machine, plus longue</div>
|
<div class="lc-mode-d">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
||||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||||
<div class="lc-mode-t">Backend personnalisé</div>
|
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
||||||
<div class="lc-mode-d">forks llama.cpp pour modèles spéciaux (ternaire…)</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 class="lc-mode-state" id="lc-custom-state"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -963,14 +1009,16 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
||||||
<pre id="lc-log" hidden></pre>
|
<pre id="lc-log" hidden></pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||||
<details><summary><span class="stitle">Service</span></summary>
|
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 pad">
|
<div class="slist pad">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button onclick="act('start')">start</button>
|
<button onclick="act('start')">démarrer</button>
|
||||||
<button onclick="act('restart')">restart</button>
|
<button onclick="act('restart')">redémarrer</button>
|
||||||
<button onclick="act('stop')">stop</button>
|
<button onclick="act('stop')">arrêter</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
@@ -1163,8 +1211,8 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
|||||||
<div class="pe-group">
|
<div class="pe-group">
|
||||||
<div class="pe-gh">Moteur</div>
|
<div class="pe-gh">Moteur</div>
|
||||||
<div class="pe-seg">
|
<div class="pe-seg">
|
||||||
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Rapide</span><span class="be-note" id="be-fast-note"></span></label>
|
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Précompilé</span><span class="be-note" id="be-fast-note"></span></label>
|
||||||
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Optimisé</span><span class="be-note" id="be-opt-note"></span></label>
|
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Compilé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||||
@@ -1359,6 +1407,7 @@ const VIEW_OPTS=[
|
|||||||
{id:'hide-tools', label:"masquer les appels d'outils"},
|
{id:'hide-tools', label:"masquer les appels d'outils"},
|
||||||
{id:'fold-tools', label:'garder les bulles repliées'},
|
{id:'fold-tools', label:'garder les bulles repliées'},
|
||||||
{id:'hide-side', label:'barre latérale escamotable'},
|
{id:'hide-side', label:'barre latérale escamotable'},
|
||||||
|
{id:'hide-stats', label:'masquer la vitesse de génération'},
|
||||||
];
|
];
|
||||||
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
||||||
function applyView(id, on){
|
function applyView(id, on){
|
||||||
@@ -1409,6 +1458,15 @@ async function loadPrefs(){
|
|||||||
}catch(e){}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
|
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
|
||||||
|
// Pastille d'état d'une section du menu (mode agent, internet, MCP, distant).
|
||||||
|
// `state` : true = actif (pilule pleine), false = inactif (contour), 'warn'/'err'
|
||||||
|
// = état signalé, null = pastille masquée. Un seul endroit décide de leur allure.
|
||||||
|
function setBadge(id, state, text){
|
||||||
|
const el = typeof id==='string' ? document.getElementById(id) : id;
|
||||||
|
if(!el) return;
|
||||||
|
el.className = 'sbadge' + (state===true ? ' on' : (state==='warn' || state==='err') ? ' '+state : '');
|
||||||
|
el.textContent = state===null || state===undefined ? '' : (text||'');
|
||||||
|
}
|
||||||
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800); }
|
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800); }
|
||||||
// Modales natives (askConfirm/askPrompt/askAlert) — remplacent confirm()/prompt()/
|
// Modales natives (askConfirm/askPrompt/askAlert) — remplacent confirm()/prompt()/
|
||||||
// alert() par de vraies boîtes stylées. Chacune renvoie une Promise : askConfirm →
|
// alert() par de vraies boîtes stylées. Chacune renvoie une Promise : askConfirm →
|
||||||
@@ -1610,17 +1668,17 @@ async function loadRam(){
|
|||||||
}
|
}
|
||||||
async function loadCfg(){
|
async function loadCfg(){
|
||||||
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
||||||
// moteur rapide (prebuilt.in_use) ou optimisé (in_use) — sinon c'est un custom.
|
// précompilé (prebuilt.in_use) ou compilé ici (in_use) — sinon c'est un fork perso.
|
||||||
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
||||||
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
||||||
const rows=[];
|
const rows=[];
|
||||||
if(c.BIN){
|
if(c.BIN){
|
||||||
// Moteur : rapide / optimisé / custom (avec le chemin). Le title garde
|
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
|
||||||
// toujours le chemin complet, quel que soit le libellé.
|
// toujours le chemin complet, quel que soit le libellé.
|
||||||
let v;
|
let v;
|
||||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='rapide';
|
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
|
||||||
else if(lc && lc.in_use) v='optimisé';
|
else if(lc && lc.in_use) v='llama.cpp compilé';
|
||||||
else v='custom : '+c.BIN;
|
else v='llama.cpp personnalisé : '+c.BIN;
|
||||||
rows.push(row('MOTEUR', v, c.BIN));
|
rows.push(row('MOTEUR', v, c.BIN));
|
||||||
}
|
}
|
||||||
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
||||||
@@ -1686,8 +1744,7 @@ async function loadAgent(){
|
|||||||
const on = s.enabled;
|
const on = s.enabled;
|
||||||
document.getElementById('agent-toggle').checked = on;
|
document.getElementById('agent-toggle').checked = on;
|
||||||
document.getElementById('compact-toggle').checked = (s.compact !== false);
|
document.getElementById('compact-toggle').checked = (s.compact !== false);
|
||||||
document.getElementById('agent-badge').innerHTML = on
|
setBadge('agent-badge', on, on?'actif':'inactif');
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
|
||||||
document.getElementById('brand').classList.toggle('agent', on);
|
document.getElementById('brand').classList.toggle('agent', on);
|
||||||
setAgentGate(on);
|
setAgentGate(on);
|
||||||
if(s.mem_mode){ document.getElementById('mem-mode').value = s.mem_mode; renderMemModeDesc(s.mem_mode); }
|
if(s.mem_mode){ document.getElementById('mem-mode').value = s.mem_mode; renderMemModeDesc(s.mem_mode); }
|
||||||
@@ -1773,8 +1830,10 @@ function renderInternet(s){
|
|||||||
document.getElementById('internet-toggle').checked = internetOn;
|
document.getElementById('internet-toggle').checked = internetOn;
|
||||||
if(document.activeElement !== document.getElementById('crawl-url'))
|
if(document.activeElement !== document.getElementById('crawl-url'))
|
||||||
document.getElementById('crawl-url').value = s.url || '';
|
document.getElementById('crawl-url').value = s.url || '';
|
||||||
document.getElementById('internet-badge').innerHTML = internetOn
|
// Actif mais serveur injoignable : la pastille le dit (sinon l'UI ment — les
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
// outils web ne sont pas proposés au modèle dans ce cas).
|
||||||
|
if(internetOn && s.url && !s.reachable) setBadge('internet-badge','warn','injoignable');
|
||||||
|
else setBadge('internet-badge', internetOn, internetOn?'actif':'inactif');
|
||||||
// Clé du serveur Crawl4AI : le champ reste vide (la clé n'est jamais renvoyée),
|
// Clé du serveur Crawl4AI : le champ reste vide (la clé n'est jamais renvoyée),
|
||||||
// on indique seulement si une clé est enregistrée.
|
// on indique seulement si une clé est enregistrée.
|
||||||
const ks=document.getElementById('crawl-key-state'), kc=document.getElementById('crawl-key-clear');
|
const ks=document.getElementById('crawl-key-state'), kc=document.getElementById('crawl-key-clear');
|
||||||
@@ -1865,6 +1924,7 @@ async function toggleInternet(){
|
|||||||
async function saveCrawlUrl(){
|
async function saveCrawlUrl(){
|
||||||
const url=document.getElementById('crawl-url').value.trim();
|
const url=document.getElementById('crawl-url').value.trim();
|
||||||
renderInternet(await jpost('/api/internet',{url}));
|
renderInternet(await jpost('/api/internet',{url}));
|
||||||
|
toast(url ? 'serveur Crawl4AI enregistré' : 'serveur Crawl4AI retiré');
|
||||||
}
|
}
|
||||||
async function saveCrawlKey(){
|
async function saveCrawlKey(){
|
||||||
const el=document.getElementById('crawl-key'), key=el.value.trim();
|
const el=document.getElementById('crawl-key'), key=el.value.trim();
|
||||||
@@ -2028,7 +2088,7 @@ function onPickModel(){
|
|||||||
toast('MODEL='+val);
|
toast('MODEL='+val);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choix du moteur PAR MODÈLE : 3 options (rapide / optimisé / personnalisé)
|
// Choix du moteur PAR MODÈLE : 3 options (précompilé / compilé / personnalisé)
|
||||||
// qui réécrivent la ligne BIN= du preset. C'est LE point où on décide quel
|
// qui réécrivent la ligne BIN= du preset. C'est LE point où on décide quel
|
||||||
// backend fait tourner ce modèle — la barre latérale ne fait qu'installer.
|
// backend fait tourner ce modèle — la barre latérale ne fait qu'installer.
|
||||||
function currentBinInTextarea(){
|
function currentBinInTextarea(){
|
||||||
@@ -2078,11 +2138,11 @@ function toggleBackendCustom(mode){
|
|||||||
function onBackendMode(mode){
|
function onBackendMode(mode){
|
||||||
toggleBackendCustom(mode);
|
toggleBackendCustom(mode);
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!beFastPath){ toast('installez d\'abord la version rapide (section MOTEUR)'); return; }
|
if(!beFastPath){ toast('installez d\'abord llama.cpp précompilé (section MOTEUR)'); return; }
|
||||||
setBinInTextarea(beFastPath); toast('moteur : rapide');
|
setBinInTextarea(beFastPath); toast('moteur : llama.cpp précompilé');
|
||||||
} else if(mode === 'opt'){
|
} else if(mode === 'opt'){
|
||||||
if(!beOptPath){ toast('installez d\'abord la version optimisée (section MOTEUR)'); return; }
|
if(!beOptPath){ toast('installez d\'abord llama.cpp compilé (section MOTEUR)'); return; }
|
||||||
setBinInTextarea(beOptPath); toast('moteur : optimisé');
|
setBinInTextarea(beOptPath); toast('moteur : llama.cpp compilé');
|
||||||
}
|
}
|
||||||
// custom : on attend que l'utilisateur saisisse un chemin / choisisse un backend
|
// custom : on attend que l'utilisateur saisisse un chemin / choisisse un backend
|
||||||
}
|
}
|
||||||
@@ -2357,6 +2417,24 @@ function collapseInstant(el){
|
|||||||
requestAnimationFrame(()=>{ bw.style.transition=''; });
|
requestAnimationFrame(()=>{ bw.style.transition=''; });
|
||||||
}
|
}
|
||||||
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
|
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
|
||||||
|
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
|
||||||
|
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
|
||||||
|
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
|
||||||
|
// par la préférence d'affichage « masquer la vitesse de génération » (CSS).
|
||||||
|
function setStats(el, text){
|
||||||
|
if(!el) return;
|
||||||
|
let s = el.querySelector(':scope > .statline');
|
||||||
|
if(!s){
|
||||||
|
s=document.createElement('div'); s.className='statline';
|
||||||
|
// Apparition en fondu, à la PREMIÈRE pose seulement : la ligne arrive une fois
|
||||||
|
// la réponse finie, un surgissement sec accrochait l'œil. Les mises à jour
|
||||||
|
// suivantes ne rejouent pas l'animation (elle clignoterait), et le rejeu du
|
||||||
|
// journal au chargement n'anime rien du tout.
|
||||||
|
if(!(typeof REPLAYING!=='undefined' && REPLAYING)) s.classList.add('statline-in');
|
||||||
|
el.appendChild(s);
|
||||||
|
}
|
||||||
|
s.textContent = text;
|
||||||
|
}
|
||||||
function bodyOf(el){ return el.querySelector('.body'); }
|
function bodyOf(el){ return el.querySelector('.body'); }
|
||||||
// Render markdown into a message body in place; safe because md() escapes HTML.
|
// Render markdown into a message body in place; safe because md() escapes HTML.
|
||||||
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); markNotices(b); addCopyButtons(b); scrollMaybe(); }
|
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); markNotices(b); addCopyButtons(b); scrollMaybe(); }
|
||||||
@@ -2568,12 +2646,16 @@ function showTyping(){ if(!simpleMode()) return; const c=document.getElementById
|
|||||||
// qu'au replay — pas de timer performance.now, qui n'a pas de sens hors-ligne).
|
// qu'au replay — pas de timer performance.now, qui n'a pas de sens hors-ligne).
|
||||||
function renderStats(el, s){
|
function renderStats(el, s){
|
||||||
if(!el||!s) return;
|
if(!el||!s) return;
|
||||||
const role=(el===T.contentEl)?'assistant':'reasoning';
|
const parts=[];
|
||||||
const parts=[role];
|
|
||||||
const pt=s.prompt_tokens||s.prompt_tokens_total;
|
const pt=s.prompt_tokens||s.prompt_tokens_total;
|
||||||
if(pt) parts.push('prefill '+pt+' tok · '+(s.prompt_per_second||0).toFixed(0)+' tok/s');
|
if(pt) parts.push('prefill '+pt+' tok · '+(s.prompt_per_second||0).toFixed(0)+' tok/s');
|
||||||
if(s.gen_tokens) parts.push('decode '+s.gen_tokens+' tok · '+(s.gen_per_second||0).toFixed(1)+' tok/s');
|
if(s.gen_tokens) parts.push('decode '+s.gen_tokens+' tok · '+(s.gen_per_second||0).toFixed(1)+' tok/s');
|
||||||
setLabel(el, parts.join(' · '));
|
if(!parts.length) return;
|
||||||
|
// Réponse de l'assistant : ligne de mesures dédiée sous le texte (son étiquette
|
||||||
|
// est masquée dans cette mise en page). Bulle repliable : l'étiquette EST le
|
||||||
|
// bouton de repli, on y écrit comme avant.
|
||||||
|
if(el.classList.contains('collapsible')) setLabel(el, ['reasoning'].concat(parts).join(' · '));
|
||||||
|
else setStats(el, parts.join(' · '));
|
||||||
}
|
}
|
||||||
// Label d'une bulle : nombre de tokens + vitesse. La vitesse est calculée à
|
// Label d'une bulle : nombre de tokens + vitesse. La vitesse est calculée à
|
||||||
// partir des HORODATAGES SERVEUR (firstTs→lastTs) : le temps réel de génération,
|
// partir des HORODATAGES SERVEUR (firstTs→lastTs) : le temps réel de génération,
|
||||||
@@ -2727,8 +2809,9 @@ setInterval(loadStatus, 5000);
|
|||||||
setInterval(loadVram, 3000);
|
setInterval(loadVram, 3000);
|
||||||
setInterval(loadRam, 3000);
|
setInterval(loadRam, 3000);
|
||||||
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
||||||
// « Version rapide » = binaires officiels précompilés (aucune compilation)
|
// « llama.cpp précompilé » = binaires officiels, aucune compilation
|
||||||
// « Version optimisée » = compilée pour la machine
|
// « llama.cpp compilé » = compilé ici, pour cette machine
|
||||||
|
// « llama.cpp personnalisé » = un fork (modèles à quant spéciale)
|
||||||
// Le CHOIX du moteur utilisé se fait par modèle (preset), pas ici — voir la
|
// Le CHOIX du moteur utilisé se fait par modèle (preset), pas ici — voir la
|
||||||
// section « Moteur » dans l'éditeur de modèle. Ça évite que la barre latérale
|
// section « Moteur » dans l'éditeur de modèle. Ça évite que la barre latérale
|
||||||
// et les presets se battent pour la ligne BIN.
|
// et les presets se battent pour la ligne BIN.
|
||||||
@@ -2744,7 +2827,7 @@ async function loadLlamacpp(){
|
|||||||
|
|
||||||
const head = document.getElementById('lc-status');
|
const head = document.getElementById('lc-status');
|
||||||
if(!fastInstalled && !optInstalled){
|
if(!fastInstalled && !optInstalled){
|
||||||
head.innerHTML = 'Installez le moteur d\'IA. La <b>version rapide</b> convient à presque tout le monde.';
|
head.innerHTML = 'Installez le moteur d\'IA. Le <b>précompilé</b> convient à presque tout le monde.';
|
||||||
} else {
|
} else {
|
||||||
head.innerHTML = 'Installez les moteurs ici. Vous choisissez lequel utiliser en éditant un modèle (bouton <b>✎</b>).';
|
head.innerHTML = 'Installez les moteurs ici. Vous choisissez lequel utiliser en éditant un modèle (bouton <b>✎</b>).';
|
||||||
}
|
}
|
||||||
@@ -2784,12 +2867,12 @@ async function lcCheck(mode){
|
|||||||
const r = await jpost('/api/llamacpp/prebuilt/check', {});
|
const r = await jpost('/api/llamacpp/prebuilt/check', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
if(r.update) toast('nouvelle version disponible : '+r.latest+(r.current ? ' (installée : '+r.current+')' : ''));
|
if(r.update) toast('nouvelle version disponible : '+r.latest+(r.current ? ' (installée : '+r.current+')' : ''));
|
||||||
else toast('moteur rapide à jour ✓'+(r.latest ? ' ('+r.latest+')' : ''));
|
else toast('llama.cpp précompilé à jour ✓'+(r.latest ? ' ('+r.latest+')' : ''));
|
||||||
} else {
|
} else {
|
||||||
const r = await jpost('/api/llamacpp/check', {});
|
const r = await jpost('/api/llamacpp/check', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
if(r.behind > 0) toast(r.behind+' nouveau(x) commit(s) disponible(s) — utilisez « mettre à jour »');
|
if(r.behind > 0) toast(r.behind+' nouveau(x) commit(s) disponible(s) — utilisez « mettre à jour »');
|
||||||
else toast('moteur optimisé à jour ✓');
|
else toast('llama.cpp compilé à jour ✓');
|
||||||
}
|
}
|
||||||
}catch(_){ toast('erreur réseau'); }
|
}catch(_){ toast('erreur réseau'); }
|
||||||
}
|
}
|
||||||
@@ -2803,11 +2886,11 @@ async function lcPick(mode){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!await askConfirm('Installer la version rapide de llama.cpp : téléchargement prêt à l\'emploi (~2 min, aucune compilation).', {title:'Version rapide', okText:'Installer'})) return;
|
if(!await askConfirm('Télécharger le binaire officiel de llama.cpp, prêt à l\'emploi (~2 min, aucune compilation).', {title:'llama.cpp précompilé', okText:'Installer'})) return;
|
||||||
const r = await jpost('/api/llamacpp/prebuilt', {});
|
const r = await jpost('/api/llamacpp/prebuilt', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
} else {
|
} else {
|
||||||
if(!await askConfirm('Compiler la version optimisée pour votre machine. Ça peut prendre de longues minutes (surtout avec une carte NVIDIA).', {title:'Version optimisée', okText:'Compiler'})) return;
|
if(!await askConfirm('Compiler llama.cpp pour votre machine. Ça peut prendre de longues minutes (surtout avec une carte NVIDIA).', {title:'llama.cpp compilé', okText:'Compiler'})) return;
|
||||||
const r = await jpost('/api/llamacpp/install', {});
|
const r = await jpost('/api/llamacpp/install', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
}
|
}
|
||||||
@@ -2817,11 +2900,11 @@ async function lcPick(mode){
|
|||||||
// « Mettre à jour » sur une carte installée.
|
// « Mettre à jour » sur une carte installée.
|
||||||
async function lcUpdate(mode){
|
async function lcUpdate(mode){
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!await askConfirm('Vérifier et installer la dernière version rapide.', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
if(!await askConfirm('Vérifier et installer le dernier binaire précompilé.', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
||||||
const r = await jpost('/api/llamacpp/prebuilt', {});
|
const r = await jpost('/api/llamacpp/prebuilt', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
} else {
|
} else {
|
||||||
if(!await askConfirm('Vérifier et installer la dernière version optimisée (recompilation si besoin).', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
if(!await askConfirm('Vérifier et installer la dernière version compilée (recompilation si besoin).', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
||||||
const r = await jpost('/api/llamacpp/update', {clean:false});
|
const r = await jpost('/api/llamacpp/update', {clean:false});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
}
|
}
|
||||||
@@ -2959,9 +3042,9 @@ async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
|||||||
function renderMCP(r){
|
function renderMCP(r){
|
||||||
mcpServers = (r && r.servers) || [];
|
mcpServers = (r && r.servers) || [];
|
||||||
const enabled = mcpServers.filter(s=>s.enabled).length;
|
const enabled = mcpServers.filter(s=>s.enabled).length;
|
||||||
document.getElementById('mcp-badge').innerHTML = enabled
|
if(enabled) setBadge('mcp-badge', true, enabled+(enabled>1?' actifs':' actif'));
|
||||||
? '<span class="tag ok">'+enabled+'</span>'
|
else if(mcpServers.length) setBadge('mcp-badge', false, 'aucun actif');
|
||||||
: (mcpServers.length ? '<span class="tag" style="opacity:.6">0</span>' : '');
|
else setBadge('mcp-badge', null);
|
||||||
const list = document.getElementById('mcp-list');
|
const list = document.getElementById('mcp-list');
|
||||||
list.textContent = '';
|
list.textContent = '';
|
||||||
if(!mcpServers.length){
|
if(!mcpServers.length){
|
||||||
@@ -3151,7 +3234,7 @@ function renderRemote(d){
|
|||||||
const badge = document.getElementById('remote-badge');
|
const badge = document.getElementById('remote-badge');
|
||||||
if(!d || !d.linked){
|
if(!d || !d.linked){
|
||||||
off.style.display=''; on.style.display='none';
|
off.style.display=''; on.style.display='none';
|
||||||
if(badge){ badge.textContent=''; }
|
setBadge(badge, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
off.style.display='none'; on.style.display='';
|
off.style.display='none'; on.style.display='';
|
||||||
@@ -3161,7 +3244,7 @@ function renderRemote(d){
|
|||||||
else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
||||||
const sb = document.getElementById('remote-start');
|
const sb = document.getElementById('remote-start');
|
||||||
if(sb){ sb.style.display = d.active ? 'none' : ''; }
|
if(sb){ sb.style.display = d.active ? 'none' : ''; }
|
||||||
if(badge){ badge.textContent = d.active ? '● connecté' : '○ arrêté'; badge.style.color = d.active?'var(--accent)':'var(--warn)'; }
|
setBadge(badge, d.active ? true : 'warn', d.active ? 'connecté' : 'arrêté');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRemote(){
|
async function loadRemote(){
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
||||||
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
||||||
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
||||||
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||||
<style>
|
<style>
|
||||||
@@CSS@@
|
@@CSS@@
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
@@ -82,20 +82,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="net-block" class="gated-block">
|
<div id="net-block" class="gated-block">
|
||||||
<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> via un serveur <b>Crawl4AI</b>. Actifs seulement si le serveur répond.</span></span></div>
|
<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> via un serveur <b>Crawl4AI</b> que tu héberges toi-même (ex. <code>docker run -d -p 11235:11235 --shm-size=1g unclecode/crawl4ai:latest</code>). Actifs seulement si le <b>mode agent</b> est actif et que le serveur répond.</span></span></div>
|
||||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="internet-toggle" onchange="toggleInternet()"><span class="slider"></span></span> donner internet à l'IA</label>
|
||||||
|
<!-- URL et clé s'enregistrent en quittant le champ (onchange) : deux boutons
|
||||||
|
« enregistrer » côte à côte n'apportaient rien et brouillaient la carte. -->
|
||||||
<div class="row" style="margin-top:6px">
|
<div class="row" style="margin-top:6px">
|
||||||
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
<input id="crawl-url" type="text" placeholder="http://localhost:11235" class="sctl" onchange="saveCrawlUrl()">
|
||||||
<button onclick="saveCrawlUrl()">enregistrer</button>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
<!-- Clé d'accès du serveur Crawl4AI (facultative) : envoyée en
|
||||||
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
« Authorization: Bearer … ». Jamais relue par l'API, on n'affiche
|
||||||
qu'un indice sur les 4 derniers caractères. -->
|
qu'un indice sur les 4 derniers caractères. -->
|
||||||
<div class="row" style="margin-top:6px">
|
<div class="row" style="margin-top:6px">
|
||||||
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
<input id="crawl-key" type="password" autocomplete="off" placeholder="clé API (si le serveur en exige une)" class="sctl" onchange="saveCrawlKey()">
|
||||||
<button onclick="saveCrawlKey()">enregistrer</button>
|
|
||||||
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
<button id="crawl-key-clear" onclick="clearCrawlKey()" style="display:none">retirer</button>
|
||||||
</div>
|
</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 id="crawl-key-state" class="muted" style="font-size:11px;margin-top:3px"></div>
|
||||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,6 +122,7 @@
|
|||||||
<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-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-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-side" onchange="toggleView('hide-side')"><span class="slider"></span></span> barre latérale escamotable<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">La barre latérale devient un tiroir <b>même sur ordinateur</b> : elle reste fermée et s'ouvre avec le bouton ☰. Toute la largeur pour la conversation.</span></span></label>
|
<label class="switchrow"><span class="switch"><input type="checkbox" id="view-hide-side" onchange="toggleView('hide-side')"><span class="slider"></span></span> barre latérale escamotable<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">La barre latérale devient un tiroir <b>même sur ordinateur</b> : elle reste fermée et s'ouvre avec le bouton ☰. Toute la largeur pour la conversation.</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>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details><summary><span class="stitle">System prompt</span></summary>
|
<details><summary><span class="stitle">System prompt</span></summary>
|
||||||
@@ -193,18 +195,18 @@
|
|||||||
<div id="lc-status" class="lc-head muted">…</div>
|
<div id="lc-status" class="lc-head muted">…</div>
|
||||||
<div class="lc-modes">
|
<div class="lc-modes">
|
||||||
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
<div class="lc-mode" id="lc-mode-fast" onclick="lcPick('fast')">
|
||||||
<div class="lc-mode-t">Version rapide <span class="lc-reco">conseillé</span></div>
|
<div class="lc-mode-t">llama.cpp précompilé <span class="lc-reco">conseillé</span></div>
|
||||||
<div class="lc-mode-d">prête à l'emploi, installée en ~2 min</div>
|
<div class="lc-mode-d">binaire officiel téléchargé tel quel, prêt en ~2 min</div>
|
||||||
<div class="lc-mode-state" id="lc-fast-state"></div>
|
<div class="lc-mode-state" id="lc-fast-state"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
<div class="lc-mode" id="lc-mode-opt" onclick="lcPick('opt')">
|
||||||
<div class="lc-mode-t">Version optimisée</div>
|
<div class="lc-mode-t">llama.cpp compilé</div>
|
||||||
<div class="lc-mode-d">compilée pour votre machine, plus longue</div>
|
<div class="lc-mode-d">compilé ici pour votre machine — plus rapide, plus long à installer</div>
|
||||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||||
<div class="lc-mode-t">Backend personnalisé</div>
|
<div class="lc-mode-t">llama.cpp personnalisé</div>
|
||||||
<div class="lc-mode-d">forks llama.cpp pour modèles spéciaux (ternaire…)</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 class="lc-mode-state" id="lc-custom-state"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,14 +215,16 @@
|
|||||||
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
<div class="lc-logbar" onclick="lcToggleLog()"><span id="lc-log-caret">▸</span> détails</div>
|
||||||
<pre id="lc-log" hidden></pre>
|
<pre id="lc-log" hidden></pre>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||||
<details><summary><span class="stitle">Service</span></summary>
|
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 pad">
|
<div class="slist pad">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button onclick="act('start')">start</button>
|
<button onclick="act('start')">démarrer</button>
|
||||||
<button onclick="act('restart')">restart</button>
|
<button onclick="act('restart')">redémarrer</button>
|
||||||
<button onclick="act('stop')">stop</button>
|
<button onclick="act('stop')">arrêter</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
<details id="upd-details"><summary><span class="stitle">Actions</span></summary>
|
||||||
@@ -413,8 +417,8 @@
|
|||||||
<div class="pe-group">
|
<div class="pe-group">
|
||||||
<div class="pe-gh">Moteur</div>
|
<div class="pe-gh">Moteur</div>
|
||||||
<div class="pe-seg">
|
<div class="pe-seg">
|
||||||
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Rapide</span><span class="be-note" id="be-fast-note"></span></label>
|
<label><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Précompilé</span><span class="be-note" id="be-fast-note"></span></label>
|
||||||
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Optimisé</span><span class="be-note" id="be-opt-note"></span></label>
|
<label><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Compilé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const VIEW_OPTS=[
|
|||||||
{id:'hide-tools', label:"masquer les appels d'outils"},
|
{id:'hide-tools', label:"masquer les appels d'outils"},
|
||||||
{id:'fold-tools', label:'garder les bulles repliées'},
|
{id:'fold-tools', label:'garder les bulles repliées'},
|
||||||
{id:'hide-side', label:'barre latérale escamotable'},
|
{id:'hide-side', label:'barre latérale escamotable'},
|
||||||
|
{id:'hide-stats', label:'masquer la vitesse de génération'},
|
||||||
];
|
];
|
||||||
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
||||||
function applyView(id, on){
|
function applyView(id, on){
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
// Pastille d'état d'une section du menu (mode agent, internet, MCP, distant).
|
||||||
|
// `state` : true = actif (pilule pleine), false = inactif (contour), 'warn'/'err'
|
||||||
|
// = état signalé, null = pastille masquée. Un seul endroit décide de leur allure.
|
||||||
|
function setBadge(id, state, text){
|
||||||
|
const el = typeof id==='string' ? document.getElementById(id) : id;
|
||||||
|
if(!el) return;
|
||||||
|
el.className = 'sbadge' + (state===true ? ' on' : (state==='warn' || state==='err') ? ' '+state : '');
|
||||||
|
el.textContent = state===null || state===undefined ? '' : (text||'');
|
||||||
|
}
|
||||||
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800); }
|
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800); }
|
||||||
// Modales natives (askConfirm/askPrompt/askAlert) — remplacent confirm()/prompt()/
|
// Modales natives (askConfirm/askPrompt/askAlert) — remplacent confirm()/prompt()/
|
||||||
// alert() par de vraies boîtes stylées. Chacune renvoie une Promise : askConfirm →
|
// alert() par de vraies boîtes stylées. Chacune renvoie une Promise : askConfirm →
|
||||||
|
|||||||
@@ -117,17 +117,17 @@ async function loadRam(){
|
|||||||
}
|
}
|
||||||
async function loadCfg(){
|
async function loadCfg(){
|
||||||
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
||||||
// moteur rapide (prebuilt.in_use) ou optimisé (in_use) — sinon c'est un custom.
|
// précompilé (prebuilt.in_use) ou compilé ici (in_use) — sinon c'est un fork perso.
|
||||||
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
||||||
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
||||||
const rows=[];
|
const rows=[];
|
||||||
if(c.BIN){
|
if(c.BIN){
|
||||||
// Moteur : rapide / optimisé / custom (avec le chemin). Le title garde
|
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
|
||||||
// toujours le chemin complet, quel que soit le libellé.
|
// toujours le chemin complet, quel que soit le libellé.
|
||||||
let v;
|
let v;
|
||||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='rapide';
|
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
|
||||||
else if(lc && lc.in_use) v='optimisé';
|
else if(lc && lc.in_use) v='llama.cpp compilé';
|
||||||
else v='custom : '+c.BIN;
|
else v='llama.cpp personnalisé : '+c.BIN;
|
||||||
rows.push(row('MOTEUR', v, c.BIN));
|
rows.push(row('MOTEUR', v, c.BIN));
|
||||||
}
|
}
|
||||||
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ async function loadAgent(){
|
|||||||
const on = s.enabled;
|
const on = s.enabled;
|
||||||
document.getElementById('agent-toggle').checked = on;
|
document.getElementById('agent-toggle').checked = on;
|
||||||
document.getElementById('compact-toggle').checked = (s.compact !== false);
|
document.getElementById('compact-toggle').checked = (s.compact !== false);
|
||||||
document.getElementById('agent-badge').innerHTML = on
|
setBadge('agent-badge', on, on?'actif':'inactif');
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
|
||||||
document.getElementById('brand').classList.toggle('agent', on);
|
document.getElementById('brand').classList.toggle('agent', on);
|
||||||
setAgentGate(on);
|
setAgentGate(on);
|
||||||
if(s.mem_mode){ document.getElementById('mem-mode').value = s.mem_mode; renderMemModeDesc(s.mem_mode); }
|
if(s.mem_mode){ document.getElementById('mem-mode').value = s.mem_mode; renderMemModeDesc(s.mem_mode); }
|
||||||
@@ -139,8 +138,10 @@ function renderInternet(s){
|
|||||||
document.getElementById('internet-toggle').checked = internetOn;
|
document.getElementById('internet-toggle').checked = internetOn;
|
||||||
if(document.activeElement !== document.getElementById('crawl-url'))
|
if(document.activeElement !== document.getElementById('crawl-url'))
|
||||||
document.getElementById('crawl-url').value = s.url || '';
|
document.getElementById('crawl-url').value = s.url || '';
|
||||||
document.getElementById('internet-badge').innerHTML = internetOn
|
// Actif mais serveur injoignable : la pastille le dit (sinon l'UI ment — les
|
||||||
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
|
// outils web ne sont pas proposés au modèle dans ce cas).
|
||||||
|
if(internetOn && s.url && !s.reachable) setBadge('internet-badge','warn','injoignable');
|
||||||
|
else setBadge('internet-badge', internetOn, internetOn?'actif':'inactif');
|
||||||
// Clé du serveur Crawl4AI : le champ reste vide (la clé n'est jamais renvoyée),
|
// Clé du serveur Crawl4AI : le champ reste vide (la clé n'est jamais renvoyée),
|
||||||
// on indique seulement si une clé est enregistrée.
|
// on indique seulement si une clé est enregistrée.
|
||||||
const ks=document.getElementById('crawl-key-state'), kc=document.getElementById('crawl-key-clear');
|
const ks=document.getElementById('crawl-key-state'), kc=document.getElementById('crawl-key-clear');
|
||||||
@@ -231,6 +232,7 @@ async function toggleInternet(){
|
|||||||
async function saveCrawlUrl(){
|
async function saveCrawlUrl(){
|
||||||
const url=document.getElementById('crawl-url').value.trim();
|
const url=document.getElementById('crawl-url').value.trim();
|
||||||
renderInternet(await jpost('/api/internet',{url}));
|
renderInternet(await jpost('/api/internet',{url}));
|
||||||
|
toast(url ? 'serveur Crawl4AI enregistré' : 'serveur Crawl4AI retiré');
|
||||||
}
|
}
|
||||||
async function saveCrawlKey(){
|
async function saveCrawlKey(){
|
||||||
const el=document.getElementById('crawl-key'), key=el.value.trim();
|
const el=document.getElementById('crawl-key'), key=el.value.trim();
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ function onPickModel(){
|
|||||||
toast('MODEL='+val);
|
toast('MODEL='+val);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Choix du moteur PAR MODÈLE : 3 options (rapide / optimisé / personnalisé)
|
// Choix du moteur PAR MODÈLE : 3 options (précompilé / compilé / personnalisé)
|
||||||
// qui réécrivent la ligne BIN= du preset. C'est LE point où on décide quel
|
// qui réécrivent la ligne BIN= du preset. C'est LE point où on décide quel
|
||||||
// backend fait tourner ce modèle — la barre latérale ne fait qu'installer.
|
// backend fait tourner ce modèle — la barre latérale ne fait qu'installer.
|
||||||
function currentBinInTextarea(){
|
function currentBinInTextarea(){
|
||||||
@@ -196,11 +196,11 @@ function toggleBackendCustom(mode){
|
|||||||
function onBackendMode(mode){
|
function onBackendMode(mode){
|
||||||
toggleBackendCustom(mode);
|
toggleBackendCustom(mode);
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!beFastPath){ toast('installez d\'abord la version rapide (section MOTEUR)'); return; }
|
if(!beFastPath){ toast('installez d\'abord llama.cpp précompilé (section MOTEUR)'); return; }
|
||||||
setBinInTextarea(beFastPath); toast('moteur : rapide');
|
setBinInTextarea(beFastPath); toast('moteur : llama.cpp précompilé');
|
||||||
} else if(mode === 'opt'){
|
} else if(mode === 'opt'){
|
||||||
if(!beOptPath){ toast('installez d\'abord la version optimisée (section MOTEUR)'); return; }
|
if(!beOptPath){ toast('installez d\'abord llama.cpp compilé (section MOTEUR)'); return; }
|
||||||
setBinInTextarea(beOptPath); toast('moteur : optimisé');
|
setBinInTextarea(beOptPath); toast('moteur : llama.cpp compilé');
|
||||||
}
|
}
|
||||||
// custom : on attend que l'utilisateur saisisse un chemin / choisisse un backend
|
// custom : on attend que l'utilisateur saisisse un chemin / choisisse un backend
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,24 @@ function collapseInstant(el){
|
|||||||
requestAnimationFrame(()=>{ bw.style.transition=''; });
|
requestAnimationFrame(()=>{ bw.style.transition=''; });
|
||||||
}
|
}
|
||||||
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
|
function setLabel(el, text){ el.querySelector('.label').textContent = text; }
|
||||||
|
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
|
||||||
|
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
|
||||||
|
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
|
||||||
|
// par la préférence d'affichage « masquer la vitesse de génération » (CSS).
|
||||||
|
function setStats(el, text){
|
||||||
|
if(!el) return;
|
||||||
|
let s = el.querySelector(':scope > .statline');
|
||||||
|
if(!s){
|
||||||
|
s=document.createElement('div'); s.className='statline';
|
||||||
|
// Apparition en fondu, à la PREMIÈRE pose seulement : la ligne arrive une fois
|
||||||
|
// la réponse finie, un surgissement sec accrochait l'œil. Les mises à jour
|
||||||
|
// suivantes ne rejouent pas l'animation (elle clignoterait), et le rejeu du
|
||||||
|
// journal au chargement n'anime rien du tout.
|
||||||
|
if(!(typeof REPLAYING!=='undefined' && REPLAYING)) s.classList.add('statline-in');
|
||||||
|
el.appendChild(s);
|
||||||
|
}
|
||||||
|
s.textContent = text;
|
||||||
|
}
|
||||||
function bodyOf(el){ return el.querySelector('.body'); }
|
function bodyOf(el){ return el.querySelector('.body'); }
|
||||||
// Render markdown into a message body in place; safe because md() escapes HTML.
|
// Render markdown into a message body in place; safe because md() escapes HTML.
|
||||||
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); markNotices(b); addCopyButtons(b); scrollMaybe(); }
|
function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); markNotices(b); addCopyButtons(b); scrollMaybe(); }
|
||||||
|
|||||||
@@ -48,12 +48,16 @@ function showTyping(){ if(!simpleMode()) return; const c=document.getElementById
|
|||||||
// qu'au replay — pas de timer performance.now, qui n'a pas de sens hors-ligne).
|
// qu'au replay — pas de timer performance.now, qui n'a pas de sens hors-ligne).
|
||||||
function renderStats(el, s){
|
function renderStats(el, s){
|
||||||
if(!el||!s) return;
|
if(!el||!s) return;
|
||||||
const role=(el===T.contentEl)?'assistant':'reasoning';
|
const parts=[];
|
||||||
const parts=[role];
|
|
||||||
const pt=s.prompt_tokens||s.prompt_tokens_total;
|
const pt=s.prompt_tokens||s.prompt_tokens_total;
|
||||||
if(pt) parts.push('prefill '+pt+' tok · '+(s.prompt_per_second||0).toFixed(0)+' tok/s');
|
if(pt) parts.push('prefill '+pt+' tok · '+(s.prompt_per_second||0).toFixed(0)+' tok/s');
|
||||||
if(s.gen_tokens) parts.push('decode '+s.gen_tokens+' tok · '+(s.gen_per_second||0).toFixed(1)+' tok/s');
|
if(s.gen_tokens) parts.push('decode '+s.gen_tokens+' tok · '+(s.gen_per_second||0).toFixed(1)+' tok/s');
|
||||||
setLabel(el, parts.join(' · '));
|
if(!parts.length) return;
|
||||||
|
// Réponse de l'assistant : ligne de mesures dédiée sous le texte (son étiquette
|
||||||
|
// est masquée dans cette mise en page). Bulle repliable : l'étiquette EST le
|
||||||
|
// bouton de repli, on y écrit comme avant.
|
||||||
|
if(el.classList.contains('collapsible')) setLabel(el, ['reasoning'].concat(parts).join(' · '));
|
||||||
|
else setStats(el, parts.join(' · '));
|
||||||
}
|
}
|
||||||
// Label d'une bulle : nombre de tokens + vitesse. La vitesse est calculée à
|
// Label d'une bulle : nombre de tokens + vitesse. La vitesse est calculée à
|
||||||
// partir des HORODATAGES SERVEUR (firstTs→lastTs) : le temps réel de génération,
|
// partir des HORODATAGES SERVEUR (firstTs→lastTs) : le temps réel de génération,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
||||||
// « Version rapide » = binaires officiels précompilés (aucune compilation)
|
// « llama.cpp précompilé » = binaires officiels, aucune compilation
|
||||||
// « Version optimisée » = compilée pour la machine
|
// « llama.cpp compilé » = compilé ici, pour cette machine
|
||||||
|
// « llama.cpp personnalisé » = un fork (modèles à quant spéciale)
|
||||||
// Le CHOIX du moteur utilisé se fait par modèle (preset), pas ici — voir la
|
// Le CHOIX du moteur utilisé se fait par modèle (preset), pas ici — voir la
|
||||||
// section « Moteur » dans l'éditeur de modèle. Ça évite que la barre latérale
|
// section « Moteur » dans l'éditeur de modèle. Ça évite que la barre latérale
|
||||||
// et les presets se battent pour la ligne BIN.
|
// et les presets se battent pour la ligne BIN.
|
||||||
@@ -16,7 +17,7 @@ async function loadLlamacpp(){
|
|||||||
|
|
||||||
const head = document.getElementById('lc-status');
|
const head = document.getElementById('lc-status');
|
||||||
if(!fastInstalled && !optInstalled){
|
if(!fastInstalled && !optInstalled){
|
||||||
head.innerHTML = 'Installez le moteur d\'IA. La <b>version rapide</b> convient à presque tout le monde.';
|
head.innerHTML = 'Installez le moteur d\'IA. Le <b>précompilé</b> convient à presque tout le monde.';
|
||||||
} else {
|
} else {
|
||||||
head.innerHTML = 'Installez les moteurs ici. Vous choisissez lequel utiliser en éditant un modèle (bouton <b>✎</b>).';
|
head.innerHTML = 'Installez les moteurs ici. Vous choisissez lequel utiliser en éditant un modèle (bouton <b>✎</b>).';
|
||||||
}
|
}
|
||||||
@@ -56,12 +57,12 @@ async function lcCheck(mode){
|
|||||||
const r = await jpost('/api/llamacpp/prebuilt/check', {});
|
const r = await jpost('/api/llamacpp/prebuilt/check', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
if(r.update) toast('nouvelle version disponible : '+r.latest+(r.current ? ' (installée : '+r.current+')' : ''));
|
if(r.update) toast('nouvelle version disponible : '+r.latest+(r.current ? ' (installée : '+r.current+')' : ''));
|
||||||
else toast('moteur rapide à jour ✓'+(r.latest ? ' ('+r.latest+')' : ''));
|
else toast('llama.cpp précompilé à jour ✓'+(r.latest ? ' ('+r.latest+')' : ''));
|
||||||
} else {
|
} else {
|
||||||
const r = await jpost('/api/llamacpp/check', {});
|
const r = await jpost('/api/llamacpp/check', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
if(r.behind > 0) toast(r.behind+' nouveau(x) commit(s) disponible(s) — utilisez « mettre à jour »');
|
if(r.behind > 0) toast(r.behind+' nouveau(x) commit(s) disponible(s) — utilisez « mettre à jour »');
|
||||||
else toast('moteur optimisé à jour ✓');
|
else toast('llama.cpp compilé à jour ✓');
|
||||||
}
|
}
|
||||||
}catch(_){ toast('erreur réseau'); }
|
}catch(_){ toast('erreur réseau'); }
|
||||||
}
|
}
|
||||||
@@ -75,11 +76,11 @@ async function lcPick(mode){
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!await askConfirm('Installer la version rapide de llama.cpp : téléchargement prêt à l\'emploi (~2 min, aucune compilation).', {title:'Version rapide', okText:'Installer'})) return;
|
if(!await askConfirm('Télécharger le binaire officiel de llama.cpp, prêt à l\'emploi (~2 min, aucune compilation).', {title:'llama.cpp précompilé', okText:'Installer'})) return;
|
||||||
const r = await jpost('/api/llamacpp/prebuilt', {});
|
const r = await jpost('/api/llamacpp/prebuilt', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
} else {
|
} else {
|
||||||
if(!await askConfirm('Compiler la version optimisée pour votre machine. Ça peut prendre de longues minutes (surtout avec une carte NVIDIA).', {title:'Version optimisée', okText:'Compiler'})) return;
|
if(!await askConfirm('Compiler llama.cpp pour votre machine. Ça peut prendre de longues minutes (surtout avec une carte NVIDIA).', {title:'llama.cpp compilé', okText:'Compiler'})) return;
|
||||||
const r = await jpost('/api/llamacpp/install', {});
|
const r = await jpost('/api/llamacpp/install', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
}
|
}
|
||||||
@@ -89,11 +90,11 @@ async function lcPick(mode){
|
|||||||
// « Mettre à jour » sur une carte installée.
|
// « Mettre à jour » sur une carte installée.
|
||||||
async function lcUpdate(mode){
|
async function lcUpdate(mode){
|
||||||
if(mode === 'fast'){
|
if(mode === 'fast'){
|
||||||
if(!await askConfirm('Vérifier et installer la dernière version rapide.', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
if(!await askConfirm('Vérifier et installer le dernier binaire précompilé.', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
||||||
const r = await jpost('/api/llamacpp/prebuilt', {});
|
const r = await jpost('/api/llamacpp/prebuilt', {});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
} else {
|
} else {
|
||||||
if(!await askConfirm('Vérifier et installer la dernière version optimisée (recompilation si besoin).', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
if(!await askConfirm('Vérifier et installer la dernière version compilée (recompilation si besoin).', {title:'Mettre à jour', okText:'Mettre à jour'})) return;
|
||||||
const r = await jpost('/api/llamacpp/update', {clean:false});
|
const r = await jpost('/api/llamacpp/update', {clean:false});
|
||||||
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
if(!r.ok){ toast('erreur : '+(r.error||'')); return; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ async function loadMCP(){ renderMCP(await jget('/api/mcp')); }
|
|||||||
function renderMCP(r){
|
function renderMCP(r){
|
||||||
mcpServers = (r && r.servers) || [];
|
mcpServers = (r && r.servers) || [];
|
||||||
const enabled = mcpServers.filter(s=>s.enabled).length;
|
const enabled = mcpServers.filter(s=>s.enabled).length;
|
||||||
document.getElementById('mcp-badge').innerHTML = enabled
|
if(enabled) setBadge('mcp-badge', true, enabled+(enabled>1?' actifs':' actif'));
|
||||||
? '<span class="tag ok">'+enabled+'</span>'
|
else if(mcpServers.length) setBadge('mcp-badge', false, 'aucun actif');
|
||||||
: (mcpServers.length ? '<span class="tag" style="opacity:.6">0</span>' : '');
|
else setBadge('mcp-badge', null);
|
||||||
const list = document.getElementById('mcp-list');
|
const list = document.getElementById('mcp-list');
|
||||||
list.textContent = '';
|
list.textContent = '';
|
||||||
if(!mcpServers.length){
|
if(!mcpServers.length){
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function renderRemote(d){
|
|||||||
const badge = document.getElementById('remote-badge');
|
const badge = document.getElementById('remote-badge');
|
||||||
if(!d || !d.linked){
|
if(!d || !d.linked){
|
||||||
off.style.display=''; on.style.display='none';
|
off.style.display=''; on.style.display='none';
|
||||||
if(badge){ badge.textContent=''; }
|
setBadge(badge, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
off.style.display='none'; on.style.display='';
|
off.style.display='none'; on.style.display='';
|
||||||
@@ -20,7 +20,7 @@ function renderRemote(d){
|
|||||||
else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
else { st.textContent='○ tunnel arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
|
||||||
const sb = document.getElementById('remote-start');
|
const sb = document.getElementById('remote-start');
|
||||||
if(sb){ sb.style.display = d.active ? 'none' : ''; }
|
if(sb){ sb.style.display = d.active ? 'none' : ''; }
|
||||||
if(badge){ badge.textContent = d.active ? '● connecté' : '○ arrêté'; badge.style.color = d.active?'var(--accent)':'var(--warn)'; }
|
setBadge(badge, d.active ? true : 'warn', d.active ? 'connecté' : 'arrêté');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRemote(){
|
async function loadRemote(){
|
||||||
|
|||||||
@@ -147,7 +147,10 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
|||||||
les confondre avec les traits des sections principales. */
|
les confondre avec les traits des sections principales. */
|
||||||
/* Bloc du mode agent = carte de liste : le rembourrage est porté par les
|
/* Bloc du mode agent = carte de liste : le rembourrage est porté par les
|
||||||
lignes (comme .slist), pas par la carte, sinon les retraits se cumulent. */
|
lignes (comme .slist), pas par la carte, sinon les retraits se cumulent. */
|
||||||
.gated-block{margin:6px 0;border:1px solid var(--border);border-radius:10px;padding:0 0 10px;background:var(--panel);overflow:hidden;transition:opacity .15s}
|
/* ⚠️ PAS d'overflow:hidden ici : les bulles d'aide (« ? ») sont positionnées en
|
||||||
|
absolu par rapport à la ligne, donc un débordement caché les TRONQUE — c'est
|
||||||
|
ce qui rendait plusieurs infobulles illisibles. */
|
||||||
|
.gated-block{margin:6px 0;border:1px solid var(--border);border-radius:10px;padding:0 0 10px;background:var(--panel);overflow:visible;transition:opacity .15s}
|
||||||
.gated-block>.switchrow:first-of-type{padding-top:0}
|
.gated-block>.switchrow:first-of-type{padding-top:0}
|
||||||
.gated-block.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
.gated-block.gated{opacity:.4;pointer-events:none;filter:grayscale(.4)}
|
||||||
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
|
||||||
@@ -303,7 +306,24 @@ details>summary{display:flex;align-items:center;gap:8px;height:34px;font-size:13
|
|||||||
couleur distinguent les deux niveaux. */
|
couleur distinguent les deux niveaux. */
|
||||||
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
.stitle,.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{
|
||||||
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
|
font-family:var(--mono);text-transform:uppercase;letter-spacing:.08em}
|
||||||
.sbadge{flex:none;font-size:10px}
|
/* Pastille d'état d'une section (mode agent, accès internet, MCP, accès distant).
|
||||||
|
Un point + un mot, dans une pilule — l'ancien couple « on / off » en étiquette
|
||||||
|
carrée se lisait mal et jurait avec le reste du menu. Actif = pilule pleine
|
||||||
|
(inversée), inactif = contour discret. */
|
||||||
|
.sbadge{flex:none;display:inline-flex;align-items:center;gap:5px;
|
||||||
|
font-family:var(--font);font-size:9.5px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;
|
||||||
|
padding:2px 8px;border-radius:999px;border:1px solid var(--border);color:var(--dim);background:transparent;white-space:nowrap}
|
||||||
|
.sbadge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.55}
|
||||||
|
.sbadge.on{background:var(--text);border-color:var(--text);color:var(--bg)}
|
||||||
|
.sbadge.on::before{opacity:1}
|
||||||
|
.sbadge.warn{color:var(--warn);border-color:color-mix(in srgb,var(--warn) 45%,transparent)}
|
||||||
|
.sbadge.err{color:var(--err);border-color:color-mix(in srgb,var(--err) 45%,transparent)}
|
||||||
|
.sbadge:empty{display:none}
|
||||||
|
/* Dans une étiquette de groupe, la pastille se plaque à DROITE (comme dans les
|
||||||
|
titres de section, où .stitle occupe la place) au lieu de coller au libellé.
|
||||||
|
L'ordre visuel reste : libellé · « ? » · … · pastille. */
|
||||||
|
.subhead .sbadge{margin-left:auto;order:2}
|
||||||
|
.subhead .help{order:1}
|
||||||
details>summary::-webkit-details-marker{display:none}
|
details>summary::-webkit-details-marker{display:none}
|
||||||
details>summary:hover{color:var(--dim)}
|
details>summary:hover{color:var(--dim)}
|
||||||
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
|
/* Chevron : un carré dont on n'affiche que deux côtés. Il pivote AUTOUR DE SON
|
||||||
@@ -317,11 +337,20 @@ details[open]>summary::after{transform:rotate(45deg)}
|
|||||||
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
|
.subhead,.io-cap,.pages-bar .pages-title,.skillshead>span{position:relative;display:flex;align-items:center;
|
||||||
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
|
font-size:11px;font-weight:600;color:var(--dim);margin:10px 2px 5px}
|
||||||
.io-cap{margin:0 2px 5px}
|
.io-cap{margin:0 2px 5px}
|
||||||
|
/* Premier élément d'une carte : sa marge haute s'ajoutait au rembourrage de la
|
||||||
|
carte (18 px de vide au-dessus de « Mémoire », « Accès internet », etc.). */
|
||||||
|
.gated-block>:first-child{margin-top:0}
|
||||||
|
/* Une étiquette de groupe qui suit immédiatement le titre de section n'a pas
|
||||||
|
besoin de reprendre de l'air : le titre en a déjà donné. */
|
||||||
|
details>summary+.subhead{margin-top:4px}
|
||||||
.io-group{margin-top:8px}
|
.io-group{margin-top:8px}
|
||||||
|
|
||||||
/* --- Carte de liste --- */
|
/* --- Carte de liste --- */
|
||||||
|
/* overflow VISIBLE : cf. la note sur .gated-block — les infobulles doivent
|
||||||
|
pouvoir déborder de la carte. Les lignes n'ont pas de fond propre, l'arrondi
|
||||||
|
n'a donc rien à rogner. */
|
||||||
.slist,.gated-block{background:var(--panel);border:1px solid var(--border);border-radius:9px;
|
.slist,.gated-block{background:var(--panel);border:1px solid var(--border);border-radius:9px;
|
||||||
overflow:hidden;margin:0 0 8px}
|
overflow:visible;margin:0 0 8px}
|
||||||
.slist:last-child,.gated-block:last-child{margin-bottom:0}
|
.slist:last-child,.gated-block:last-child{margin-bottom:0}
|
||||||
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
|
/* Contenu libre (champ + boutons) : le rembourrage passe sur la carte. */
|
||||||
.slist.pad,.gated-block{padding:8px 10px}
|
.slist.pad,.gated-block{padding:8px 10px}
|
||||||
@@ -464,7 +493,9 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
|||||||
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
|
.lc-mode{border:1px solid var(--border);border-radius:8px;padding:10px 12px;cursor:pointer;transition:border-color .12s,background .12s}
|
||||||
.lc-mode:hover{border-color:var(--dim)}
|
.lc-mode:hover{border-color:var(--dim)}
|
||||||
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
|
.lc-mode-t{font-size:13px;font-weight:600;color:var(--text);display:flex;align-items:center;gap:6px}
|
||||||
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border:1px solid var(--accent);border-radius:8px;padding:1px 6px}
|
/* « conseillé » : pilule PLEINE (texte inversé). En contour, elle héritait du
|
||||||
|
gris atténué du thème monochrome et devenait illisible en mode sombre. */
|
||||||
|
.lc-reco{font-size:9px;font-weight:600;text-transform:uppercase;letter-spacing:.05em;background:var(--text);color:var(--bg);border:1px solid var(--text);border-radius:8px;padding:1px 6px}
|
||||||
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
|
.lc-mode-d{font-size:11px;color:var(--dim);margin-top:3px}
|
||||||
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
.lc-mode-state{font-size:11px;margin-top:7px;display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
.lc-mode-go{color:var(--accent)}
|
.lc-mode-go{color:var(--accent)}
|
||||||
@@ -668,6 +699,19 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
|||||||
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
||||||
#chat .msg .label{display:none}
|
#chat .msg .label{display:none}
|
||||||
#chat .msg.collapsible .label{display:block}
|
#chat .msg.collapsible .label{display:block}
|
||||||
|
/* Mesures de génération sous la réponse (prefill / decode). Assez discrètes pour
|
||||||
|
ne pas concurrencer le texte, assez lisibles pour être utiles. Escamotables
|
||||||
|
depuis Apparence → Affichage. */
|
||||||
|
#chat .msg .statline{margin-top:10px;font-family:var(--mono);font-size:10.5px;letter-spacing:.02em;
|
||||||
|
color:var(--dim);opacity:.75}
|
||||||
|
#chat .msg .statline:empty{display:none}
|
||||||
|
html[data-hide-stats="1"] #chat .msg .statline{display:none}
|
||||||
|
/* Arrivée en fondu montant plutôt qu'un surgissement sec. L'opacité finale (.75)
|
||||||
|
est celle de la règle ci-dessus : l'animation doit y ATTERRIR, sinon la ligne
|
||||||
|
clignote en repassant à sa valeur de repos. */
|
||||||
|
#chat .msg .statline-in{animation:statin .28s cubic-bezier(.2,.7,.3,1) both}
|
||||||
|
@keyframes statin{from{opacity:0;transform:translateY(3px)}to{opacity:.75;transform:none}}
|
||||||
|
@media (prefers-reduced-motion:reduce){#chat .msg .statline-in{animation:none}}
|
||||||
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
|
/* Raisonnement / outils : encart gris discret plutôt qu'un cadre pointillé. */
|
||||||
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
|
#chat .msg.reasoning,#chat .msg.tool{background:var(--panel);border:1px solid var(--border);border-radius:14px;color:var(--dim);padding:12px 16px}
|
||||||
#chat .msg.reasoning{color:var(--dim)}
|
#chat .msg.reasoning{color:var(--dim)}
|
||||||
@@ -731,7 +775,7 @@ button,.addbtn,.mem-search{border-radius:10px}
|
|||||||
.preset{background:var(--panel);border-color:var(--border)}
|
.preset{background:var(--panel);border-color:var(--border)}
|
||||||
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
|
.preset:hover{background:var(--accent-bg);border-color:var(--dim)}
|
||||||
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
|
.preset.active{background:var(--panel);border-color:var(--text);color:var(--text)}
|
||||||
.subhead,.lc-reco,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
|
.subhead,.lc-mode-go,.lc-mode-active-tag{color:var(--dim)}
|
||||||
button:hover{border-color:var(--dim);color:var(--text)}
|
button:hover{border-color:var(--dim);color:var(--text)}
|
||||||
|
|
||||||
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
|
/* Encart de garde-fou : c'est le serveur qui parle (limite d'appels d'outils,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ var webPrefsAllowed = map[string]map[string]bool{
|
|||||||
"hide_tools": {"0": true, "1": true},
|
"hide_tools": {"0": true, "1": true},
|
||||||
"fold_tools": {"0": true, "1": true},
|
"fold_tools": {"0": true, "1": true},
|
||||||
"hide_side": {"0": true, "1": true},
|
"hide_side": {"0": true, "1": true},
|
||||||
|
"hide_stats": {"0": true, "1": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadWebPrefs lit les préférences enregistrées (map vide si absent/illisible).
|
// loadWebPrefs lit les préférences enregistrées (map vide si absent/illisible).
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ func newWebMux() *http.ServeMux {
|
|||||||
// Charge l'état de conversation persisté (une fois par process : jean web ET
|
// Charge l'état de conversation persisté (une fois par process : jean web ET
|
||||||
// jean link serve appellent newWebMux).
|
// jean link serve appellent newWebMux).
|
||||||
convLoadOnce.Do(LoadConversation)
|
convLoadOnce.Do(LoadConversation)
|
||||||
|
// Pré-chauffe les serveurs MCP en tâche de fond : sinon le handshake (plusieurs
|
||||||
|
// secondes pour un serveur lancé via npx) est payé par le premier message.
|
||||||
|
MCPPrewarm()
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
// Pages publiques : le HTML et le JS ne contiennent aucun secret. Toute la
|
// Pages publiques : le HTML et le JS ne contiennent aucun secret. Toute la
|
||||||
// donnée et toutes les actions passent par /api/* qui, lui, exige la clé.
|
// donnée et toutes les actions passent par /api/* qui, lui, exige la clé.
|
||||||
|
|||||||
Reference in new issue
Block a user