mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 18:58:43 +02:00
v0.5.10 : le produit s'appelle AJEAN + diff des ecritures dans le chat + variante AJEAN sombre
- renommage : « Jean » devient « AJEAN » dans tous les textes visibles (titre de l'UI, proprietes du binaire Windows, barre de menus macOS / zone de notification, messages du terminal, README). La commande, les services, les chemins et le depot ne changent pas. - nouveau thème « AJEAN sombre » (même habillage pixel, palette nuit) ; les regles de mise en page sont partagees via [data-theme^="ajean"]. - l'IA montre ce qu'elle ecrit : les outils edit / mem_add / mem_edit renvoient un diff ligne a ligne, affiche en vert (+) et rouge (-) dans la bulle d'outil et persiste avec la conversation (chat_diff.go). - ecritures idempotentes : si la modification est deja en place, edit et mem_edit repondent « deja a jour » au lieu d'une erreur trompeuse ; un appel d'outil rigoureusement identique n'est plus rejoue dans le meme tour. - saisie du chat auto-agrandissante (une ligne au repos en thème ChatGPT, deux ailleurs), bouton d'envoi enfin aligne, astuce clavier detachee de la carte. - editeur de preset relu pour le thème AJEAN (plus de double encadrement), champ + bouton de telechargement recolles dans tous les thèmes, interrupteurs carres. - emojis retires de l'interface (les glyphes monochromes restent).
This commit is contained in:
1 parent
6195d77759
commit
e2475c6688
30 files changed
+641
-209
No files matched your search
@@ -1,10 +1,10 @@
|
||||
# Jean
|
||||
# AJEAN
|
||||
|
||||

|
||||

|
||||
|
||||
**Un gestionnaire mono-binaire pour serveurs [llama.cpp](https://github.com/ggml-org/llama.cpp) auto-hébergés — avec une interface web intégrée, un chat terminal, et un compilateur de backend qui détecte automatiquement le matériel.**
|
||||
|
||||
Dépose un seul binaire sur une machine, lance `jean llamacpp install`, et Jean clone, configure et compile llama.cpp pour le matériel de *cette* machine (CUDA / ROCm / Metal / Vulkan / CPU) — aucun flag à retenir. Ensuite `jean start` et tu as un endpoint compatible OpenAI avec un chat web par-dessus.
|
||||
Dépose un seul binaire sur une machine, lance `jean llamacpp install`, et AJEAN clone, configure et compile llama.cpp pour le matériel de *cette* machine (CUDA / ROCm / Metal / Vulkan / CPU) — aucun flag à retenir. Ensuite `jean start` et tu as un endpoint compatible OpenAI avec un chat web par-dessus.
|
||||
|
||||
```
|
||||
télécharge le binaire → jean llamacpp install → jean edit → jean start → c'est parti
|
||||
@@ -14,7 +14,7 @@ télécharge le binaire → jean llamacpp install → jean edit → jean s
|
||||
|
||||
## Pourquoi
|
||||
|
||||
Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouver les bons flags CMake pour ton GPU, écrire une unité systemd, gérer une clé API, changer de modèle, garder le build à jour… Jean transforme tout ça en quelques sous-commandes derrière un binaire statique unique, **sans dépendance à l'exécution** (à part llama.cpp lui-même, que Jean peut compiler pour toi).
|
||||
Faire tourner llama.cpp comme un vrai service, ça veut dire d'habitude : trouver les bons flags CMake pour ton GPU, écrire une unité systemd, gérer une clé API, changer de modèle, garder le build à jour… AJEAN transforme tout ça en quelques sous-commandes derrière un binaire statique unique, **sans dépendance à l'exécution** (à part llama.cpp lui-même, que AJEAN peut compiler pour toi).
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
@@ -59,7 +59,7 @@ sudo jean install
|
||||
jean llamacpp install
|
||||
```
|
||||
|
||||
Jean détecte ton accélérateur, compile `llama-server`, et pointe la config sur le nouveau binaire. Nécessite `git` et `cmake` (plus le toolkit correspondant, ex. CUDA, si tu veux l'accélération GPU).
|
||||
AJEAN détecte ton accélérateur, compile `llama-server`, et pointe la config sur le nouveau binaire. Nécessite `git` et `cmake` (plus le toolkit correspondant, ex. CUDA, si tu veux l'accélération GPU).
|
||||
|
||||
### 4. Pointe-le sur un modèle et démarre
|
||||
|
||||
@@ -77,7 +77,7 @@ jean web # http://<hôte>:8090
|
||||
|
||||
## Windows
|
||||
|
||||
Jean fonctionne aussi sur Windows. Les différences avec Linux :
|
||||
AJEAN fonctionne aussi sur Windows. Les différences avec Linux :
|
||||
|
||||
- **Pas de systemd.** `jean start` lance `jean serve` en arrière-plan (processus détaché), suivi via un fichier PID. `stop` / `restart` / `status` / `logs` agissent dessus — aucun droit administrateur requis. `enable` / `disable` (démarrage au boot) ne sont pas gérés : utilise une tâche planifiée ou `sc.exe` si tu en as besoin.
|
||||
- **`JEAN_HOME`** vaut par défaut `%ProgramData%\jean` (repli `%LOCALAPPDATA%\jean`). Surcharge avec la variable d'environnement `JEAN_HOME`.
|
||||
@@ -98,7 +98,7 @@ jean logs # suit %ProgramData%\jean\jean.log
|
||||
|
||||
## macOS
|
||||
|
||||
Comme sous Windows, tu peux lancer Jean **d'un simple clic** : la page [Releases](../../releases) publie `Jean-macos-arm64.zip` / `Jean-macos-amd64.zip`, un bundle **`Jean.app`**. Dézippe, glisse-le dans *Applications*, ouvre-le : Jean démarre son UI web sur `http://localhost:8090`, l'ouvre dans le navigateur et pose son **icône dans la barre de menus** (en haut à droite) — pendant exact du system tray Windows, avec « Ouvrir Jean » / « Quitter ». Pas de fenêtre de Terminal, pas d'icône dans le Dock.
|
||||
Comme sous Windows, tu peux lancer AJEAN **d'un simple clic** : la page [Releases](../../releases) publie `Jean-macos-arm64.zip` / `Jean-macos-amd64.zip`, un bundle **`Jean.app`**. Dézippe, glisse-le dans *Applications*, ouvre-le : AJEAN démarre son UI web sur `http://localhost:8090`, l'ouvre dans le navigateur et pose son **icône dans la barre de menus** (en haut à droite) — pendant exact du system tray Windows, avec « Ouvrir AJEAN » / « Quitter ». Pas de fenêtre de Terminal, pas d'icône dans le Dock.
|
||||
|
||||
L'app n'est **pas signée** par un certificat Apple (seulement ad-hoc) : au premier lancement, fais **clic droit → Ouvrir** (ou *Réglages Système → Confidentialité et sécurité → Ouvrir quand même*).
|
||||
|
||||
@@ -188,7 +188,7 @@ La clé API (quand elle est définie avec `jean set-api-key`) est stockée dans
|
||||
|
||||
### API de pilotage à distance
|
||||
|
||||
`jean web` expose une API HTTP pour piloter Jean à distance : status, VRAM, liste/sélection de presets (switch de modèle), démarrage/arrêt/redémarrage du service, chat. Pour l'exposer sur internet en sécurité, protège-la par une clé :
|
||||
`jean web` expose une API HTTP pour piloter AJEAN à distance : status, VRAM, liste/sélection de presets (switch de modèle), démarrage/arrêt/redémarrage du service, chat. Pour l'exposer sur internet en sécurité, protège-la par une clé :
|
||||
|
||||
```
|
||||
jean set-web-key # génère une clé aléatoire
|
||||
@@ -215,7 +215,7 @@ Endpoints utiles pour un client :
|
||||
|
||||
La clé est stockée dans `$JEAN_HOME/.web_key`, distincte de `.api_key` (pilotage ≠ accès complétions), et relue à chaud à chaque requête. Le pilotage du service est cross-platform (systemd sous Linux, supervision par PID-file sous Windows).
|
||||
|
||||
> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, place Jean derrière un reverse-proxy HTTPS (Caddy, nginx) ou un tunnel (Tailscale, Cloudflare Tunnel).
|
||||
> ⚠️ La clé voyage en clair en HTTP. Pour une exposition publique, place AJEAN derrière un reverse-proxy HTTPS (Caddy, nginx) ou un tunnel (Tailscale, Cloudflare Tunnel).
|
||||
|
||||
### Accès internet de l'IA (Crawl4AI)
|
||||
|
||||
@@ -256,13 +256,13 @@ Une fois lié, tu retrouves depuis le portail :
|
||||
- la gestion de **plusieurs serveurs** et d'**agents** depuis un tableau de bord ;
|
||||
- en option, un **endpoint compatible OpenAI** (voir ci-dessous).
|
||||
|
||||
C'est un service optionnel et payant ; tout le reste de Jean est et restera open source et gratuit.
|
||||
C'est un service optionnel et payant ; tout le reste d'AJEAN est et restera open source et gratuit.
|
||||
|
||||
### Sécurité — boîte noire
|
||||
|
||||
`jean link` est conçu pour que le relais ajean.link soit un **pur tube aveugle** : il transporte tes données mais ne peut pas les lire.
|
||||
|
||||
- **Chat chiffré de bout en bout.** Le chat entre ton navigateur et ton serveur Jean est chiffré (X25519 + AES-GCM) : le relais ne voit ni tes prompts ni les réponses, seulement de l'opaque. La clé est dérivée de ton mot de passe (protocole **OPAQUE**) et ne quitte jamais ton navigateur.
|
||||
- **Chat chiffré de bout en bout.** Le chat entre ton navigateur et ton serveur AJEAN est chiffré (X25519 + AES-GCM) : le relais ne voit ni tes prompts ni les réponses, seulement de l'opaque. La clé est dérivée de ton mot de passe (protocole **OPAQUE**) et ne quitte jamais ton navigateur.
|
||||
- **Empreinte vérifiée.** `jean link` affiche une empreinte de la clé de la machine, à confirmer une fois dans le portail — ça défait toute tentative d'interception par le relais.
|
||||
- **Appairage authentifié.** En plus de l'empreinte, tu autorises ton navigateur avec un **code d'appairage** (`jean link code`) : à usage unique et valable 10 min, il garantit que seul *ton* navigateur peut piloter le serveur — même un relais compromis ne peut pas forger de commande.
|
||||
- **Aucun chat en clair par le relais.** L'ancien chemin en clair est refusé à travers le tunnel ; seul le chemin chiffré transporte du contenu.
|
||||
@@ -272,7 +272,7 @@ C'est un service optionnel et payant ; tout le reste de Jean est et restera open
|
||||
|
||||
#### Endpoint OpenAI (`<machine>.oai.ajean.link`) — opt-in
|
||||
|
||||
Pour brancher des outils tiers (OpenCode, etc.), Jean peut exposer un endpoint compatible OpenAI **par machine** : `https://<machine>.oai.ajean.link/v1`. L'authentification est la clé API de ton `llama-server` (`jean set-api-key`), présentée en `Authorization: Bearer <clé>`. C'est **désactivé par défaut**.
|
||||
Pour brancher des outils tiers (OpenCode, etc.), AJEAN peut exposer un endpoint compatible OpenAI **par machine** : `https://<machine>.oai.ajean.link/v1`. L'authentification est la clé API de ton `llama-server` (`jean set-api-key`), présentée en `Authorization: Bearer <clé>`. C'est **désactivé par défaut**.
|
||||
|
||||
Tu l'actives par machine depuis l'**interface web** (panneau « Accès OpenAI ») : le drapeau est relu **en direct** par le tunnel, sans redémarrage. *(Ancien mécanisme : la variable d'environnement `JEAN_LINK_ALLOW_OAI=1` sur le service — encore acceptée pour rétro-compatibilité, mais le toggle de l'UI est désormais la voie recommandée.)*
|
||||
|
||||
@@ -288,7 +288,7 @@ Contrairement aux premières versions, ce flux **n'est pas en clair chez le rela
|
||||
|
||||
## Compiler depuis les sources
|
||||
|
||||
Nécessite Go 1.25+. Jean est un binaire 100 % Go (l'UI web est embarquée via `go:embed`) :
|
||||
Nécessite Go 1.25+. AJEAN est un binaire 100 % Go (l'UI web est embarquée via `go:embed`) :
|
||||
|
||||
```bash
|
||||
git clone https://github.com/nathaninline/jean.git
|
||||
@@ -299,7 +299,7 @@ CGO_ENABLED=0 go build -o jean ./cmd/jean
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o jean-linux-amd64 ./cmd/jean
|
||||
```
|
||||
|
||||
> Compiler **Jean** ne nécessite que Go. Compiler le **backend llama.cpp** (`jean llamacpp install`) nécessite `git`, `cmake`, et le toolkit de ton accélérateur (CUDA, ROCm, etc.).
|
||||
> Compiler **AJEAN** ne nécessite que Go. Compiler le **backend llama.cpp** (`jean llamacpp install`) nécessite `git`, `cmake`, et le toolkit de ton accélérateur (CUDA, ROCm, etc.).
|
||||
|
||||
## Comment ça marche
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 9,
|
||||
"Patch": 10,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 5,
|
||||
"Patch": 9,
|
||||
"Patch": 10,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -19,13 +19,13 @@
|
||||
"FileSubType": "00"
|
||||
},
|
||||
"StringFileInfo": {
|
||||
"CompanyName": "Jean contributors",
|
||||
"FileDescription": "Jean — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés",
|
||||
"CompanyName": "AJEAN contributors",
|
||||
"FileDescription": "AJEAN — gestionnaire mono-binaire de serveurs llama.cpp auto-hébergés",
|
||||
"InternalName": "jean",
|
||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||
"LegalCopyright": "Copyright (c) 2026 AJEAN contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "Jean",
|
||||
"ProductVersion": "0.5.9",
|
||||
"ProductName": "AJEAN",
|
||||
"ProductVersion": "0.5.10",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -324,10 +324,16 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
|
||||
case ev.Err != nil:
|
||||
c.appendDelta(epoch, map[string]any{"error": ev.Err.Error()})
|
||||
case ev.ToolUsed != nil:
|
||||
c.appendDelta(epoch, map[string]any{"tool_used": map[string]any{
|
||||
tu := map[string]any{
|
||||
"name": ev.ToolUsed.Name, "label": ev.ToolUsed.Label,
|
||||
"result": ev.ToolUsed.Result, "done": ev.ToolUsed.Done, "typing": ev.ToolUsed.Typing,
|
||||
}})
|
||||
}
|
||||
// Lignes +/- d'une écriture (edit / mémoire) : persistées avec le reste
|
||||
// pour que le diff soit encore là après un rafraîchissement.
|
||||
if len(ev.ToolUsed.Diff) > 0 {
|
||||
tu["diff"] = ev.ToolUsed.Diff
|
||||
}
|
||||
c.appendDelta(epoch, map[string]any{"tool_used": tu})
|
||||
case ev.Stats != nil:
|
||||
// Taille réelle du contexte (usage.prompt_tokens + généré) pour le compteur
|
||||
// et la décision de compactage au tour suivant.
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package jean
|
||||
|
||||
// chat_diff.go — petit diff ligne à ligne pour l'interface : quand l'IA modifie
|
||||
// un fichier (outil `edit`) ou une page de mémoire (`mem_edit`, `mem_add`), on
|
||||
// envoie à l'UI le détail des lignes ajoutées et retirées, qu'elle affiche en
|
||||
// vert (+) et rouge (-). Aucune dépendance : LCS classique sur les lignes, avec
|
||||
// des garde-fous pour ne jamais transformer un gros remplacement en pavé.
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
diffMaxLines = 400 // au-delà, on compare sans détail (trop gros / trop lent)
|
||||
diffMaxShown = 120 // lignes envoyées à l'UI (le reste est résumé)
|
||||
)
|
||||
|
||||
// DiffLine est une ligne de diff : Op vaut " " (contexte), "-" ou "+".
|
||||
type DiffLine struct {
|
||||
Op string `json:"op"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// lineDiff compare deux blocs de texte ligne à ligne. Le résultat garde les
|
||||
// lignes communes comme contexte : c'est ce qui rend le changement lisible
|
||||
// quand l'IA ne touche qu'un mot au milieu d'un paragraphe.
|
||||
func lineDiff(oldText, newText string) []DiffLine {
|
||||
a := splitLines(oldText)
|
||||
b := splitLines(newText)
|
||||
// Blocs énormes : on ne calcule pas la LCS (coût quadratique), on montre
|
||||
// simplement l'ancien en retrait et le nouveau en ajout.
|
||||
if len(a) > diffMaxLines || len(b) > diffMaxLines {
|
||||
out := make([]DiffLine, 0, len(a)+len(b))
|
||||
for _, l := range a {
|
||||
out = append(out, DiffLine{Op: "-", Text: l})
|
||||
}
|
||||
for _, l := range b {
|
||||
out = append(out, DiffLine{Op: "+", Text: l})
|
||||
}
|
||||
return capLines(out)
|
||||
}
|
||||
|
||||
// LCS : table des longueurs, puis remontée.
|
||||
n, m := len(a), len(b)
|
||||
lcs := make([][]int, n+1)
|
||||
for i := range lcs {
|
||||
lcs[i] = make([]int, m+1)
|
||||
}
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
for j := m - 1; j >= 0; j-- {
|
||||
if a[i] == b[j] {
|
||||
lcs[i][j] = lcs[i+1][j+1] + 1
|
||||
} else if lcs[i+1][j] >= lcs[i][j+1] {
|
||||
lcs[i][j] = lcs[i+1][j]
|
||||
} else {
|
||||
lcs[i][j] = lcs[i][j+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
var out []DiffLine
|
||||
i, j := 0, 0
|
||||
for i < n && j < m {
|
||||
switch {
|
||||
case a[i] == b[j]:
|
||||
out = append(out, DiffLine{Op: " ", Text: a[i]})
|
||||
i++
|
||||
j++
|
||||
case lcs[i+1][j] >= lcs[i][j+1]:
|
||||
out = append(out, DiffLine{Op: "-", Text: a[i]})
|
||||
i++
|
||||
default:
|
||||
out = append(out, DiffLine{Op: "+", Text: b[j]})
|
||||
j++
|
||||
}
|
||||
}
|
||||
for ; i < n; i++ {
|
||||
out = append(out, DiffLine{Op: "-", Text: a[i]})
|
||||
}
|
||||
for ; j < m; j++ {
|
||||
out = append(out, DiffLine{Op: "+", Text: b[j]})
|
||||
}
|
||||
return capLines(out)
|
||||
}
|
||||
|
||||
// addedDiff présente un contenu entièrement nouveau (création d'une page).
|
||||
func addedDiff(text string) []DiffLine {
|
||||
lines := splitLines(text)
|
||||
out := make([]DiffLine, 0, len(lines))
|
||||
for _, l := range lines {
|
||||
out = append(out, DiffLine{Op: "+", Text: l})
|
||||
}
|
||||
return capLines(out)
|
||||
}
|
||||
|
||||
func splitLines(s string) []string {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return strings.Split(s, "\n")
|
||||
}
|
||||
|
||||
// capLines borne la taille envoyée à l'UI et signale ce qui a été coupé.
|
||||
func capLines(d []DiffLine) []DiffLine {
|
||||
if len(d) <= diffMaxShown {
|
||||
return d
|
||||
}
|
||||
cut := len(d) - diffMaxShown
|
||||
out := append([]DiffLine{}, d[:diffMaxShown]...)
|
||||
return append(out, DiffLine{Op: " ", Text: "…(" + itoa(cut) + " lignes de plus)"})
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b []byte
|
||||
for n > 0 {
|
||||
b = append([]byte{byte('0' + n%10)}, b...)
|
||||
n /= 10
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package jean
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -215,6 +216,10 @@ func MemAdd(name, content string) error {
|
||||
|
||||
// MemEdit remplace oldText par newText dans une page. oldText doit apparaître
|
||||
// EXACTEMENT une fois (sinon erreur), pour une édition sans ambiguïté.
|
||||
// errAlreadyApplied signale une édition dont le résultat est DÉJÀ en place :
|
||||
// ce n'est pas un échec, la page est dans l'état demandé.
|
||||
var errAlreadyApplied = errors.New("déjà à jour — la page contient déjà cette modification")
|
||||
|
||||
func MemEdit(name, oldText, newText string) error {
|
||||
p, err := safeMemPath(name)
|
||||
if err != nil {
|
||||
@@ -230,6 +235,11 @@ func MemEdit(name, oldText, newText string) error {
|
||||
return fmt.Errorf("old vide")
|
||||
}
|
||||
if n == 0 {
|
||||
// Déjà remplacé (le modèle rejoue souvent la même édition) : ce n'est pas
|
||||
// une erreur, la page est dans l'état demandé.
|
||||
if newText != "" && strings.Contains(content, newText) {
|
||||
return errAlreadyApplied
|
||||
}
|
||||
return fmt.Errorf("old introuvable dans la page")
|
||||
}
|
||||
if n > 1 {
|
||||
|
||||
@@ -179,6 +179,11 @@ func fileEdit(path, oldText, newText string) string {
|
||||
content := string(b)
|
||||
n := strings.Count(content, oldText)
|
||||
if n == 0 {
|
||||
// Modification déjà en place : on le dit clairement plutôt que de renvoyer
|
||||
// une erreur, sinon le modèle croit avoir échoué et recommence.
|
||||
if newText != "" && strings.Contains(content, newText) {
|
||||
return "[ok] déjà à jour — le fichier contient déjà cette modification"
|
||||
}
|
||||
return "[erreur] old introuvable dans le fichier"
|
||||
}
|
||||
if n > 1 {
|
||||
|
||||
@@ -35,7 +35,7 @@ func cmdApp(args []string) error {
|
||||
if err != nil {
|
||||
// Jean tourne déjà sur ce port : on ouvre juste l'UI sur l'instance
|
||||
// existante plutôt que d'échouer.
|
||||
fmt.Printf("Jean est déjà lancé — ouverture de %s\n", url)
|
||||
fmt.Printf("AJEAN est déjà lancé — ouverture de %s\n", url)
|
||||
return openBrowser(url)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func cmdApp(args []string) error {
|
||||
startAppLink(appWebMux)
|
||||
}()
|
||||
|
||||
sp := showSplash("Lancement de Jean en cours…")
|
||||
sp := showSplash("Lancement d'AJEAN en cours…")
|
||||
waitServerReady(url)
|
||||
_ = openBrowser(url)
|
||||
time.Sleep(900 * time.Millisecond) // laisse le navigateur s'afficher par-dessus le splash
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -264,6 +265,9 @@ type ToolUsedEvent struct {
|
||||
Result string // tool output (stdout/stderr/exit for run_shell, skill body for read_skill)
|
||||
Done bool // false = call announced (command only); true = result is ready
|
||||
Typing bool // true = command still being written (partial), no spinner yet
|
||||
// Diff : lignes ajoutées/retirées quand l'outil a MODIFIÉ quelque chose
|
||||
// (edit, mem_add, mem_edit). L'UI les affiche en vert (+) et rouge (-).
|
||||
Diff []DiffLine
|
||||
}
|
||||
|
||||
// previewArg pulls the (possibly incomplete) string value of key out of a
|
||||
@@ -414,6 +418,9 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// several times, it's stuck — break instead of spinning to the iteration cap.
|
||||
lastSig := ""
|
||||
repeatSig := 0
|
||||
// Appels d'outil déjà exécutés (clé = nom + arguments bruts) : sert à ne pas
|
||||
// rejouer deux fois exactement la même écriture dans un même échange.
|
||||
doneCalls := map[string]string{}
|
||||
// Garde-fou « pensé sans agir » : certains modèles à reasoning planifient un
|
||||
// appel d'outil dans leur <think> puis émettent le token de fin SANS l'émettre
|
||||
// (ni réponse, ni tool_call). On relance alors UNE fois le tour avec un nudge
|
||||
@@ -747,6 +754,22 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label}})
|
||||
|
||||
result := ""
|
||||
// diff : rempli par les outils d'écriture (edit / mémoire) pour que
|
||||
// l'UI montre les lignes ajoutées et retirées.
|
||||
var diff []DiffLine
|
||||
// Appel rigoureusement identique déjà exécuté dans ce tour : on ne le
|
||||
// rejoue pas. Les petits modèles réémettent volontiers deux fois la
|
||||
// même écriture ; la rejouer produisait une fausse erreur (« old
|
||||
// introuvable », puisque le remplacement est déjà fait).
|
||||
callKey := tc.Function.Name + "\x00" + tc.Function.Arguments
|
||||
if prev, seen := doneCalls[callKey]; seen {
|
||||
result = prev + "\n[note] appel identique déjà exécuté dans ce tour — non rejoué"
|
||||
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: result, Done: true}})
|
||||
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
||||
messages = append(messages, toolMsg)
|
||||
extra = append(extra, toolMsg)
|
||||
continue
|
||||
}
|
||||
switch tc.Function.Name {
|
||||
case "mem_search":
|
||||
lim := 0
|
||||
@@ -782,19 +805,27 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' créée", label)
|
||||
diff = addedDiff(content)
|
||||
}
|
||||
case "mem_edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
if werr := MemEdit(label, oldText, newText); werr != nil {
|
||||
if werr := MemEdit(label, oldText, newText); errors.Is(werr, errAlreadyApplied) {
|
||||
result = fmt.Sprintf("[ok] page '%s' %s", label, werr.Error())
|
||||
} else if werr != nil {
|
||||
result = "[erreur] " + werr.Error()
|
||||
} else {
|
||||
result = fmt.Sprintf("[ok] page '%s' modifiée", label)
|
||||
diff = lineDiff(oldText, newText)
|
||||
}
|
||||
case "edit":
|
||||
oldText, _ := args["old"].(string)
|
||||
newText, _ := args["new"].(string)
|
||||
result = fileEdit(label, oldText, newText)
|
||||
// Diff seulement si l'édition a réussi (sinon le fichier n'a pas bougé).
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
diff = lineDiff(oldText, newText)
|
||||
}
|
||||
case "bash":
|
||||
to := 0
|
||||
switch v := args["timeout"].(type) {
|
||||
@@ -819,11 +850,14 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
result = "[erreur] outil inconnu: " + tc.Function.Name
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(result, "[erreur]") {
|
||||
doneCalls[callKey] = result
|
||||
}
|
||||
shown := result
|
||||
if r := []rune(shown); len(r) > 4000 {
|
||||
shown = string(r[:4000]) + "\n…[tronqué]"
|
||||
}
|
||||
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shown, Done: true}})
|
||||
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shown, Done: true, Diff: diff}})
|
||||
toolMsg := Message{Role: "tool", ToolCallID: tc.ID, Content: result}
|
||||
messages = append(messages, toolMsg)
|
||||
extra = append(extra, toolMsg)
|
||||
|
||||
@@ -225,7 +225,7 @@ func runLinkForeground() error {
|
||||
return fmt.Errorf("aucun token. Usage: jean link <token>")
|
||||
}
|
||||
fmt.Printf("%s connexion au relais %s …\n", cyan("[link]"), relayURL())
|
||||
fmt.Printf(" (UI Jean + endpoint OpenAI servis dans le tunnel — pas besoin de 'jean web')\n")
|
||||
fmt.Printf(" (UI AJEAN + endpoint OpenAI servis dans le tunnel — pas besoin de 'jean web')\n")
|
||||
if fp := e2eFingerprint(); fp != "" {
|
||||
fmt.Printf("%s empreinte E2E : %s (code d'appairage : jean link code)\n", green("[e2e]"), bold(fp))
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.5.9"
|
||||
const Version = "0.5.10"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/jean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -53,7 +53,7 @@ func appWarning() string {
|
||||
if !strings.Contains(exe, "/AppTranslocation/") {
|
||||
return ""
|
||||
}
|
||||
return "Jean tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " +
|
||||
return "AJEAN tourne depuis une copie temporaire en lecture seule (App Translocation de macOS). " +
|
||||
"Fermez l'app, déplacez Jean.app dans le dossier Applications, puis rouvrez-la — " +
|
||||
"sinon chaque lancement crée une instance séparée et les mises à jour du moteur échouent."
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func createSplashWindow() uintptr {
|
||||
cy, _, _ := pGetSystemMetrics.Call(smCY)
|
||||
x := (int(cx) - splashW) / 2
|
||||
y := (int(cy) - splashH) / 2
|
||||
title, _ := syscall.UTF16PtrFromString("Jean")
|
||||
title, _ := syscall.UTF16PtrFromString("AJEAN")
|
||||
|
||||
hwnd, _, _ := pCreateWindowExW.Call(
|
||||
uintptr(exTopmost|exToolwindow),
|
||||
@@ -240,7 +240,7 @@ func paintSplash(hdc uintptr) {
|
||||
pSetTextColor.Call(hdc, colWhite)
|
||||
|
||||
textX := ox + logo + 26
|
||||
drawText(hdc, "Jean", -30, 700, rect{textX, 40, splashW - 20, 82})
|
||||
drawText(hdc, "AJEAN", -30, 700, rect{textX, 40, splashW - 20, 82})
|
||||
drawText(hdc, "Lancement en cours…", -17, 400, rect{textX, 84, splashW - 20, 118})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
func runTray(url string) {
|
||||
systray.Run(func() {
|
||||
systray.SetIcon(trayIconPNG())
|
||||
systray.SetTooltip("Jean — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir Jean", "Ouvrir l'interface")
|
||||
systray.SetTooltip("AJEAN — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter Jean")
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
func runTray(url string) {
|
||||
systray.Run(func() {
|
||||
systray.SetIcon(trayIcon())
|
||||
systray.SetTitle("Jean")
|
||||
systray.SetTooltip("Jean — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir Jean", "Ouvrir l'interface")
|
||||
systray.SetTitle("AJEAN")
|
||||
systray.SetTooltip("AJEAN — votre IA locale")
|
||||
mOpen := systray.AddMenuItem("Ouvrir AJEAN", "Ouvrir l'interface")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter Jean")
|
||||
mQuit := systray.AddMenuItem("Quitter", "Arrêter AJEAN")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
|
||||
@@ -368,7 +368,7 @@ func restartAfterUpdate() (bool, string) {
|
||||
|
||||
func restartHintText() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return "Redémarre Jean (quitte puis relance) pour appliquer la mise à jour."
|
||||
return "Redémarre AJEAN (quitte puis relance) pour appliquer la mise à jour."
|
||||
}
|
||||
return "Redémarre les services pour appliquer : sudo systemctl restart jean jean-link"
|
||||
}
|
||||
|
||||
+212
-84
@@ -7,8 +7,8 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Jean">
|
||||
<title>Jean</title>
|
||||
<meta name="apple-mobile-web-app-title" content="AJEAN">
|
||||
<title>AJEAN</title>
|
||||
<!-- Favicon (même marque « ajean » que la landing/portail) + icône PWA (écran d'accueil
|
||||
iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon
|
||||
(iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. -->
|
||||
@@ -24,7 +24,7 @@
|
||||
reste (sélecteur, persistance, application) est automatique. */
|
||||
/* --font : police par défaut (mono). Un thème peut la surcharger (voir "soft").
|
||||
Les blocs plus spécifiques qui ne redéfinissent pas --font héritent de :root. */
|
||||
:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f}
|
||||
:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--add:#3fb950;--add-bg:rgba(63,185,80,.12);--del:#f85149;--del-bg:rgba(248,81,73,.12)}
|
||||
[data-theme="dark"]{color-scheme:dark;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f}
|
||||
[data-theme="light"]{color-scheme:light;--bg:#f0f1f4;--panel:#f8f9fb;--border:#d7dbe1;--text:#24292f;--dim:#636b74;--accent:#0969da;--accent-bg:rgba(9,105,218,.10);--ok:#0969da;--warn:#9a6700;--err:#cf222e;--mag:#8250df;--code-bg:#e6e9ee;--danger-bg:#fbe6e6;--warn-bg:#fbefda}
|
||||
/* "doux" — thème clair chaud + police Lexend (lecture agréable). */
|
||||
@@ -39,7 +39,10 @@
|
||||
/* "AJEAN" — clair, bleu, esprit rétro-machine : bordures nettes de 2 px,
|
||||
aucun arrondi, ombres en blocs décalés, fond quadrillé (même charte que
|
||||
ajean.app). Mise en page complète plus bas (section "Thème pixel"). */
|
||||
[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0}
|
||||
[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0;--soft:#e8f0ff;--sh:#cddffb;--knob:#ffffff}
|
||||
/* « AJEAN sombre » — même habillage pixel, palette nuit : bleu vif sur fond
|
||||
presque noir, quadrillage discret, ombres bleutées. */
|
||||
[data-theme="ajean-dark"]{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#3b82f6;--bg:#050810;--panel:#0a1020;--border:#16233f;--text:#dce6ff;--dim:#63739c;--accent:#3b82f6;--accent-bg:rgba(59,130,246,.16);--ok:#3b82f6;--warn:#d29922;--err:#f85149;--mag:#9b8cff;--code-bg:#03060d;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--grid:#0a1120;--line2:#1e3054;--soft:#101b33;--sh:#132342;--knob:#dce6ff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);height:100vh;height:100dvh;display:flex;font-size:14px}
|
||||
/* PWA plein écran (viewport-fit=cover) : on décale le contenu des bords pour ne
|
||||
@@ -215,6 +218,16 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.msg.reasoning{align-self:flex-start;background:transparent;border:1px dashed var(--border);color:var(--mag);font-size:12px;max-width:90%;overflow-wrap:anywhere;word-break:break-word}
|
||||
.msg.tool{align-self:flex-start;background:transparent;border:1px dashed var(--border);max-width:90%;white-space:normal;padding:8px 12px;font-size:12px;overflow-wrap:anywhere;word-break:break-word}
|
||||
.msg.tool .tool-head{font-size:12px;color:var(--accent);font-weight:600;margin-bottom:6px}
|
||||
/* ===== Diff des écritures (outil edit / mémoire) ==================
|
||||
Lignes ajoutées en vert, retirées en rouge, contexte neutre. Les couleurs
|
||||
viennent de variables : chaque thème peut les ajuster (clair vs sombre). */
|
||||
.msg.tool pre.diff{display:block;padding:0;overflow-x:auto;background:var(--code-bg)}
|
||||
.msg.tool pre.diff .dl{display:block;padding:1px 10px;white-space:pre-wrap;word-break:break-word;color:var(--dim)}
|
||||
.msg.tool pre.diff .dl.add{color:var(--add);background:var(--add-bg)}
|
||||
.msg.tool pre.diff .dl.del{color:var(--del);background:var(--del-bg)}
|
||||
.diff-count{margin-left:8px;letter-spacing:0;text-transform:none}
|
||||
.diff-count .a{color:var(--add)}
|
||||
.diff-count .d{color:var(--del)}
|
||||
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
||||
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
||||
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
||||
@@ -265,7 +278,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
#ctx-text{font-size:10px;letter-spacing:.03em;white-space:nowrap}
|
||||
#ctx-compact{display:none;margin:0;padding:2px 10px;font-size:10px;background:var(--warn-bg);border-color:var(--warn,#c93)}
|
||||
#inputbar{padding:8px 12px;display:flex;gap:8px}
|
||||
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:44px;max-height:200px}
|
||||
/* Astuce clavier sous la saisie (style repris du HTML : un thème doit pouvoir
|
||||
changer ses marges, ce qu'un style en ligne empêchait). */
|
||||
#sendhint{padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7}
|
||||
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:56px;max-height:200px;overflow-y:hidden}
|
||||
#input:focus{outline:none;border-color:var(--accent)}
|
||||
#send{padding:0 20px}
|
||||
.muted{color:var(--dim);font-size:12px}
|
||||
@@ -363,10 +379,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.pe-row.stack.plain .pe-inp:focus{box-shadow:none}
|
||||
/* Champ + bouton accolé (téléchargement) — même trait que les autres champs,
|
||||
sans pastille colorée qui ressort. */
|
||||
.pe-inpbtn{display:flex}
|
||||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0}
|
||||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-left:0;border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s}
|
||||
.pe-inpbtn button:hover{color:var(--accent);border-color:var(--accent)}
|
||||
.pe-inpbtn{display:flex;align-items:stretch}
|
||||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}
|
||||
/* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) :
|
||||
sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du
|
||||
champ, avec un coin rond au milieu de la paire. */
|
||||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s}
|
||||
.pe-inpbtn button:hover:not(:disabled){color:#fff;background:var(--accent);border-color:var(--accent)}
|
||||
.pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)}
|
||||
.pe-inpbtn button:disabled{opacity:.5;cursor:default}
|
||||
/* Contrôle segmenté (moteur) façon iOS. */
|
||||
.pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%}
|
||||
@@ -539,7 +559,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
[data-theme="gpt"] #scrollbtn{bottom:180px;border-radius:999px}
|
||||
[data-theme="gpt"] #compact-banner{order:0;margin:0 var(--col) 8px;border-radius:14px}
|
||||
[data-theme="gpt"] #inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:26px 26px 0 0}
|
||||
[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:26px}
|
||||
[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px}
|
||||
/* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même
|
||||
bordure, seule la saisie change (curseur). */
|
||||
[data-theme="gpt"] #input:focus{border-color:transparent}
|
||||
@@ -559,9 +579,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
l'arrondi du pied de carte. */
|
||||
[data-theme="gpt"] #ctxtrack{top:auto;bottom:0;height:7px;background:var(--border)}
|
||||
[data-theme="gpt"] #ctx-fill{background:var(--text);opacity:.75}
|
||||
[data-theme="gpt"] #sendhint{order:4;padding:8px var(--col) 0}
|
||||
[data-theme="gpt"] #sendhint{order:4;padding:12px var(--col) 2px}
|
||||
/* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */
|
||||
[data-theme="gpt"] button,[data-theme="gpt"] .addbtn,[data-theme="gpt"] .mem-search,[data-theme="gpt"] #theme-select{border-radius:10px}
|
||||
/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */
|
||||
[data-theme="gpt"] .pe-inpbtn .pe-inp{border-radius:10px 0 0 10px}
|
||||
[data-theme="gpt"] .pe-inpbtn button{border-radius:0 10px 10px 0}
|
||||
[data-theme="gpt"] .input-btn .ctl{border-radius:10px 0 0 10px}
|
||||
[data-theme="gpt"] .input-btn .btn-in{border-radius:0 10px 10px 0}
|
||||
[data-theme="gpt"] .preset,[data-theme="gpt"] .gated-block,[data-theme="gpt"] .lc-mode{border-radius:12px}
|
||||
[data-theme="gpt"] .preset{background:transparent;border-color:transparent}
|
||||
[data-theme="gpt"] .preset:hover{background:var(--accent-bg);border-color:transparent}
|
||||
@@ -576,52 +601,122 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
Rétro-machine : plus aucun arrondi, bordures de 2 px, ombres portées en
|
||||
blocs pleins (pas de flou), fond quadrillé comme du papier millimétré,
|
||||
boutons en capitales qui « s'enfoncent » au survol. Tout est préfixé
|
||||
[data-theme="ajean"] : aucun autre thème n'est touché. */
|
||||
[data-theme^="ajean"] : aucun autre thème n'est touché. */
|
||||
/* Un seul balayage pour supprimer TOUS les arrondis (bulles, boutons, pastilles,
|
||||
interrupteurs, points animés…) — c'est ce qui fait l'identité du thème. */
|
||||
[data-theme="ajean"] *,[data-theme="ajean"] *::before,[data-theme="ajean"] *::after{border-radius:0 !important}
|
||||
[data-theme^="ajean"] *,[data-theme^="ajean"] *::before,[data-theme^="ajean"] *::after{border-radius:0 !important}
|
||||
/* Fond quadrillé : le papier millimétré derrière toute l'interface. */
|
||||
[data-theme="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px}
|
||||
[data-theme="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)}
|
||||
[data-theme="ajean"] #composer,[data-theme="ajean"] #chat{background:transparent}
|
||||
[data-theme^="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px}
|
||||
[data-theme^="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)}
|
||||
[data-theme^="ajean"] #composer,[data-theme^="ajean"] #chat{background:transparent}
|
||||
/* Boutons : capitales, bordure épaisse, ombre en bloc qui disparaît au survol
|
||||
pendant que le bouton se décale — impression de touche enfoncée. */
|
||||
[data-theme="ajean"] button,[data-theme="ajean"] .addbtn,[data-theme="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s}
|
||||
[data-theme="ajean"] button:hover:not(:disabled),[data-theme="ajean"] .addbtn:hover,[data-theme="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)}
|
||||
[data-theme="ajean"] .preset-edit,[data-theme="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none}
|
||||
[data-theme="ajean"] .copybtn{border:2px solid var(--border)}
|
||||
[data-theme="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] button,[data-theme^="ajean"] .addbtn,[data-theme^="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s}
|
||||
[data-theme^="ajean"] button:hover:not(:disabled),[data-theme^="ajean"] .addbtn:hover,[data-theme^="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)}
|
||||
[data-theme^="ajean"] .preset-edit,[data-theme^="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none}
|
||||
[data-theme^="ajean"] .copybtn{border:2px solid var(--border)}
|
||||
[data-theme^="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
/* Messages : la question en carré bleu plein, la réponse en fiche blanche
|
||||
posée sur son ombre. Raisonnement et outils restent en pointillés. */
|
||||
[data-theme="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)}
|
||||
[data-theme="ajean"] #chat .msg.reasoning,[data-theme="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)}
|
||||
[data-theme="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em}
|
||||
[data-theme="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)}
|
||||
[data-theme="ajean"] .msg.assistant .body pre,[data-theme="ajean"] .msg.reasoning .body pre,[data-theme="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)}
|
||||
[data-theme="ajean"] .msg.typing span{background:var(--accent)}
|
||||
[data-theme^="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] #chat .msg.reasoning,[data-theme^="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)}
|
||||
[data-theme^="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em}
|
||||
[data-theme^="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)}
|
||||
[data-theme^="ajean"] .msg.assistant .body pre,[data-theme^="ajean"] .msg.reasoning .body pre,[data-theme^="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)}
|
||||
[data-theme^="ajean"] .msg.typing span{background:var(--accent)}
|
||||
/* Panneaux, encadrés et champs : mêmes bordures épaisses, mêmes ombres. */
|
||||
[data-theme="ajean"] .preset,[data-theme="ajean"] .gated-block,[data-theme="ajean"] .tag,[data-theme="ajean"] .qtag,[data-theme="ajean"] .rtag,[data-theme="ajean"] .btag,[data-theme="ajean"] .statuspill,[data-theme="ajean"] .lc-mode{border-width:2px}
|
||||
[data-theme="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #input,[data-theme="ajean"] .mem-search,[data-theme="ajean"] .ask-input,[data-theme="ajean"] #modal input,[data-theme="ajean"] #modal textarea,[data-theme="ajean"] #modal select,[data-theme="ajean"] #theme-select{border:2px solid var(--line2);background:var(--panel)}
|
||||
[data-theme="ajean"] #input:focus,[data-theme="ajean"] .mem-search:focus,[data-theme="ajean"] .ask-input:focus{border-color:var(--accent)}
|
||||
[data-theme="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)}
|
||||
[data-theme^="ajean"] .preset,[data-theme^="ajean"] .gated-block,[data-theme^="ajean"] .tag,[data-theme^="ajean"] .qtag,[data-theme^="ajean"] .rtag,[data-theme^="ajean"] .btag,[data-theme^="ajean"] .statuspill,[data-theme^="ajean"] .lc-mode{border-width:2px}
|
||||
[data-theme^="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #input,[data-theme^="ajean"] .mem-search,[data-theme^="ajean"] .ask-input,[data-theme^="ajean"] #theme-select,[data-theme^="ajean"] .modal-card .ctl{border:2px solid var(--line2);background:var(--panel)}
|
||||
[data-theme^="ajean"] #input:focus,[data-theme^="ajean"] .mem-search:focus,[data-theme^="ajean"] .ask-input:focus{border-color:var(--accent)}
|
||||
[data-theme^="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)}
|
||||
/* Jauge de contexte : un vrai bandeau plein, pas un filet dégradé. */
|
||||
[data-theme="ajean"] #ctxtrack{height:5px;background:var(--grid)}
|
||||
[data-theme="ajean"] #ctx-fill{background:var(--accent)}
|
||||
[data-theme^="ajean"] #ctxtrack{height:5px;background:var(--grid)}
|
||||
[data-theme^="ajean"] #ctx-fill{background:var(--accent)}
|
||||
/* Fenêtres modales / info-bulles / toast : cadre net + ombre en bloc bleutée. */
|
||||
[data-theme="ajean"] .ask-box,[data-theme="ajean"] .help>.tip,[data-theme="ajean"] #toast,[data-theme="ajean"] .modal-box,[data-theme="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] .ask-box,[data-theme^="ajean"] .help>.tip,[data-theme^="ajean"] #toast,[data-theme^="ajean"] .modal-box,[data-theme^="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
/* Interrupteurs : carrés, bleus quand ils sont actifs. */
|
||||
[data-theme="ajean"] .switch .slider{background:var(--line2)}
|
||||
[data-theme="ajean"] .switch input:checked + .slider{background:var(--accent)}
|
||||
[data-theme^="ajean"] .switch .slider{background:var(--line2)}
|
||||
[data-theme^="ajean"] .switch input:checked + .slider{background:var(--accent)}
|
||||
/* Pastille carrée : box-sizing explicite (un ::before n'hérite pas de la règle
|
||||
globale *{box-sizing:border-box}) — sans ça la bordure débordait du rail. */
|
||||
[data-theme="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:#fff;border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme="ajean"] .switch input:checked + .slider::before{border-color:#fff;background:var(--panel)}
|
||||
[data-theme^="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:var(--knob);border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme^="ajean"] .switch input:checked + .slider::before{border-color:var(--knob);background:var(--knob)}
|
||||
/* Étiquettes de section : capitales espacées, comme des en-têtes de terminal. */
|
||||
[data-theme="ajean"] .subhead,[data-theme="ajean"] .skillshead>span{letter-spacing:.16em}
|
||||
[data-theme^="ajean"] .subhead,[data-theme^="ajean"] .skillshead>span{letter-spacing:.16em}
|
||||
|
||||
/* ----- Thème AJEAN : modales (éditeur de preset, mémoire, MCP) -------------
|
||||
Les règles génériques du thème (bouton en capitales + ombre en bloc + décalage
|
||||
au survol) sont faites pour la barre latérale ; dans un formulaire dense elles
|
||||
rendaient l'éditeur de preset illisible : boutons énormes, ombres qui se
|
||||
chevauchent, champs collés au fond de la carte. On rejoue donc la modale à
|
||||
plat — hiérarchie nette, un seul niveau de relief (la carte). */
|
||||
[data-theme^="ajean"] .modal-ov{background:rgba(15,31,61,.45)}
|
||||
[data-theme^="ajean"] .modal-card{background:var(--panel);border:2px solid var(--line2);box-shadow:8px 8px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] .modal-head{border-bottom:2px solid var(--border);background:var(--soft,#e8f0ff)}
|
||||
[data-theme^="ajean"] .modal-head strong{text-transform:uppercase;letter-spacing:.14em;font-size:12px;color:var(--accent)}
|
||||
[data-theme^="ajean"] .modal-foot{border-top:2px solid var(--border)}
|
||||
/* Champs simples (mémoire, MCP…) : fond teinté pour se détacher de la carte. */
|
||||
[data-theme^="ajean"] .ctl{border:2px solid var(--line2);background:var(--bg);background-image:none}
|
||||
[data-theme^="ajean"] .ctl:focus{border-color:var(--accent)}
|
||||
[data-theme^="ajean"] .fld-l{color:var(--text);letter-spacing:.1em}
|
||||
[data-theme^="ajean"] .fld-hint,[data-theme^="ajean"] .seg .be-note{color:var(--dim)}
|
||||
[data-theme^="ajean"] .modal-body .card{border:2px solid var(--border);box-shadow:none;background:var(--panel)}
|
||||
[data-theme^="ajean"] .card-h{letter-spacing:.14em}
|
||||
|
||||
/* --- Éditeur de preset (listes façon Réglages) --- */
|
||||
[data-theme^="ajean"] .pe-body{background:var(--bg)}
|
||||
/* En-tête de groupe : petite étiquette bleue en capitales. */
|
||||
[data-theme^="ajean"] .pe-gh{color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:11px;font-weight:700}
|
||||
/* La LISTE porte le cadre ; les champs à l'intérieur n'en ont pas (sinon double
|
||||
encadrement). Séparateurs de rangées à la même épaisseur. */
|
||||
[data-theme^="ajean"] .pe-list,[data-theme^="ajean"] .pe-adv{border:2px solid var(--border);background:var(--panel)}
|
||||
[data-theme^="ajean"] .pe-row:not(:last-child){border-bottom:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .pe-adv[open]>summary{border-bottom:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .pe-val,[data-theme^="ajean"] .pe-selc select{border:none;background:transparent}
|
||||
/* Champs pleine largeur (URL, chemin…) : eux gardent un cadre, sur fond teinté. */
|
||||
[data-theme^="ajean"] .pe-inp{border:2px solid var(--line2);background:var(--bg)}
|
||||
[data-theme^="ajean"] .pe-inp:focus{border-color:var(--accent);box-shadow:none}
|
||||
[data-theme^="ajean"] .pe-row.stack.plain .pe-inp{border:none;background:transparent}
|
||||
[data-theme^="ajean"] .pe-inpbtn button{border:2px solid var(--line2);box-shadow:none;background:var(--panel);text-transform:none;letter-spacing:0}
|
||||
[data-theme^="ajean"] .pe-inpbtn .pe-inp{border-right:0}
|
||||
[data-theme^="ajean"] .pe-inpbtn button:hover:not(:disabled){transform:none;background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
/* Sélecteur segmenté : cases carrées côte à côte, bleu plein pour l'active. */
|
||||
[data-theme^="ajean"] .pe-seg{background:transparent;padding:0;gap:8px}
|
||||
[data-theme^="ajean"] .pe-seg>label{border:2px solid var(--border);background:var(--panel);font-weight:700;text-transform:uppercase;letter-spacing:.06em;font-size:11px;padding:9px 4px}
|
||||
[data-theme^="ajean"] .pe-seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none}
|
||||
/* Interrupteurs de la modale : carrés, comme ceux de la barre latérale. */
|
||||
[data-theme^="ajean"] .pe-sl{background:var(--line2)}
|
||||
[data-theme^="ajean"] .pe-sl::before{box-sizing:border-box;background:var(--knob);border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme^="ajean"] .pe-switch input:checked+.pe-sl{background:var(--accent)}
|
||||
[data-theme^="ajean"] .pe-switch input:checked+.pe-sl::before{border-color:var(--knob);background:var(--knob)}
|
||||
/* Boutons DANS la modale : mêmes capitales, mais posés à plat — pas d'ombre ni
|
||||
de décalage au survol, sinon les rangées de boutons dansent et se recouvrent. */
|
||||
[data-theme^="ajean"] .modal-card button{box-shadow:none;font-size:11.5px}
|
||||
[data-theme^="ajean"] .modal-card button:hover:not(:disabled){transform:none}
|
||||
[data-theme^="ajean"] .modal-card .btn-save{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] .modal-card .btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err)}
|
||||
[data-theme^="ajean"] .modal-card .btn-danger:hover{background:var(--err);border-color:var(--err);color:#fff}
|
||||
[data-theme^="ajean"] .modal-x{border:2px solid transparent;background:transparent;color:var(--dim)}
|
||||
[data-theme^="ajean"] .modal-x:hover{background:transparent;border-color:var(--line2);color:var(--text)}
|
||||
/* Champ + bouton accolés : les bordures se chevauchent au lieu de doubler. */
|
||||
[data-theme^="ajean"] .input-btn .btn-in{margin-left:-2px}
|
||||
/* Sélecteur segmenté : bleu plein quand c'est coché (le liseré seul se voyait mal). */
|
||||
[data-theme^="ajean"] .seg>label{border:2px solid var(--border);background:var(--bg)}
|
||||
[data-theme^="ajean"] .seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] .seg>label:has(input:checked) .be-note{color:rgba(255,255,255,.85)}
|
||||
/* Listes (MCP, outils) : mêmes bordures épaisses, survol teinté. */
|
||||
[data-theme^="ajean"] .mcp-row,[data-theme^="ajean"] .mcp-tools-list,[data-theme^="ajean"] #lc-log{border:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .mcp-tool-row:hover{background:var(--soft,#e8f0ff)}
|
||||
|
||||
/* Diff sur fond clair : mêmes rôles, teintes plus profondes pour rester lisibles. */
|
||||
[data-theme="light"],[data-theme="soft"],[data-theme="gpt"],[data-theme="ajean"]{
|
||||
--add:#1a7f37;--add-bg:rgba(26,127,55,.10);--del:#cf222e;--del-bg:rgba(207,34,46,.10)}
|
||||
[data-theme="soft-dark"],[data-theme="ajean-dark"]{
|
||||
--add:#4ac26b;--add-bg:rgba(74,194,107,.13);--del:#f85149;--del-bg:rgba(248,81,73,.13)}
|
||||
</style></head><body>
|
||||
<button id="menubtn" onclick="toggleSide()">☰</button>
|
||||
<div id="backdrop" onclick="toggleSide()"></div>
|
||||
@@ -655,7 +750,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div id="presets"></div>
|
||||
</details>
|
||||
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠️ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label>
|
||||
<!-- Mémoire / Accès internet / Paramètres : sous-réglages du mode agent, en cartes. Grisés/inertes quand l'agent est off (voir setAgentGate). -->
|
||||
<div id="mem-block" class="gated-block">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
@@ -686,7 +781,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||
</div>
|
||||
<div id="mcp-block" class="gated-block">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" style="margin-left:6px;font-size:10px"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠️ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" style="margin-left:6px;font-size:10px"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div id="mcp-list" class="mcp-list"></div>
|
||||
<button class="addbtn" style="margin-top:6px" onclick="openMcp('')">+ ajouter un serveur</button>
|
||||
</div>
|
||||
@@ -770,17 +865,17 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div id="lc-status" class="lc-head muted">…</div>
|
||||
<div class="lc-modes">
|
||||
<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">Version rapide <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-state" id="lc-fast-state"></div>
|
||||
</div>
|
||||
<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">Version optimisée</div>
|
||||
<div class="lc-mode-d">compilée pour votre machine, plus longue</div>
|
||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||
</div>
|
||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||
<div class="lc-mode-t">🧩 Backend personnalisé</div>
|
||||
<div class="lc-mode-t">Backend personnalisé</div>
|
||||
<div class="lc-mode-d">forks llama.cpp pour modèles spéciaux (ternaire…)</div>
|
||||
<div class="lc-mode-state" id="lc-custom-state"></div>
|
||||
</div>
|
||||
@@ -804,7 +899,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div class="row" style="margin-top:6px">
|
||||
<button onclick="loadAll()">↻ refresh</button>
|
||||
<button onclick="resetChat()">⌫ clear chat</button>
|
||||
<button id="btn-bench" onclick="runBenchUI()">⚡ bench</button>
|
||||
<button id="btn-bench" onclick="runBenchUI()">bench</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -822,18 +917,18 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
</span>
|
||||
</div>
|
||||
<div id="inputbar">
|
||||
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
|
||||
<textarea id="input" rows="1" placeholder="message…" onkeydown="onKey(event)" oninput="autoGrow(this)"></textarea>
|
||||
<button id="send" onclick="send()">send</button>
|
||||
<button id="stop" onclick="stopGen()" style="display:none;background:var(--danger-bg);border-color:var(--err);color:var(--err)">stop</button>
|
||||
</div>
|
||||
<div id="sendhint" class="muted" style="padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
|
||||
<div id="sendhint" class="muted">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast"></div>
|
||||
<div id="bench-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:30;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeBenchModal()">
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column">
|
||||
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<strong>⚡ Benchmark</strong>
|
||||
<strong>Benchmark</strong>
|
||||
<button onclick="closeBenchModal()" style="margin:0">×</button>
|
||||
</div>
|
||||
<div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div>
|
||||
@@ -857,7 +952,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()">
|
||||
<div class="modal-card" style="max-width:560px">
|
||||
<div class="modal-head">
|
||||
<strong>🧩 Backends personnalisés</strong>
|
||||
<strong>Backends personnalisés</strong>
|
||||
<button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="modal-body mform">
|
||||
@@ -898,8 +993,8 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<div class="fld">
|
||||
<label class="fld-l">Transport</label>
|
||||
<div class="seg">
|
||||
<label><input type="radio" name="mcp-transport" value="stdio" onchange="mcpTransportUI('stdio')"><span>⚙️ stdio (local)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="http" onchange="mcpTransportUI('http')"><span>🌐 http (distant)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="stdio" onchange="mcpTransportUI('stdio')"><span>stdio (local)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="http" onchange="mcpTransportUI('http')"><span>http (distant)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-stdio-fields">
|
||||
@@ -973,7 +1068,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">lien direct vers un .gguf</span></span>
|
||||
<div class="pe-inpbtn">
|
||||
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
|
||||
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">⬇</button>
|
||||
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">↓</button>
|
||||
</div>
|
||||
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
|
||||
</div>
|
||||
@@ -1120,7 +1215,7 @@ async function loadSys(){
|
||||
// Registre des thèmes disponibles. Pour en AJOUTER un : ajouter un bloc
|
||||
// [data-theme="…"] dans le <style> puis une entrée {id,label} ici — le reste
|
||||
// (menu déroulant, persistance localStorage, application) est automatique.
|
||||
const THEMES=[{id:'ajean',label:'AJEAN'},{id:'dark',label:'sombre'},{id:'light',label:'clair'},{id:'soft',label:'doux'},{id:'soft-dark',label:'doux sombre'},{id:'gpt',label:'ChatGPT'}];
|
||||
const THEMES=[{id:'ajean',label:'AJEAN'},{id:'ajean-dark',label:'AJEAN sombre'},{id:'dark',label:'sombre'},{id:'light',label:'clair'},{id:'soft',label:'doux'},{id:'soft-dark',label:'doux sombre'},{id:'gpt',label:'ChatGPT'}];
|
||||
// Anciens identifiants (v0.5.7, thème livré en deux variantes) → thème unique.
|
||||
const THEME_ALIASES={'gpt-light':'gpt','gpt-dark':'gpt'};
|
||||
// Lexend (thème "doux") n'est chargé QUE si ce thème est choisi : aucune requête
|
||||
@@ -1252,7 +1347,7 @@ async function jfetch(u, opts){
|
||||
let r;
|
||||
try{ r = await fetch(u, opts); }
|
||||
catch(e){
|
||||
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets Jean ouverts ?)'); }
|
||||
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets AJEAN ouverts ?)'); }
|
||||
throw e;
|
||||
}
|
||||
finally{ if(timer) clearTimeout(timer); }
|
||||
@@ -1323,7 +1418,7 @@ async function checkUpdate(){
|
||||
msg.innerHTML='Nouvelle version <b>v'+r.latest+'</b> disponible. ';
|
||||
const btn=document.createElement('button'); btn.textContent='Mettre à jour'; btn.onclick=applyUpdate;
|
||||
msg.appendChild(btn);
|
||||
} else { msg.textContent='Jean est à jour ✓'; }
|
||||
} else { msg.textContent='AJEAN est à jour ✓'; }
|
||||
}catch(e){ msg.textContent='Erreur réseau'; }
|
||||
b.disabled=false;
|
||||
}
|
||||
@@ -1385,8 +1480,8 @@ async function loadCfg(){
|
||||
// Moteur : rapide / optimisé / custom (avec le chemin). Le title garde
|
||||
// toujours le chemin complet, quel que soit le libellé.
|
||||
let v;
|
||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='⚡ rapide';
|
||||
else if(lc && lc.in_use) v='🔧 optimisé';
|
||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='rapide';
|
||||
else if(lc && lc.in_use) v='optimisé';
|
||||
else v='custom : '+c.BIN;
|
||||
rows.push(row('MOTEUR', v, c.BIN));
|
||||
}
|
||||
@@ -1551,7 +1646,7 @@ function renderInternet(s){
|
||||
const st=document.getElementById('internet-status');
|
||||
if(!s.url){ st.textContent='serveur non configuré'; st.style.color=''; }
|
||||
else if(s.reachable){ st.innerHTML='<span style="color:var(--accent)">✓</span> serveur joignable — outils web actifs'; }
|
||||
else { st.innerHTML='⚠️ serveur injoignable — les outils web ne seront pas proposés'; }
|
||||
else { st.innerHTML='⚠ serveur injoignable — les outils web ne seront pas proposés'; }
|
||||
}
|
||||
async function loadInternet(){ renderInternet(await jget('/api/internet')); }
|
||||
// --- Accès OpenAI (endpoint /v1 + clé API des complétions) -----------------
|
||||
@@ -1669,7 +1764,7 @@ async function runBenchUI(){
|
||||
'</div>' +
|
||||
'<div class="muted" style="text-align:center;font-size:11px">total '+x.elapsed_sec.toFixed(2)+'s</div>';
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '⚡ bench';
|
||||
btn.disabled = false; btn.textContent = 'bench';
|
||||
rerun.disabled = false;
|
||||
loadPresets();
|
||||
}
|
||||
@@ -1952,7 +2047,7 @@ async function delItem(){
|
||||
const name = document.getElementById('m-name').value.trim() || editingKey;
|
||||
const delModel = editingKind==='preset' && document.getElementById('m-del-model').checked;
|
||||
let msg = 'Supprimer le ' + K.label.toLowerCase() + ' « ' + name + ' » ?';
|
||||
if(delModel) msg += '\n\n⚠️ Le fichier .gguf du modèle sera AUSSI supprimé du disque (irréversible).';
|
||||
if(delModel) msg += '\n\n⚠ Le fichier .gguf du modèle sera AUSSI supprimé du disque (irréversible).';
|
||||
if(!await askConfirm(msg, {title:'Suppression', okText:'Supprimer', danger:true})) return;
|
||||
const payload = editingKind==='preset'
|
||||
? {id: editingKey, deleteModel: delModel}
|
||||
@@ -1997,7 +2092,7 @@ async function startDownload(){
|
||||
}
|
||||
const pct = st.total>0 ? Math.round(st.done*100/st.total) : 0;
|
||||
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+pct+'%)' : '';
|
||||
prog.textContent = '⬇ '+fmtSize(st.done)+tot+' — '+fname;
|
||||
prog.textContent = '↓ '+fmtSize(st.done)+tot+' — '+fname;
|
||||
}, 800);
|
||||
}
|
||||
// Smart autoscroll: follow the bottom while the user hasn't manually scrolled
|
||||
@@ -2101,26 +2196,26 @@ function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); addCo
|
||||
// Render a tool call as its own conversation message: the command the model
|
||||
// wrote, then the response it got back. textContent keeps it injection-safe.
|
||||
function renderToolMsg(el, tu){
|
||||
// Métadonnées d'affichage par outil : nom court + en-tête avec icône. Les outils
|
||||
// web (web_search/open/read/grep) doivent afficher 🌐, pas le fallback mémoire.
|
||||
// Métadonnées d'affichage par outil : nom court + en-tête. Les outils web
|
||||
// (web_search/open/read/grep) ont leur propre libellé, pas le fallback mémoire.
|
||||
const META = {
|
||||
bash: {lbl:'terminal', head:'⚙️ commande'},
|
||||
edit: {lbl:'édition', head:'✏️ édition'},
|
||||
web_search: {lbl:'recherche', head:'🌐 recherche web'},
|
||||
web_open: {lbl:'page web', head:'🌐 ouverture'},
|
||||
web_read: {lbl:'page web', head:'🌐 lecture'},
|
||||
web_grep: {lbl:'page web', head:'🌐 recherche'},
|
||||
bash: {lbl:'terminal', head:'commande'},
|
||||
edit: {lbl:'édition', head:'édition'},
|
||||
web_search: {lbl:'recherche', head:'recherche web'},
|
||||
web_open: {lbl:'page web', head:'ouverture'},
|
||||
web_read: {lbl:'page web', head:'lecture'},
|
||||
web_grep: {lbl:'page web', head:'recherche'},
|
||||
};
|
||||
// Outils MCP (nom mcp__<serveur>__<outil>) : icône dédiée + libellé lisible,
|
||||
// Outils MCP (nom mcp__<serveur>__<outil>) : en-tête = nom du serveur, libellé lisible,
|
||||
// pas le fallback mémoire. On extrait serveur et outil du nom namespacé.
|
||||
let meta = META[tu.name];
|
||||
if(!meta && tu.name && tu.name.indexOf('mcp__')===0){
|
||||
const parts = tu.name.slice(5).split('__');
|
||||
const server = parts.shift() || 'mcp';
|
||||
const tool = parts.join('__') || tu.name;
|
||||
meta = {lbl: tool, head: '🔌 '+server};
|
||||
meta = {lbl: tool, head: server};
|
||||
}
|
||||
meta = meta || {lbl:'mémoire', head:'🧠 mémoire'};
|
||||
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
||||
let lbl = meta.lbl;
|
||||
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
||||
if(tu.result){ lbl += ' · ~' + Math.max(1, Math.round(tu.result.length/4)) + ' tok'; }
|
||||
@@ -2135,6 +2230,26 @@ function renderToolMsg(el, tu){
|
||||
if(tu.typing){ const car=document.createElement('span'); car.className='tool-caret'; car.textContent='▋'; code.appendChild(car); }
|
||||
pre.appendChild(code); body.appendChild(pre);
|
||||
}
|
||||
// Diff d'une écriture (fichier ou page de mémoire) : lignes ajoutées en vert,
|
||||
// retirées en rouge, contexte en gris — comme un diff de terminal.
|
||||
if(tu.diff && tu.diff.length){
|
||||
const sub=document.createElement('div'); sub.className='tool-sub';
|
||||
let add=0, del=0;
|
||||
tu.diff.forEach(l=>{ if(l.op==='+') add++; else if(l.op==='-') del++; });
|
||||
sub.textContent='modifications';
|
||||
const cnt=document.createElement('span'); cnt.className='diff-count';
|
||||
cnt.innerHTML='<span class="a">+'+add+'</span> <span class="d">-'+del+'</span>';
|
||||
sub.appendChild(cnt);
|
||||
body.appendChild(sub);
|
||||
const pre=document.createElement('pre'); pre.className='diff';
|
||||
tu.diff.forEach(l=>{
|
||||
const ln=document.createElement('span');
|
||||
ln.className='dl'+(l.op==='+'?' add':l.op==='-'?' del':'');
|
||||
ln.textContent=(l.op==='+'?'+':l.op==='-'?'-':' ')+' '+l.text;
|
||||
pre.appendChild(ln);
|
||||
});
|
||||
body.appendChild(pre);
|
||||
}
|
||||
const hasResult = tu.result!==undefined && tu.result!=='';
|
||||
if(hasResult){
|
||||
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
||||
@@ -2152,6 +2267,8 @@ function renderToolMsg(el, tu){
|
||||
function addCopyButtons(root){
|
||||
root.querySelectorAll('pre').forEach(pre=>{
|
||||
if(pre.querySelector('.copybtn')) return;
|
||||
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
||||
if(pre.classList.contains('diff')) return;
|
||||
const btn=document.createElement('button');
|
||||
btn.className='copybtn'; btn.type='button'; btn.textContent='copier';
|
||||
btn.onclick=async(e)=>{
|
||||
@@ -2208,6 +2325,17 @@ function restoreChat(){
|
||||
connectStream();
|
||||
}
|
||||
function onKey(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); } }
|
||||
// La zone de saisie s'ajuste à son contenu : une ligne au repos, puis elle
|
||||
// grandit jusqu'à sa max-height (au-delà, elle défile). Appelée à la frappe, à
|
||||
// l'envoi et au chargement.
|
||||
function autoGrow(ta){
|
||||
ta = ta || document.getElementById('input');
|
||||
if(!ta) return;
|
||||
ta.style.height='auto';
|
||||
const max=parseInt(getComputedStyle(ta).maxHeight,10)||200;
|
||||
ta.style.height=Math.min(ta.scrollHeight, max)+'px';
|
||||
ta.style.overflowY = ta.scrollHeight>max ? 'auto' : 'hidden';
|
||||
}
|
||||
// ===== Conversation SERVEUR (source de vérité, partagée entre appareils) =====
|
||||
// L'historique et la génération vivent sur le serveur jean. Le client ouvre un
|
||||
// flux d'ABONNEMENT permanent (SSE) qui rejoue le journal depuis lastSeq puis
|
||||
@@ -2362,13 +2490,13 @@ async function send(){
|
||||
if(busy) return;
|
||||
const ta=document.getElementById('input'); const text=ta.value.trim();
|
||||
if(!text) return;
|
||||
ta.value='';
|
||||
ta.value=''; autoGrow(ta);
|
||||
for(let attempt=0; attempt<3; attempt++){
|
||||
try{
|
||||
const r=await jfetch('/api/chat/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,ctx_used:CTX_USED})});
|
||||
if(r.status===409) return; // déjà en cours (notre envoi a abouti) → OK
|
||||
if(r.ok) return; // la bulle + les tokens arrivent par le flux
|
||||
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} toast(m); ta.value=text; return; }
|
||||
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} toast(m); ta.value=text; autoGrow(ta); return; }
|
||||
}catch(e){ /* réseau : on retente */ }
|
||||
await new Promise(res=>setTimeout(res, 600));
|
||||
}
|
||||
@@ -2381,8 +2509,8 @@ setInterval(loadStatus, 5000);
|
||||
setInterval(loadVram, 3000);
|
||||
setInterval(loadRam, 3000);
|
||||
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
||||
// ⚡ Version rapide = binaires officiels précompilés (aucune compilation)
|
||||
// 🔧 Version optimisée = compilée pour la machine
|
||||
// « Version rapide » = binaires officiels précompilés (aucune compilation)
|
||||
// « Version optimisée » = compilée pour la machine
|
||||
// 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
|
||||
// et les presets se battent pour la ligne BIN.
|
||||
@@ -2419,7 +2547,7 @@ function lcRenderMode(mode, installed){
|
||||
card.classList.toggle('installed', installed);
|
||||
// Lien de vérification : interroge la dernière version SANS rien installer.
|
||||
// event.stopPropagation empêche le clic de la carte (qui lance l'install).
|
||||
const check = '<span class="lc-update-link" onclick="event.stopPropagation();lcCheck(\''+mode+'\')">🔍 vérifier la version</span>';
|
||||
const check = '<span class="lc-update-link" onclick="event.stopPropagation();lcCheck(\''+mode+'\')">vérifier la version</span>';
|
||||
if(installed){
|
||||
state.innerHTML = '<span class="lc-mode-active-tag">✓ installée</span>'
|
||||
+ '<span class="lc-update-link" onclick="event.stopPropagation();lcUpdate(\''+mode+'\')">↻ mettre à jour</span>'
|
||||
@@ -2698,7 +2826,7 @@ function openMcp(name){
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠️ '+s.error; st.style.color = 'var(--err)'; }
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
else if(s && s.connected){ st.innerHTML = '<span style="color:var(--accent)">✓</span> connecté — '+(s.tools?s.tools.length:0)+' outil(s)'; }
|
||||
document.getElementById('mcp-del').style.display = s ? '' : 'none';
|
||||
renderMcpTools(s);
|
||||
@@ -2777,11 +2905,11 @@ async function saveMcp(){
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
if(!r.ok){ st.textContent = '⚠️ '+(r.error||'échec'); st.style.color = 'var(--err)'; return; }
|
||||
if(!r.ok){ st.textContent = '⚠ '+(r.error||'échec'); st.style.color = 'var(--err)'; return; }
|
||||
renderMCP(r);
|
||||
const saved = (r.servers||[]).find(x=>x.name===name);
|
||||
if(saved && saved.connected){ toast('serveur connecté — '+(saved.tools?saved.tools.length:0)+' outil(s)'); closeMcp(); }
|
||||
else if(saved && saved.error){ st.textContent = '⚠️ enregistré mais connexion échouée : '+saved.error; st.style.color = 'var(--err)'; }
|
||||
else if(saved && saved.error){ st.textContent = '⚠ enregistré mais connexion échouée : '+saved.error; st.style.color = 'var(--err)'; }
|
||||
else { toast('enregistré'); closeMcp(); }
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Jean">
|
||||
<title>Jean</title>
|
||||
<meta name="apple-mobile-web-app-title" content="AJEAN">
|
||||
<title>AJEAN</title>
|
||||
<!-- Favicon (même marque « ajean » que la landing/portail) + icône PWA (écran d'accueil
|
||||
iOS). SVG inline pour le favicon, PNG 180×180 en data-URI pour apple-touch-icon
|
||||
(iOS n'accepte pas le SVG pour l'icône d'accueil). Aucun fichier externe requis. -->
|
||||
@@ -51,7 +51,7 @@
|
||||
<div id="presets"></div>
|
||||
</details>
|
||||
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠️ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label>
|
||||
<label class="switchrow"><span class="switch"><input type="checkbox" id="agent-toggle" onchange="toggleAgent()"><span class="slider"></span></span> activer le mode agent<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">⚠ active l'<b>accès machine</b> (shell complet via <b>bash</b> sur le serveur) — l'IA peut inspecter, lancer des scripts, lire/éditer des fichiers (timeout 30s, max 300s). Mémoire et accès internet ci-dessous ne fonctionnent que si le mode agent est activé.</span></span></label>
|
||||
<!-- Mémoire / Accès internet / Paramètres : sous-réglages du mode agent, en cartes. Grisés/inertes quand l'agent est off (voir setAgentGate). -->
|
||||
<div id="mem-block" class="gated-block">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
@@ -82,7 +82,7 @@
|
||||
<div id="internet-status" class="muted" style="font-size:11px;margin-top:4px"></div>
|
||||
</div>
|
||||
<div id="mcp-block" class="gated-block">
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" style="margin-left:6px;font-size:10px"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠️ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div class="subhead">Serveurs MCP<span id="mcp-badge" style="margin-left:6px;font-size:10px"></span><span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Connecte des <b>serveurs MCP</b> (Model Context Protocol) pour donner de nouveaux outils à l'IA : accès fichiers, bases de données, APIs métier… <b>stdio</b> = programme lancé localement (npx/uvx/binaire) · <b>http</b> = serveur distant. ⚠ un serveur stdio exécute du code sur cette machine, au même titre que le shell.</span></span></div>
|
||||
<div id="mcp-list" class="mcp-list"></div>
|
||||
<button class="addbtn" style="margin-top:6px" onclick="openMcp('')">+ ajouter un serveur</button>
|
||||
</div>
|
||||
@@ -166,17 +166,17 @@
|
||||
<div id="lc-status" class="lc-head muted">…</div>
|
||||
<div class="lc-modes">
|
||||
<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">Version rapide <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-state" id="lc-fast-state"></div>
|
||||
</div>
|
||||
<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">Version optimisée</div>
|
||||
<div class="lc-mode-d">compilée pour votre machine, plus longue</div>
|
||||
<div class="lc-mode-state" id="lc-opt-state"></div>
|
||||
</div>
|
||||
<div class="lc-mode" id="lc-mode-custom" onclick="openCustomBackends()">
|
||||
<div class="lc-mode-t">🧩 Backend personnalisé</div>
|
||||
<div class="lc-mode-t">Backend personnalisé</div>
|
||||
<div class="lc-mode-d">forks llama.cpp pour modèles spéciaux (ternaire…)</div>
|
||||
<div class="lc-mode-state" id="lc-custom-state"></div>
|
||||
</div>
|
||||
@@ -200,7 +200,7 @@
|
||||
<div class="row" style="margin-top:6px">
|
||||
<button onclick="loadAll()">↻ refresh</button>
|
||||
<button onclick="resetChat()">⌫ clear chat</button>
|
||||
<button id="btn-bench" onclick="runBenchUI()">⚡ bench</button>
|
||||
<button id="btn-bench" onclick="runBenchUI()">bench</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
@@ -218,18 +218,18 @@
|
||||
</span>
|
||||
</div>
|
||||
<div id="inputbar">
|
||||
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
|
||||
<textarea id="input" rows="1" placeholder="message…" onkeydown="onKey(event)" oninput="autoGrow(this)"></textarea>
|
||||
<button id="send" onclick="send()">send</button>
|
||||
<button id="stop" onclick="stopGen()" style="display:none;background:var(--danger-bg);border-color:var(--err);color:var(--err)">stop</button>
|
||||
</div>
|
||||
<div id="sendhint" class="muted" style="padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
|
||||
<div id="sendhint" class="muted">Entrée pour envoyer · Maj+Entrée = nouvelle ligne</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast"></div>
|
||||
<div id="bench-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:30;align-items:center;justify-content:center;padding:20px" onclick="if(event.target===this)closeBenchModal()">
|
||||
<div style="background:var(--bg);border:1px solid var(--border);border-radius:8px;width:100%;max-width:480px;display:flex;flex-direction:column">
|
||||
<div style="padding:14px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
|
||||
<strong>⚡ Benchmark</strong>
|
||||
<strong>Benchmark</strong>
|
||||
<button onclick="closeBenchModal()" style="margin:0">×</button>
|
||||
</div>
|
||||
<div id="bench-body" style="padding:18px 20px;display:flex;flex-direction:column;gap:12px"></div>
|
||||
@@ -253,7 +253,7 @@
|
||||
<div id="lc-custom-modal" class="modal-ov" style="display:none" onclick="if(event.target===this)closeCustomBackends()">
|
||||
<div class="modal-card" style="max-width:560px">
|
||||
<div class="modal-head">
|
||||
<strong>🧩 Backends personnalisés</strong>
|
||||
<strong>Backends personnalisés</strong>
|
||||
<button class="modal-x" onclick="closeCustomBackends()" aria-label="Fermer">×</button>
|
||||
</div>
|
||||
<div class="modal-body mform">
|
||||
@@ -294,8 +294,8 @@
|
||||
<div class="fld">
|
||||
<label class="fld-l">Transport</label>
|
||||
<div class="seg">
|
||||
<label><input type="radio" name="mcp-transport" value="stdio" onchange="mcpTransportUI('stdio')"><span>⚙️ stdio (local)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="http" onchange="mcpTransportUI('http')"><span>🌐 http (distant)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="stdio" onchange="mcpTransportUI('stdio')"><span>stdio (local)</span></label>
|
||||
<label><input type="radio" name="mcp-transport" value="http" onchange="mcpTransportUI('http')"><span>http (distant)</span></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mcp-stdio-fields">
|
||||
@@ -369,7 +369,7 @@
|
||||
<span class="pe-row-l">Télécharger un modèle <span class="pe-sub">lien direct vers un .gguf</span></span>
|
||||
<div class="pe-inpbtn">
|
||||
<input id="m-hf-url" class="pe-inp" placeholder="https://…/model.gguf">
|
||||
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">⬇</button>
|
||||
<button id="m-hf-btn" onclick="startDownload()" title="Télécharger">↓</button>
|
||||
</div>
|
||||
<div id="m-hf-progress" class="pe-note" style="display:none"></div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Registre des thèmes disponibles. Pour en AJOUTER un : ajouter un bloc
|
||||
// [data-theme="…"] dans le <style> puis une entrée {id,label} ici — le reste
|
||||
// (menu déroulant, persistance localStorage, application) est automatique.
|
||||
const THEMES=[{id:'ajean',label:'AJEAN'},{id:'dark',label:'sombre'},{id:'light',label:'clair'},{id:'soft',label:'doux'},{id:'soft-dark',label:'doux sombre'},{id:'gpt',label:'ChatGPT'}];
|
||||
const THEMES=[{id:'ajean',label:'AJEAN'},{id:'ajean-dark',label:'AJEAN sombre'},{id:'dark',label:'sombre'},{id:'light',label:'clair'},{id:'soft',label:'doux'},{id:'soft-dark',label:'doux sombre'},{id:'gpt',label:'ChatGPT'}];
|
||||
// Anciens identifiants (v0.5.7, thème livré en deux variantes) → thème unique.
|
||||
const THEME_ALIASES={'gpt-light':'gpt','gpt-dark':'gpt'};
|
||||
// Lexend (thème "doux") n'est chargé QUE si ce thème est choisi : aucune requête
|
||||
|
||||
@@ -25,7 +25,7 @@ async function jfetch(u, opts){
|
||||
let r;
|
||||
try{ r = await fetch(u, opts); }
|
||||
catch(e){
|
||||
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets Jean ouverts ?)'); }
|
||||
if(e && e.name==='AbortError'){ throw new Error('requête sans réponse après 30 s (trop d\'onglets AJEAN ouverts ?)'); }
|
||||
throw e;
|
||||
}
|
||||
finally{ if(timer) clearTimeout(timer); }
|
||||
|
||||
@@ -57,7 +57,7 @@ async function checkUpdate(){
|
||||
msg.innerHTML='Nouvelle version <b>v'+r.latest+'</b> disponible. ';
|
||||
const btn=document.createElement('button'); btn.textContent='Mettre à jour'; btn.onclick=applyUpdate;
|
||||
msg.appendChild(btn);
|
||||
} else { msg.textContent='Jean est à jour ✓'; }
|
||||
} else { msg.textContent='AJEAN est à jour ✓'; }
|
||||
}catch(e){ msg.textContent='Erreur réseau'; }
|
||||
b.disabled=false;
|
||||
}
|
||||
@@ -119,8 +119,8 @@ async function loadCfg(){
|
||||
// Moteur : rapide / optimisé / custom (avec le chemin). Le title garde
|
||||
// toujours le chemin complet, quel que soit le libellé.
|
||||
let v;
|
||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='⚡ rapide';
|
||||
else if(lc && lc.in_use) v='🔧 optimisé';
|
||||
if(lc && lc.prebuilt && lc.prebuilt.in_use) v='rapide';
|
||||
else if(lc && lc.in_use) v='optimisé';
|
||||
else v='custom : '+c.BIN;
|
||||
rows.push(row('MOTEUR', v, c.BIN));
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ function renderInternet(s){
|
||||
const st=document.getElementById('internet-status');
|
||||
if(!s.url){ st.textContent='serveur non configuré'; st.style.color=''; }
|
||||
else if(s.reachable){ st.innerHTML='<span style="color:var(--accent)">✓</span> serveur joignable — outils web actifs'; }
|
||||
else { st.innerHTML='⚠️ serveur injoignable — les outils web ne seront pas proposés'; }
|
||||
else { st.innerHTML='⚠ serveur injoignable — les outils web ne seront pas proposés'; }
|
||||
}
|
||||
async function loadInternet(){ renderInternet(await jget('/api/internet')); }
|
||||
// --- Accès OpenAI (endpoint /v1 + clé API des complétions) -----------------
|
||||
|
||||
@@ -36,7 +36,7 @@ async function runBenchUI(){
|
||||
'</div>' +
|
||||
'<div class="muted" style="text-align:center;font-size:11px">total '+x.elapsed_sec.toFixed(2)+'s</div>';
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = '⚡ bench';
|
||||
btn.disabled = false; btn.textContent = 'bench';
|
||||
rerun.disabled = false;
|
||||
loadPresets();
|
||||
}
|
||||
@@ -319,7 +319,7 @@ async function delItem(){
|
||||
const name = document.getElementById('m-name').value.trim() || editingKey;
|
||||
const delModel = editingKind==='preset' && document.getElementById('m-del-model').checked;
|
||||
let msg = 'Supprimer le ' + K.label.toLowerCase() + ' « ' + name + ' » ?';
|
||||
if(delModel) msg += '\n\n⚠️ Le fichier .gguf du modèle sera AUSSI supprimé du disque (irréversible).';
|
||||
if(delModel) msg += '\n\n⚠ Le fichier .gguf du modèle sera AUSSI supprimé du disque (irréversible).';
|
||||
if(!await askConfirm(msg, {title:'Suppression', okText:'Supprimer', danger:true})) return;
|
||||
const payload = editingKind==='preset'
|
||||
? {id: editingKey, deleteModel: delModel}
|
||||
@@ -364,7 +364,7 @@ async function startDownload(){
|
||||
}
|
||||
const pct = st.total>0 ? Math.round(st.done*100/st.total) : 0;
|
||||
const tot = st.total>0 ? ' / '+fmtSize(st.total)+' ('+pct+'%)' : '';
|
||||
prog.textContent = '⬇ '+fmtSize(st.done)+tot+' — '+fname;
|
||||
prog.textContent = '↓ '+fmtSize(st.done)+tot+' — '+fname;
|
||||
}, 800);
|
||||
}
|
||||
// Smart autoscroll: follow the bottom while the user hasn't manually scrolled
|
||||
|
||||
@@ -97,26 +97,26 @@ function renderBody(el, text){ const b=bodyOf(el); b.innerHTML = md(text); addCo
|
||||
// Render a tool call as its own conversation message: the command the model
|
||||
// wrote, then the response it got back. textContent keeps it injection-safe.
|
||||
function renderToolMsg(el, tu){
|
||||
// Métadonnées d'affichage par outil : nom court + en-tête avec icône. Les outils
|
||||
// web (web_search/open/read/grep) doivent afficher 🌐, pas le fallback mémoire.
|
||||
// Métadonnées d'affichage par outil : nom court + en-tête. Les outils web
|
||||
// (web_search/open/read/grep) ont leur propre libellé, pas le fallback mémoire.
|
||||
const META = {
|
||||
bash: {lbl:'terminal', head:'⚙️ commande'},
|
||||
edit: {lbl:'édition', head:'✏️ édition'},
|
||||
web_search: {lbl:'recherche', head:'🌐 recherche web'},
|
||||
web_open: {lbl:'page web', head:'🌐 ouverture'},
|
||||
web_read: {lbl:'page web', head:'🌐 lecture'},
|
||||
web_grep: {lbl:'page web', head:'🌐 recherche'},
|
||||
bash: {lbl:'terminal', head:'commande'},
|
||||
edit: {lbl:'édition', head:'édition'},
|
||||
web_search: {lbl:'recherche', head:'recherche web'},
|
||||
web_open: {lbl:'page web', head:'ouverture'},
|
||||
web_read: {lbl:'page web', head:'lecture'},
|
||||
web_grep: {lbl:'page web', head:'recherche'},
|
||||
};
|
||||
// Outils MCP (nom mcp__<serveur>__<outil>) : icône dédiée + libellé lisible,
|
||||
// Outils MCP (nom mcp__<serveur>__<outil>) : en-tête = nom du serveur, libellé lisible,
|
||||
// pas le fallback mémoire. On extrait serveur et outil du nom namespacé.
|
||||
let meta = META[tu.name];
|
||||
if(!meta && tu.name && tu.name.indexOf('mcp__')===0){
|
||||
const parts = tu.name.slice(5).split('__');
|
||||
const server = parts.shift() || 'mcp';
|
||||
const tool = parts.join('__') || tu.name;
|
||||
meta = {lbl: tool, head: '🔌 '+server};
|
||||
meta = {lbl: tool, head: server};
|
||||
}
|
||||
meta = meta || {lbl:'mémoire', head:'🧠 mémoire'};
|
||||
meta = meta || {lbl:'mémoire', head:'mémoire'};
|
||||
let lbl = meta.lbl;
|
||||
// Indication du volume de la réponse de l'outil (~tokens, estimation 1 tok ≈ 4 car).
|
||||
if(tu.result){ lbl += ' · ~' + Math.max(1, Math.round(tu.result.length/4)) + ' tok'; }
|
||||
@@ -131,6 +131,26 @@ function renderToolMsg(el, tu){
|
||||
if(tu.typing){ const car=document.createElement('span'); car.className='tool-caret'; car.textContent='▋'; code.appendChild(car); }
|
||||
pre.appendChild(code); body.appendChild(pre);
|
||||
}
|
||||
// Diff d'une écriture (fichier ou page de mémoire) : lignes ajoutées en vert,
|
||||
// retirées en rouge, contexte en gris — comme un diff de terminal.
|
||||
if(tu.diff && tu.diff.length){
|
||||
const sub=document.createElement('div'); sub.className='tool-sub';
|
||||
let add=0, del=0;
|
||||
tu.diff.forEach(l=>{ if(l.op==='+') add++; else if(l.op==='-') del++; });
|
||||
sub.textContent='modifications';
|
||||
const cnt=document.createElement('span'); cnt.className='diff-count';
|
||||
cnt.innerHTML='<span class="a">+'+add+'</span> <span class="d">-'+del+'</span>';
|
||||
sub.appendChild(cnt);
|
||||
body.appendChild(sub);
|
||||
const pre=document.createElement('pre'); pre.className='diff';
|
||||
tu.diff.forEach(l=>{
|
||||
const ln=document.createElement('span');
|
||||
ln.className='dl'+(l.op==='+'?' add':l.op==='-'?' del':'');
|
||||
ln.textContent=(l.op==='+'?'+':l.op==='-'?'-':' ')+' '+l.text;
|
||||
pre.appendChild(ln);
|
||||
});
|
||||
body.appendChild(pre);
|
||||
}
|
||||
const hasResult = tu.result!==undefined && tu.result!=='';
|
||||
if(hasResult){
|
||||
const sub=document.createElement('div'); sub.className='tool-sub'; sub.textContent='réponse';
|
||||
@@ -148,6 +168,8 @@ function renderToolMsg(el, tu){
|
||||
function addCopyButtons(root){
|
||||
root.querySelectorAll('pre').forEach(pre=>{
|
||||
if(pre.querySelector('.copybtn')) return;
|
||||
// Pas de bouton copier sur un diff : on copierait les préfixes + / - .
|
||||
if(pre.classList.contains('diff')) return;
|
||||
const btn=document.createElement('button');
|
||||
btn.className='copybtn'; btn.type='button'; btn.textContent='copier';
|
||||
btn.onclick=async(e)=>{
|
||||
@@ -204,3 +226,14 @@ function restoreChat(){
|
||||
connectStream();
|
||||
}
|
||||
function onKey(e){ if(e.key==='Enter' && !e.shiftKey){ e.preventDefault(); send(); } }
|
||||
// La zone de saisie s'ajuste à son contenu : une ligne au repos, puis elle
|
||||
// grandit jusqu'à sa max-height (au-delà, elle défile). Appelée à la frappe, à
|
||||
// l'envoi et au chargement.
|
||||
function autoGrow(ta){
|
||||
ta = ta || document.getElementById('input');
|
||||
if(!ta) return;
|
||||
ta.style.height='auto';
|
||||
const max=parseInt(getComputedStyle(ta).maxHeight,10)||200;
|
||||
ta.style.height=Math.min(ta.scrollHeight, max)+'px';
|
||||
ta.style.overflowY = ta.scrollHeight>max ? 'auto' : 'hidden';
|
||||
}
|
||||
@@ -152,13 +152,13 @@ async function send(){
|
||||
if(busy) return;
|
||||
const ta=document.getElementById('input'); const text=ta.value.trim();
|
||||
if(!text) return;
|
||||
ta.value='';
|
||||
ta.value=''; autoGrow(ta);
|
||||
for(let attempt=0; attempt<3; attempt++){
|
||||
try{
|
||||
const r=await jfetch('/api/chat/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,ctx_used:CTX_USED})});
|
||||
if(r.status===409) return; // déjà en cours (notre envoi a abouti) → OK
|
||||
if(r.ok) return; // la bulle + les tokens arrivent par le flux
|
||||
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} toast(m); ta.value=text; return; }
|
||||
if(r.status<500){ let m='erreur'; try{ m=(await r.json()).error||m; }catch(_){} toast(m); ta.value=text; autoGrow(ta); return; }
|
||||
}catch(e){ /* réseau : on retente */ }
|
||||
await new Promise(res=>setTimeout(res, 600));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Backend llama.cpp — la barre latérale sert UNIQUEMENT à installer les moteurs.
|
||||
// ⚡ Version rapide = binaires officiels précompilés (aucune compilation)
|
||||
// 🔧 Version optimisée = compilée pour la machine
|
||||
// « Version rapide » = binaires officiels précompilés (aucune compilation)
|
||||
// « Version optimisée » = compilée pour la machine
|
||||
// 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
|
||||
// et les presets se battent pour la ligne BIN.
|
||||
@@ -37,7 +37,7 @@ function lcRenderMode(mode, installed){
|
||||
card.classList.toggle('installed', installed);
|
||||
// Lien de vérification : interroge la dernière version SANS rien installer.
|
||||
// event.stopPropagation empêche le clic de la carte (qui lance l'install).
|
||||
const check = '<span class="lc-update-link" onclick="event.stopPropagation();lcCheck(\''+mode+'\')">🔍 vérifier la version</span>';
|
||||
const check = '<span class="lc-update-link" onclick="event.stopPropagation();lcCheck(\''+mode+'\')">vérifier la version</span>';
|
||||
if(installed){
|
||||
state.innerHTML = '<span class="lc-mode-active-tag">✓ installée</span>'
|
||||
+ '<span class="lc-update-link" onclick="event.stopPropagation();lcUpdate(\''+mode+'\')">↻ mettre à jour</span>'
|
||||
|
||||
@@ -95,7 +95,7 @@ function openMcp(name){
|
||||
document.getElementById('mcp-headers').value = s && s.headers ? Object.entries(s.headers).map(([k,v])=>k+': '+v).join('\n') : '';
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = ''; st.style.color = '';
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠️ '+s.error; st.style.color = 'var(--err)'; }
|
||||
if(s && s.enabled && s.error){ st.textContent = '⚠ '+s.error; st.style.color = 'var(--err)'; }
|
||||
else if(s && s.connected){ st.innerHTML = '<span style="color:var(--accent)">✓</span> connecté — '+(s.tools?s.tools.length:0)+' outil(s)'; }
|
||||
document.getElementById('mcp-del').style.display = s ? '' : 'none';
|
||||
renderMcpTools(s);
|
||||
@@ -174,11 +174,11 @@ async function saveMcp(){
|
||||
const st = document.getElementById('mcp-modal-status');
|
||||
st.textContent = 'connexion…'; st.style.color = '';
|
||||
const r = await jpost('/api/mcp/save', body);
|
||||
if(!r.ok){ st.textContent = '⚠️ '+(r.error||'échec'); st.style.color = 'var(--err)'; return; }
|
||||
if(!r.ok){ st.textContent = '⚠ '+(r.error||'échec'); st.style.color = 'var(--err)'; return; }
|
||||
renderMCP(r);
|
||||
const saved = (r.servers||[]).find(x=>x.name===name);
|
||||
if(saved && saved.connected){ toast('serveur connecté — '+(saved.tools?saved.tools.length:0)+' outil(s)'); closeMcp(); }
|
||||
else if(saved && saved.error){ st.textContent = '⚠️ enregistré mais connexion échouée : '+saved.error; st.style.color = 'var(--err)'; }
|
||||
else if(saved && saved.error){ st.textContent = '⚠ enregistré mais connexion échouée : '+saved.error; st.style.color = 'var(--err)'; }
|
||||
else { toast('enregistré'); closeMcp(); }
|
||||
}
|
||||
|
||||
|
||||
+136
-41
@@ -5,7 +5,7 @@
|
||||
reste (sélecteur, persistance, application) est automatique. */
|
||||
/* --font : police par défaut (mono). Un thème peut la surcharger (voir "soft").
|
||||
Les blocs plus spécifiques qui ne redéfinissent pas --font héritent de :root. */
|
||||
:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f}
|
||||
:root{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,monospace;--bubble:#1f6feb;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--add:#3fb950;--add-bg:rgba(63,185,80,.12);--del:#f85149;--del-bg:rgba(248,81,73,.12)}
|
||||
[data-theme="dark"]{color-scheme:dark;--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--accent-bg:rgba(88,166,255,.12);--ok:#58a6ff;--warn:#d29922;--err:#f85149;--mag:#bc8cff;--code-bg:#0d1117;--danger-bg:#3d1f23;--warn-bg:#3d2f1f}
|
||||
[data-theme="light"]{color-scheme:light;--bg:#f0f1f4;--panel:#f8f9fb;--border:#d7dbe1;--text:#24292f;--dim:#636b74;--accent:#0969da;--accent-bg:rgba(9,105,218,.10);--ok:#0969da;--warn:#9a6700;--err:#cf222e;--mag:#8250df;--code-bg:#e6e9ee;--danger-bg:#fbe6e6;--warn-bg:#fbefda}
|
||||
/* "doux" — thème clair chaud + police Lexend (lecture agréable). */
|
||||
@@ -20,7 +20,10 @@
|
||||
/* "AJEAN" — clair, bleu, esprit rétro-machine : bordures nettes de 2 px,
|
||||
aucun arrondi, ombres en blocs décalés, fond quadrillé (même charte que
|
||||
ajean.app). Mise en page complète plus bas (section "Thème pixel"). */
|
||||
[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0}
|
||||
[data-theme="ajean"]{color-scheme:light;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#1f6feb;--bg:#f5f8ff;--panel:#ffffff;--border:#cfdcf5;--text:#0f1f3d;--dim:#5b6f96;--accent:#1f6feb;--accent-bg:rgba(31,111,235,.10);--ok:#1f6feb;--warn:#9a6700;--err:#cf222e;--mag:#6f52d8;--code-bg:#eef3fe;--danger-bg:#fbe6e6;--warn-bg:#fbefda;--grid:#e4ecfa;--line2:#b6cbf0;--soft:#e8f0ff;--sh:#cddffb;--knob:#ffffff}
|
||||
/* « AJEAN sombre » — même habillage pixel, palette nuit : bleu vif sur fond
|
||||
presque noir, quadrillage discret, ombres bleutées. */
|
||||
[data-theme="ajean-dark"]{color-scheme:dark;--font:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--bubble:#3b82f6;--bg:#050810;--panel:#0a1020;--border:#16233f;--text:#dce6ff;--dim:#63739c;--accent:#3b82f6;--accent-bg:rgba(59,130,246,.16);--ok:#3b82f6;--warn:#d29922;--err:#f85149;--mag:#9b8cff;--code-bg:#03060d;--danger-bg:#3d1f23;--warn-bg:#3d2f1f;--grid:#0a1120;--line2:#1e3054;--soft:#101b33;--sh:#132342;--knob:#dce6ff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:var(--font);background:var(--bg);color:var(--text);height:100vh;height:100dvh;display:flex;font-size:14px}
|
||||
/* PWA plein écran (viewport-fit=cover) : on décale le contenu des bords pour ne
|
||||
@@ -196,6 +199,16 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.msg.reasoning{align-self:flex-start;background:transparent;border:1px dashed var(--border);color:var(--mag);font-size:12px;max-width:90%;overflow-wrap:anywhere;word-break:break-word}
|
||||
.msg.tool{align-self:flex-start;background:transparent;border:1px dashed var(--border);max-width:90%;white-space:normal;padding:8px 12px;font-size:12px;overflow-wrap:anywhere;word-break:break-word}
|
||||
.msg.tool .tool-head{font-size:12px;color:var(--accent);font-weight:600;margin-bottom:6px}
|
||||
/* ===== Diff des écritures (outil edit / mémoire) ==================
|
||||
Lignes ajoutées en vert, retirées en rouge, contexte neutre. Les couleurs
|
||||
viennent de variables : chaque thème peut les ajuster (clair vs sombre). */
|
||||
.msg.tool pre.diff{display:block;padding:0;overflow-x:auto;background:var(--code-bg)}
|
||||
.msg.tool pre.diff .dl{display:block;padding:1px 10px;white-space:pre-wrap;word-break:break-word;color:var(--dim)}
|
||||
.msg.tool pre.diff .dl.add{color:var(--add);background:var(--add-bg)}
|
||||
.msg.tool pre.diff .dl.del{color:var(--del);background:var(--del-bg)}
|
||||
.diff-count{margin-left:8px;letter-spacing:0;text-transform:none}
|
||||
.diff-count .a{color:var(--add)}
|
||||
.diff-count .d{color:var(--del)}
|
||||
.msg.tool .tool-sub{font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;margin:8px 0 4px}
|
||||
.msg.tool pre{position:relative;background:var(--code-bg);border:1px solid var(--border);border-radius:6px;padding:8px 10px;overflow:auto;margin:0;max-height:280px}
|
||||
.msg.tool pre code{background:transparent;padding:0;font-size:.82em;line-height:1.4;white-space:pre}
|
||||
@@ -246,7 +259,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
#ctx-text{font-size:10px;letter-spacing:.03em;white-space:nowrap}
|
||||
#ctx-compact{display:none;margin:0;padding:2px 10px;font-size:10px;background:var(--warn-bg);border-color:var(--warn,#c93)}
|
||||
#inputbar{padding:8px 12px;display:flex;gap:8px}
|
||||
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:44px;max-height:200px}
|
||||
/* Astuce clavier sous la saisie (style repris du HTML : un thème doit pouvoir
|
||||
changer ses marges, ce qu'un style en ligne empêchait). */
|
||||
#sendhint{padding:0 12px 6px;font-size:10px;text-align:center;opacity:.7}
|
||||
#input{flex:1;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:10px;font:inherit;resize:none;min-height:56px;max-height:200px;overflow-y:hidden}
|
||||
#input:focus{outline:none;border-color:var(--accent)}
|
||||
#send{padding:0 20px}
|
||||
.muted{color:var(--dim);font-size:12px}
|
||||
@@ -344,10 +360,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.pe-row.stack.plain .pe-inp:focus{box-shadow:none}
|
||||
/* Champ + bouton accolé (téléchargement) — même trait que les autres champs,
|
||||
sans pastille colorée qui ressort. */
|
||||
.pe-inpbtn{display:flex}
|
||||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0}
|
||||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-left:0;border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s}
|
||||
.pe-inpbtn button:hover{color:var(--accent);border-color:var(--accent)}
|
||||
.pe-inpbtn{display:flex;align-items:stretch}
|
||||
.pe-inpbtn .pe-inp{border-top-right-radius:0;border-bottom-right-radius:0;border-right:0}
|
||||
/* Le bouton reprend EXACTEMENT le cadre du champ (mêmes coins, même bordure) :
|
||||
sans ça, un thème qui arrondit tous les boutons (ChatGPT) le détachait du
|
||||
champ, avec un coin rond au milieu de la paire. */
|
||||
.pe-inpbtn button{flex:none;margin:0;border:1px solid var(--border);border-radius:0 8px 8px 0;background:var(--panel);color:var(--dim);padding:0 15px;cursor:pointer;font-size:15px;transition:color .12s,border-color .12s,background .12s}
|
||||
.pe-inpbtn button:hover:not(:disabled){color:#fff;background:var(--accent);border-color:var(--accent)}
|
||||
.pe-inpbtn:focus-within .pe-inp,.pe-inpbtn:focus-within button{border-color:var(--accent)}
|
||||
.pe-inpbtn button:disabled{opacity:.5;cursor:default}
|
||||
/* Contrôle segmenté (moteur) façon iOS. */
|
||||
.pe-seg{display:flex;gap:3px;background:color-mix(in srgb,var(--dim) 16%,transparent);border-radius:9px;padding:3px;width:100%}
|
||||
@@ -520,7 +540,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
[data-theme="gpt"] #scrollbtn{bottom:180px;border-radius:999px}
|
||||
[data-theme="gpt"] #compact-banner{order:0;margin:0 var(--col) 8px;border-radius:14px}
|
||||
[data-theme="gpt"] #inputbar{order:1;margin:0 var(--col);padding:8px 8px 8px 16px;gap:8px;align-items:flex-end;background:var(--panel);border:1px solid var(--border);border-bottom:none;border-radius:26px 26px 0 0}
|
||||
[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:26px}
|
||||
[data-theme="gpt"] #input{background:transparent;border:none;border-radius:0;padding:9px 0;min-height:36px;line-height:18px;max-height:160px}
|
||||
/* Pas de cadre qui s'assombrit au focus : la carte garde exactement la même
|
||||
bordure, seule la saisie change (curseur). */
|
||||
[data-theme="gpt"] #input:focus{border-color:transparent}
|
||||
@@ -540,9 +560,14 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
l'arrondi du pied de carte. */
|
||||
[data-theme="gpt"] #ctxtrack{top:auto;bottom:0;height:7px;background:var(--border)}
|
||||
[data-theme="gpt"] #ctx-fill{background:var(--text);opacity:.75}
|
||||
[data-theme="gpt"] #sendhint{order:4;padding:8px var(--col) 0}
|
||||
[data-theme="gpt"] #sendhint{order:4;padding:12px var(--col) 2px}
|
||||
/* Reste de l'UI : coins arrondis et contrastes doux, dans le même esprit. */
|
||||
[data-theme="gpt"] button,[data-theme="gpt"] .addbtn,[data-theme="gpt"] .mem-search,[data-theme="gpt"] #theme-select{border-radius:10px}
|
||||
/* Champ + bouton accolés : on ne garde l'arrondi que sur les coins extérieurs. */
|
||||
[data-theme="gpt"] .pe-inpbtn .pe-inp{border-radius:10px 0 0 10px}
|
||||
[data-theme="gpt"] .pe-inpbtn button{border-radius:0 10px 10px 0}
|
||||
[data-theme="gpt"] .input-btn .ctl{border-radius:10px 0 0 10px}
|
||||
[data-theme="gpt"] .input-btn .btn-in{border-radius:0 10px 10px 0}
|
||||
[data-theme="gpt"] .preset,[data-theme="gpt"] .gated-block,[data-theme="gpt"] .lc-mode{border-radius:12px}
|
||||
[data-theme="gpt"] .preset{background:transparent;border-color:transparent}
|
||||
[data-theme="gpt"] .preset:hover{background:var(--accent-bg);border-color:transparent}
|
||||
@@ -557,49 +582,119 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
Rétro-machine : plus aucun arrondi, bordures de 2 px, ombres portées en
|
||||
blocs pleins (pas de flou), fond quadrillé comme du papier millimétré,
|
||||
boutons en capitales qui « s'enfoncent » au survol. Tout est préfixé
|
||||
[data-theme="ajean"] : aucun autre thème n'est touché. */
|
||||
[data-theme^="ajean"] : aucun autre thème n'est touché. */
|
||||
/* Un seul balayage pour supprimer TOUS les arrondis (bulles, boutons, pastilles,
|
||||
interrupteurs, points animés…) — c'est ce qui fait l'identité du thème. */
|
||||
[data-theme="ajean"] *,[data-theme="ajean"] *::before,[data-theme="ajean"] *::after{border-radius:0 !important}
|
||||
[data-theme^="ajean"] *,[data-theme^="ajean"] *::before,[data-theme^="ajean"] *::after{border-radius:0 !important}
|
||||
/* Fond quadrillé : le papier millimétré derrière toute l'interface. */
|
||||
[data-theme="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px}
|
||||
[data-theme="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)}
|
||||
[data-theme="ajean"] #composer,[data-theme="ajean"] #chat{background:transparent}
|
||||
[data-theme^="ajean"] body{background-image:linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:32px 32px}
|
||||
[data-theme^="ajean"] .side{background:var(--panel);border-right:2px solid var(--border)}
|
||||
[data-theme^="ajean"] #composer,[data-theme^="ajean"] #chat{background:transparent}
|
||||
/* Boutons : capitales, bordure épaisse, ombre en bloc qui disparaît au survol
|
||||
pendant que le bouton se décale — impression de touche enfoncée. */
|
||||
[data-theme="ajean"] button,[data-theme="ajean"] .addbtn,[data-theme="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s}
|
||||
[data-theme="ajean"] button:hover:not(:disabled),[data-theme="ajean"] .addbtn:hover,[data-theme="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)}
|
||||
[data-theme="ajean"] .preset-edit,[data-theme="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none}
|
||||
[data-theme="ajean"] .copybtn{border:2px solid var(--border)}
|
||||
[data-theme="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] button,[data-theme^="ajean"] .addbtn,[data-theme^="ajean"] .preset-add{border:2px solid var(--line2);background:var(--panel);color:var(--text);text-transform:uppercase;letter-spacing:.06em;font-weight:700;box-shadow:3px 3px 0 0 var(--grid);transition:.1s}
|
||||
[data-theme^="ajean"] button:hover:not(:disabled),[data-theme^="ajean"] .addbtn:hover,[data-theme^="ajean"] .preset-add:hover{background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none;transform:translate(3px,3px)}
|
||||
[data-theme^="ajean"] .preset-edit,[data-theme^="ajean"] .copybtn{border:none;box-shadow:none;text-transform:none}
|
||||
[data-theme^="ajean"] .copybtn{border:2px solid var(--border)}
|
||||
[data-theme^="ajean"] #send{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] #menubtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
/* Messages : la question en carré bleu plein, la réponse en fiche blanche
|
||||
posée sur son ombre. Raisonnement et outils restent en pointillés. */
|
||||
[data-theme="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)}
|
||||
[data-theme="ajean"] #chat .msg.reasoning,[data-theme="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)}
|
||||
[data-theme="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em}
|
||||
[data-theme="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)}
|
||||
[data-theme="ajean"] .msg.assistant .body pre,[data-theme="ajean"] .msg.reasoning .body pre,[data-theme="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)}
|
||||
[data-theme="ajean"] .msg.typing span{background:var(--accent)}
|
||||
[data-theme^="ajean"] #chat .msg.user{background:var(--accent);color:#fff;box-shadow:4px 4px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #chat .msg.assistant{background:var(--panel);border:2px solid var(--border);box-shadow:4px 4px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] #chat .msg.reasoning,[data-theme^="ajean"] #chat .msg.tool{border:2px dashed var(--line2);background:var(--panel)}
|
||||
[data-theme^="ajean"] #chat .msg .label{color:var(--dim);letter-spacing:.14em}
|
||||
[data-theme^="ajean"] #chat .msg.user .label{color:rgba(255,255,255,.85)}
|
||||
[data-theme^="ajean"] .msg.assistant .body pre,[data-theme^="ajean"] .msg.reasoning .body pre,[data-theme^="ajean"] .msg.tool pre{border:2px solid var(--border);background:var(--code-bg)}
|
||||
[data-theme^="ajean"] .msg.typing span{background:var(--accent)}
|
||||
/* Panneaux, encadrés et champs : mêmes bordures épaisses, mêmes ombres. */
|
||||
[data-theme="ajean"] .preset,[data-theme="ajean"] .gated-block,[data-theme="ajean"] .tag,[data-theme="ajean"] .qtag,[data-theme="ajean"] .rtag,[data-theme="ajean"] .btag,[data-theme="ajean"] .statuspill,[data-theme="ajean"] .lc-mode{border-width:2px}
|
||||
[data-theme="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #input,[data-theme="ajean"] .mem-search,[data-theme="ajean"] .ask-input,[data-theme="ajean"] #modal input,[data-theme="ajean"] #modal textarea,[data-theme="ajean"] #modal select,[data-theme="ajean"] #theme-select{border:2px solid var(--line2);background:var(--panel)}
|
||||
[data-theme="ajean"] #input:focus,[data-theme="ajean"] .mem-search:focus,[data-theme="ajean"] .ask-input:focus{border-color:var(--accent)}
|
||||
[data-theme="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)}
|
||||
[data-theme^="ajean"] .preset,[data-theme^="ajean"] .gated-block,[data-theme^="ajean"] .tag,[data-theme^="ajean"] .qtag,[data-theme^="ajean"] .rtag,[data-theme^="ajean"] .btag,[data-theme^="ajean"] .statuspill,[data-theme^="ajean"] .lc-mode{border-width:2px}
|
||||
[data-theme^="ajean"] .preset.active{border-color:var(--accent);color:var(--accent);box-shadow:3px 3px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #input,[data-theme^="ajean"] .mem-search,[data-theme^="ajean"] .ask-input,[data-theme^="ajean"] #theme-select,[data-theme^="ajean"] .modal-card .ctl{border:2px solid var(--line2);background:var(--panel)}
|
||||
[data-theme^="ajean"] #input:focus,[data-theme^="ajean"] .mem-search:focus,[data-theme^="ajean"] .ask-input:focus{border-color:var(--accent)}
|
||||
[data-theme^="ajean"] #composer{border-top:2px solid var(--border);background:var(--panel)}
|
||||
/* Jauge de contexte : un vrai bandeau plein, pas un filet dégradé. */
|
||||
[data-theme="ajean"] #ctxtrack{height:5px;background:var(--grid)}
|
||||
[data-theme="ajean"] #ctx-fill{background:var(--accent)}
|
||||
[data-theme^="ajean"] #ctxtrack{height:5px;background:var(--grid)}
|
||||
[data-theme^="ajean"] #ctx-fill{background:var(--accent)}
|
||||
/* Fenêtres modales / info-bulles / toast : cadre net + ombre en bloc bleutée. */
|
||||
[data-theme="ajean"] .ask-box,[data-theme="ajean"] .help>.tip,[data-theme="ajean"] #toast,[data-theme="ajean"] .modal-box,[data-theme="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 #cddffb}
|
||||
[data-theme="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
[data-theme^="ajean"] .ask-box,[data-theme^="ajean"] .help>.tip,[data-theme^="ajean"] #toast,[data-theme^="ajean"] .modal-box,[data-theme^="ajean"] #modal>div{border:2px solid var(--line2);box-shadow:7px 7px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] #scrollbtn{border:2px solid var(--line2);box-shadow:3px 3px 0 0 var(--grid)}
|
||||
/* Interrupteurs : carrés, bleus quand ils sont actifs. */
|
||||
[data-theme="ajean"] .switch .slider{background:var(--line2)}
|
||||
[data-theme="ajean"] .switch input:checked + .slider{background:var(--accent)}
|
||||
[data-theme^="ajean"] .switch .slider{background:var(--line2)}
|
||||
[data-theme^="ajean"] .switch input:checked + .slider{background:var(--accent)}
|
||||
/* Pastille carrée : box-sizing explicite (un ::before n'hérite pas de la règle
|
||||
globale *{box-sizing:border-box}) — sans ça la bordure débordait du rail. */
|
||||
[data-theme="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:#fff;border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme="ajean"] .switch input:checked + .slider::before{border-color:#fff;background:var(--panel)}
|
||||
[data-theme^="ajean"] .switch .slider::before{box-sizing:border-box;width:14px;height:14px;background:var(--knob);border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme^="ajean"] .switch input:checked + .slider::before{border-color:var(--knob);background:var(--knob)}
|
||||
/* Étiquettes de section : capitales espacées, comme des en-têtes de terminal. */
|
||||
[data-theme="ajean"] .subhead,[data-theme="ajean"] .skillshead>span{letter-spacing:.16em}
|
||||
[data-theme^="ajean"] .subhead,[data-theme^="ajean"] .skillshead>span{letter-spacing:.16em}
|
||||
|
||||
/* ----- Thème AJEAN : modales (éditeur de preset, mémoire, MCP) -------------
|
||||
Les règles génériques du thème (bouton en capitales + ombre en bloc + décalage
|
||||
au survol) sont faites pour la barre latérale ; dans un formulaire dense elles
|
||||
rendaient l'éditeur de preset illisible : boutons énormes, ombres qui se
|
||||
chevauchent, champs collés au fond de la carte. On rejoue donc la modale à
|
||||
plat — hiérarchie nette, un seul niveau de relief (la carte). */
|
||||
[data-theme^="ajean"] .modal-ov{background:rgba(15,31,61,.45)}
|
||||
[data-theme^="ajean"] .modal-card{background:var(--panel);border:2px solid var(--line2);box-shadow:8px 8px 0 0 var(--sh)}
|
||||
[data-theme^="ajean"] .modal-head{border-bottom:2px solid var(--border);background:var(--soft,#e8f0ff)}
|
||||
[data-theme^="ajean"] .modal-head strong{text-transform:uppercase;letter-spacing:.14em;font-size:12px;color:var(--accent)}
|
||||
[data-theme^="ajean"] .modal-foot{border-top:2px solid var(--border)}
|
||||
/* Champs simples (mémoire, MCP…) : fond teinté pour se détacher de la carte. */
|
||||
[data-theme^="ajean"] .ctl{border:2px solid var(--line2);background:var(--bg);background-image:none}
|
||||
[data-theme^="ajean"] .ctl:focus{border-color:var(--accent)}
|
||||
[data-theme^="ajean"] .fld-l{color:var(--text);letter-spacing:.1em}
|
||||
[data-theme^="ajean"] .fld-hint,[data-theme^="ajean"] .seg .be-note{color:var(--dim)}
|
||||
[data-theme^="ajean"] .modal-body .card{border:2px solid var(--border);box-shadow:none;background:var(--panel)}
|
||||
[data-theme^="ajean"] .card-h{letter-spacing:.14em}
|
||||
|
||||
/* --- Éditeur de preset (listes façon Réglages) --- */
|
||||
[data-theme^="ajean"] .pe-body{background:var(--bg)}
|
||||
/* En-tête de groupe : petite étiquette bleue en capitales. */
|
||||
[data-theme^="ajean"] .pe-gh{color:var(--accent);text-transform:uppercase;letter-spacing:.14em;font-size:11px;font-weight:700}
|
||||
/* La LISTE porte le cadre ; les champs à l'intérieur n'en ont pas (sinon double
|
||||
encadrement). Séparateurs de rangées à la même épaisseur. */
|
||||
[data-theme^="ajean"] .pe-list,[data-theme^="ajean"] .pe-adv{border:2px solid var(--border);background:var(--panel)}
|
||||
[data-theme^="ajean"] .pe-row:not(:last-child){border-bottom:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .pe-adv[open]>summary{border-bottom:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .pe-val,[data-theme^="ajean"] .pe-selc select{border:none;background:transparent}
|
||||
/* Champs pleine largeur (URL, chemin…) : eux gardent un cadre, sur fond teinté. */
|
||||
[data-theme^="ajean"] .pe-inp{border:2px solid var(--line2);background:var(--bg)}
|
||||
[data-theme^="ajean"] .pe-inp:focus{border-color:var(--accent);box-shadow:none}
|
||||
[data-theme^="ajean"] .pe-row.stack.plain .pe-inp{border:none;background:transparent}
|
||||
[data-theme^="ajean"] .pe-inpbtn button{border:2px solid var(--line2);box-shadow:none;background:var(--panel);text-transform:none;letter-spacing:0}
|
||||
[data-theme^="ajean"] .pe-inpbtn .pe-inp{border-right:0}
|
||||
[data-theme^="ajean"] .pe-inpbtn button:hover:not(:disabled){transform:none;background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
/* Sélecteur segmenté : cases carrées côte à côte, bleu plein pour l'active. */
|
||||
[data-theme^="ajean"] .pe-seg{background:transparent;padding:0;gap:8px}
|
||||
[data-theme^="ajean"] .pe-seg>label{border:2px solid var(--border);background:var(--panel);font-weight:700;text-transform:uppercase;letter-spacing:.06em;font-size:11px;padding:9px 4px}
|
||||
[data-theme^="ajean"] .pe-seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff;box-shadow:none}
|
||||
/* Interrupteurs de la modale : carrés, comme ceux de la barre latérale. */
|
||||
[data-theme^="ajean"] .pe-sl{background:var(--line2)}
|
||||
[data-theme^="ajean"] .pe-sl::before{box-sizing:border-box;background:var(--knob);border:2px solid var(--line2);box-shadow:none}
|
||||
[data-theme^="ajean"] .pe-switch input:checked+.pe-sl{background:var(--accent)}
|
||||
[data-theme^="ajean"] .pe-switch input:checked+.pe-sl::before{border-color:var(--knob);background:var(--knob)}
|
||||
/* Boutons DANS la modale : mêmes capitales, mais posés à plat — pas d'ombre ni
|
||||
de décalage au survol, sinon les rangées de boutons dansent et se recouvrent. */
|
||||
[data-theme^="ajean"] .modal-card button{box-shadow:none;font-size:11.5px}
|
||||
[data-theme^="ajean"] .modal-card button:hover:not(:disabled){transform:none}
|
||||
[data-theme^="ajean"] .modal-card .btn-save{background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] .modal-card .btn-danger{background:var(--danger-bg);border-color:var(--err);color:var(--err)}
|
||||
[data-theme^="ajean"] .modal-card .btn-danger:hover{background:var(--err);border-color:var(--err);color:#fff}
|
||||
[data-theme^="ajean"] .modal-x{border:2px solid transparent;background:transparent;color:var(--dim)}
|
||||
[data-theme^="ajean"] .modal-x:hover{background:transparent;border-color:var(--line2);color:var(--text)}
|
||||
/* Champ + bouton accolés : les bordures se chevauchent au lieu de doubler. */
|
||||
[data-theme^="ajean"] .input-btn .btn-in{margin-left:-2px}
|
||||
/* Sélecteur segmenté : bleu plein quand c'est coché (le liseré seul se voyait mal). */
|
||||
[data-theme^="ajean"] .seg>label{border:2px solid var(--border);background:var(--bg)}
|
||||
[data-theme^="ajean"] .seg>label:has(input:checked){background:var(--accent);border-color:var(--accent);color:#fff}
|
||||
[data-theme^="ajean"] .seg>label:has(input:checked) .be-note{color:rgba(255,255,255,.85)}
|
||||
/* Listes (MCP, outils) : mêmes bordures épaisses, survol teinté. */
|
||||
[data-theme^="ajean"] .mcp-row,[data-theme^="ajean"] .mcp-tools-list,[data-theme^="ajean"] #lc-log{border:2px solid var(--border)}
|
||||
[data-theme^="ajean"] .mcp-tool-row:hover{background:var(--soft,#e8f0ff)}
|
||||
|
||||
/* Diff sur fond clair : mêmes rôles, teintes plus profondes pour rester lisibles. */
|
||||
[data-theme="light"],[data-theme="soft"],[data-theme="gpt"],[data-theme="ajean"]{
|
||||
--add:#1a7f37;--add-bg:rgba(26,127,55,.10);--del:#cf222e;--del-bg:rgba(207,34,46,.10)}
|
||||
[data-theme="soft-dark"],[data-theme="ajean-dark"]{
|
||||
--add:#4ac26b;--add-bg:rgba(74,194,107,.13);--del:#f85149;--del-bg:rgba(248,81,73,.13)}
|
||||
@@ -26,7 +26,7 @@ var webPrefsAllowed = map[string]map[string]bool{
|
||||
// listé ICI : sinon POST /api/prefs le rejette silencieusement, le serveur
|
||||
// conserve l'ancienne valeur et loadPrefs() la réapplique à chaque
|
||||
// rafraîchissement — le thème « saute » alors qu'il est bien en localStorage.
|
||||
"theme": {"ajean": true, "dark": true, "light": true, "soft": true, "soft-dark": true, "gpt": true},
|
||||
"theme": {"ajean": true, "ajean-dark": true, "dark": true, "light": true, "soft": true, "soft-dark": true, "gpt": true},
|
||||
"display": {"full": true, "simple": true},
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user