mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
v0.8.0 : code mort supprime, UI et docs en ajean, release unique
- suppression des fonctions et types inutilises (staticcheck U1000) - sources UI, workflows, README passes en ajean - release.yml ne publie plus que les binaires ajean-* - notes de release redigees
This commit is contained in:
1 parent
62ad4155a2
commit
687ec81023
26 files changed
+123
-214
No files matched your search
@@ -100,9 +100,7 @@ func cmdServe(args []string) error {
|
||||
llmArgs = append(llmArgs, "--api-key", k)
|
||||
}
|
||||
// EXTRA_ARGS is appended verbatim, split on whitespace like the shell would.
|
||||
for _, a := range trimSplit(cfg["EXTRA_ARGS"], " ") {
|
||||
llmArgs = append(llmArgs, a)
|
||||
}
|
||||
llmArgs = append(llmArgs, trimSplit(cfg["EXTRA_ARGS"], " ")...)
|
||||
|
||||
// Working dir = AJEAN_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj
|
||||
// mmproj-F16.gguf) still resolve.
|
||||
|
||||
@@ -471,7 +471,6 @@ type searchResult struct {
|
||||
}
|
||||
|
||||
var (
|
||||
htmlTagRe = regexp.MustCompile(`<[^>]+>`)
|
||||
wsRe = regexp.MustCompile(`\s+`)
|
||||
numEntityRe = regexp.MustCompile(`&#(\d+);`)
|
||||
ddgHeadRe = regexp.MustCompile(`^##\s+\[([^\]]+)\]\(([^)]+)\)\s*$`)
|
||||
|
||||
@@ -368,23 +368,3 @@ func cmdMemory(args []string) error {
|
||||
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
|
||||
return nil
|
||||
}
|
||||
|
||||
// memorySystemPrompt liste les pages mémoire à injecter quand le mode agent est
|
||||
// actif, pour que l'IA sache ce qu'elle a déjà retenu.
|
||||
func memorySystemPrompt(caps Caps) string {
|
||||
if !caps.Agent {
|
||||
return ""
|
||||
}
|
||||
list := MemList()
|
||||
var b strings.Builder
|
||||
b.WriteString("Memory: persistent Markdown pages under MEMORY/. Use mem_search FIRST whenever the user asks about something you might already know (preferences, ongoing projects, past decisions). Open the most relevant page with mem_read. Save anything worth keeping across sessions with mem_add (one topic per page, descriptive kebab-case name); update a page with mem_edit (old must match exactly once). The first content line is a short title (#).\n")
|
||||
if len(list) == 0 {
|
||||
b.WriteString("Pages: none yet.")
|
||||
return b.String()
|
||||
}
|
||||
b.WriteString("Pages:\n")
|
||||
for _, p := range list {
|
||||
fmt.Fprintf(&b, "- %s: %s\n", p.Name, p.Title)
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
@@ -112,11 +112,6 @@ func (c MCPServerConfig) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mcpConfigFile est la représentation sur disque de mcp.json.
|
||||
type mcpConfigFile struct {
|
||||
MCPServers map[string]MCPServerConfig `json:"mcpServers"`
|
||||
}
|
||||
|
||||
// mcpConfigMu sérialise les accès concurrents à la déclaration des serveurs
|
||||
// MCP (l'UI web et les tours de chat peuvent lire/écrire en parallèle).
|
||||
var mcpConfigMu sync.Mutex
|
||||
|
||||
@@ -257,34 +257,3 @@ func writeE2EResp(w http.ResponseWriter, gcm cipher.AEAD, status int, body []byt
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte(base64.StdEncoding.EncodeToString(sealed)))
|
||||
}
|
||||
|
||||
// e2eSeal (côté agent : utilisé uniquement pour les tests) scelle un secret vers
|
||||
// une clé publique X25519. La version navigateur est en WASM (wasmclient).
|
||||
func e2eSeal(agentPub, secret []byte) ([]byte, error) {
|
||||
pub, err := ecdh.X25519().NewPublicKey(agentPub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eph, err := ecdh.X25519().GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shared, err := eph.ECDH(pub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := sealKey(shared, eph.PublicKey().Bytes(), agentPub)
|
||||
gcm, err := newGCM(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, 12)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct := gcm.Seal(nil, nonce, secret, nil)
|
||||
out := append([]byte{}, eph.PublicKey().Bytes()...)
|
||||
out = append(out, nonce...)
|
||||
out = append(out, ct...)
|
||||
return out, nil
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.7.13"
|
||||
const Version = "0.8.0"
|
||||
|
||||
// Main est le vrai main() du binaire (cmd/ajean ne fait que l'appeler).
|
||||
func Main() {
|
||||
|
||||
@@ -32,7 +32,7 @@ var glyphRects = [][4]float64{{6, 3, 2, 2}, {6, 5, 2, 2}, {4, 7, 2, 2}}
|
||||
|
||||
// brandIconImage dessine l'icône à la taille n. bg peint le carré arrondi, fg le
|
||||
// « j ». Les deux peuvent être transparents : c'est ce qui produit l'icône
|
||||
// « template » de macOS (voir brandTemplatePNG).
|
||||
// « template » de macOS.
|
||||
func brandIconImage(n int, bg, fg color.RGBA) *image.RGBA {
|
||||
const r = 2.0 // rayon des coins, en unités de la grille 12
|
||||
outside := func(gx, gy float64) bool {
|
||||
@@ -84,14 +84,6 @@ func encodePNG(img *image.RGBA) []byte {
|
||||
// Exporté pour le générateur d'icône du .exe (tools/gen-icon).
|
||||
func BrandIconPNG(n int) []byte { return encodePNG(brandIconImage(n, brandBlack, brandWhite)) }
|
||||
|
||||
// brandTemplatePNG rend la variante « template » attendue par macOS : seule la
|
||||
// couche alpha compte, le système colore la forme selon le thème de la barre de
|
||||
// menus. Le « j » est donc DÉCOUPÉ (transparent) dans un carré opaque — sans ça,
|
||||
// une icône entièrement noire disparaît sur une barre de menus sombre.
|
||||
func brandTemplatePNG(n int) []byte {
|
||||
return encodePNG(brandIconImage(n, brandBlack, brandClear))
|
||||
}
|
||||
|
||||
// BrandICO emballe une ou plusieurs tailles PNG dans un conteneur .ico. Windows
|
||||
// accepte le PNG comme image d'une entrée .ico (alpha conservé pour les coins
|
||||
// arrondis). Plusieurs tailles = un rendu net partout, de la barre des tâches
|
||||
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultAjeanHome is the data root when neither $AJEAN_HOME/$AJEAN_HOME nor
|
||||
// /etc/default/* override it. legacyDefaultHome est son équivalent d'avant le
|
||||
// renommage, migré au premier lancement (voir sys_migrate.go).
|
||||
// defaultAjeanHome est la racine des données quand ni $AJEAN_HOME ni
|
||||
// /etc/default/ajean n'en imposent une.
|
||||
//
|
||||
// Linux : /etc/ajean (machine de prod, service systemd lancé par root).
|
||||
// macOS : /etc/ajean n'est PAS écrivable par une app lancée depuis le Finder
|
||||
@@ -27,8 +26,6 @@ import (
|
||||
// un `sudo ajean install` a déjà créé /etc/ajean à notre nom.
|
||||
func defaultAjeanHome() string { return unixHome("ajean") }
|
||||
|
||||
func legacyDefaultHome() string { return unixHome("ajean") }
|
||||
|
||||
func unixHome(name string) string {
|
||||
if runtime.GOOS != "darwin" || os.Geteuid() == 0 {
|
||||
return "/etc/" + name
|
||||
|
||||
@@ -54,12 +54,9 @@ func homeRoot() string {
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
// defaultAjeanHome is the data root when no AJEAN_HOME/AJEAN_HOME is set.
|
||||
// defaultAjeanHome est la racine des données quand $AJEAN_HOME est absent.
|
||||
func defaultAjeanHome() string { return filepath.Join(homeRoot(), "ajean") }
|
||||
|
||||
// legacyDefaultHome is the pre-rename data root, migrated on first run.
|
||||
func legacyDefaultHome() string { return filepath.Join(homeRoot(), "ajean") }
|
||||
|
||||
// defaultEditor is used by `ajean edit` when $EDITOR is unset.
|
||||
func defaultEditor() string { return "notepad" }
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
||||
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
||||
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
||||
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<script>try{var t=localStorage.getItem('ajean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('ajean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<style>
|
||||
/* ===== Palette =====================================================
|
||||
Un seul thème (« Clean ») : noir et blanc, sobre. Deux variantes — claire par
|
||||
@@ -172,7 +172,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
.ask-ok.danger{border-color:var(--err);color:var(--err)}
|
||||
#brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px}
|
||||
/* Mode agent : le A passe en couleur d'accent (il était masqué hors agent,
|
||||
la marque se lisant alors « JEAN »). L'état agent reste signalé par sa
|
||||
la marque se lisant alors « AJEAN »). L'état agent reste signalé par sa
|
||||
pastille dans les réglages. */
|
||||
#brand.agent #brand-a{color:var(--accent)}
|
||||
/* interrupteur (switch) */
|
||||
@@ -961,7 +961,7 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
/* ===== Mise en page de la conversation =============================
|
||||
Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond),
|
||||
question de l'utilisateur en pastille arrondie à droite, composer en
|
||||
« pilule », pas d'étiquettes VOUS/JEAN. Ces règles surchargent la mise en
|
||||
« pilule », pas d'étiquettes VOUS/AJEAN. Ces règles surchargent la mise en
|
||||
page générique plus haut : elles doivent donc rester APRÈS elle. */
|
||||
body{font-size:15px}
|
||||
h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
@@ -975,7 +975,7 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
/* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */
|
||||
#chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6}
|
||||
#chat .msg.user .label{color:var(--dim)}
|
||||
/* Étiquettes VOUS / JEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
/* Étiquettes VOUS / AJEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
||||
#chat .msg .label{display:none}
|
||||
#chat .msg.collapsible .label{display:block}
|
||||
@@ -1227,7 +1227,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
trois îlots pour un même réglage. -->
|
||||
<div class="slist">
|
||||
<div class="rowbody">
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>jean</code></div>
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>ajean</code></div>
|
||||
<div class="row" style="margin:0">
|
||||
<input id="oai-url" readonly class="io-input">
|
||||
<button class="iconbtn" onclick="copyText(document.getElementById('oai-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
|
||||
@@ -1257,7 +1257,7 @@ button:hover{border-color:var(--dim);color:var(--text)}
|
||||
</div></div>
|
||||
</details>
|
||||
<details id="remote-details"><summary><span class="stitle">Accès distant</span><span id="remote-badge" class="sbadge"></span></summary>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur AJEAN depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
||||
<div id="remote-off" class="slist"><div class="rowbody">
|
||||
<p class="muted" style="margin:0 0 8px">Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.</p>
|
||||
@@ -1764,14 +1764,14 @@ function toggleSide(){ document.getElementById('side').classList.toggle('open');
|
||||
let _sysT = null;
|
||||
function saveSys(){
|
||||
const v = document.getElementById('sysprompt').value;
|
||||
localStorage.setItem('jean.sys', v);
|
||||
localStorage.setItem('ajean.sys', v);
|
||||
clearTimeout(_sysT);
|
||||
_sysT = setTimeout(()=>{ jpost('/api/sysprompt', {text:v}).catch(()=>{}); }, 600);
|
||||
}
|
||||
async function loadSys(){
|
||||
try{
|
||||
const d = await jget('/api/sysprompt');
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('jean.sys', d.text || ''); }
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('ajean.sys', d.text || ''); }
|
||||
}catch(e){}
|
||||
}
|
||||
// ===== Apparence =============================================================
|
||||
@@ -1785,7 +1785,7 @@ function applyTheme(id){
|
||||
// désormais la variante, déduite du suffixe.
|
||||
const dark = id==='dark' || (typeof id==='string' && id.endsWith('-dark'));
|
||||
document.documentElement.setAttribute('data-theme', dark?'dark':'light');
|
||||
try{ localStorage.setItem('jean-theme', dark?'dark':'light'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-theme', dark?'dark':'light'); }catch(e){}
|
||||
const sw=document.getElementById('theme-dark'); if(sw) sw.checked=dark;
|
||||
}
|
||||
function toggleThemeDark(){
|
||||
@@ -1793,7 +1793,7 @@ function toggleThemeDark(){
|
||||
savePrefs();
|
||||
}
|
||||
function initTheme(){
|
||||
let id='light'; try{ id=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
let id='light'; try{ id=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
applyTheme(id);
|
||||
}
|
||||
// ===== Affichage ============================================================
|
||||
@@ -1810,7 +1810,7 @@ const VIEW_OPTS=[
|
||||
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
||||
function applyView(id, on){
|
||||
document.documentElement.setAttribute('data-'+id, on?'1':'0');
|
||||
try{ localStorage.setItem('jean-'+id, on?'1':'0'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-'+id, on?'1':'0'); }catch(e){}
|
||||
const box=document.getElementById('view-'+id); if(box) box.checked=!!on;
|
||||
// La barre latérale peut rester ouverte quand on repasse en mode fixe.
|
||||
if(id==='hide-side' && !on){
|
||||
@@ -1822,18 +1822,18 @@ function applyView(id, on){
|
||||
function toggleView(id){ applyView(id, document.getElementById('view-'+id).checked); savePrefs(); }
|
||||
function initView(){
|
||||
VIEW_OPTS.forEach(o=>{
|
||||
let v=null; try{ v=localStorage.getItem('jean-'+o.id); }catch(e){}
|
||||
let v=null; try{ v=localStorage.getItem('ajean-'+o.id); }catch(e){}
|
||||
applyView(o.id, v==='1');
|
||||
});
|
||||
}
|
||||
// ===== Persistance côté serveur (partagée entre appareils) ==================
|
||||
// L'apparence est aussi enregistrée sur la machine jean (/api/prefs) : ainsi le
|
||||
// L'apparence est aussi enregistrée sur la machine ajean (/api/prefs) : ainsi le
|
||||
// thème/affichage choisi sur un appareil se retrouve sur tous les autres. Le
|
||||
// localStorage reste utilisé pour appliquer instantanément au chargement (sans
|
||||
// flash), puis loadPrefs() aligne sur la valeur du serveur (source de vérité).
|
||||
function savePrefs(){
|
||||
let theme='light';
|
||||
try{ theme=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
try{ theme=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
const p={theme};
|
||||
VIEW_OPTS.forEach(o=>{ p[o.id.replace('-','_')] = viewOn(o.id)?'1':'0'; });
|
||||
jpost('/api/prefs', p).catch(()=>{});
|
||||
@@ -1855,7 +1855,7 @@ async function loadPrefs(){
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('ajean.sys') || ''; loadSys(); restoreChat(); });
|
||||
// Pastille d'état d'une section du menu (mode agent, internet, MCP, distant).
|
||||
// `state` : true = actif (pilule pleine), false = inactif (contour), 'warn'/'err'
|
||||
// = état signalé, null = pastille masquée. Un seul endroit décide de leur allure.
|
||||
@@ -1907,7 +1907,7 @@ function _askKey(e){
|
||||
else if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); askResolve(true); }
|
||||
}
|
||||
function _openAsk(kind, message, opts){
|
||||
// Compat 2 conventions : (message, opts) [UI jean] ET l'objet unique
|
||||
// Compat 2 conventions : (message, opts) [UI ajean] ET l'objet unique
|
||||
// {title,msg,yes,no,placeholder} passé par e2e.js sur ajean.link (server.html,
|
||||
// où cette fonction remplace le window.askConfirm du bootstrap boîte noire).
|
||||
if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; }
|
||||
@@ -1939,14 +1939,14 @@ function askPrompt(message,opts){ return _openAsk('prompt',message,opts); }
|
||||
function askAlert(message,opts){ return _openAsk('alert',message,opts); }
|
||||
// Clé de pilotage : envoyée en Authorization: Bearer sur chaque appel /api/*.
|
||||
// Sur un 401 on la (re)demande et on rejoue la requête. Stockée en localStorage.
|
||||
let TOKEN = localStorage.getItem('jean.key') || '';
|
||||
let TOKEN = localStorage.getItem('ajean.key') || '';
|
||||
function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorization']='Bearer '+TOKEN; return h; }
|
||||
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
||||
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
||||
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
||||
// Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le
|
||||
// navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint
|
||||
// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS :
|
||||
// dès qu'on laisse traîner plusieurs onglets AJEAN) reste suspendue POUR TOUJOURS :
|
||||
// ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher.
|
||||
// Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal
|
||||
// et ne sont donc jamais concernés.
|
||||
@@ -1969,8 +1969,8 @@ async function jfetch(u, opts){
|
||||
}
|
||||
finally{ if(timer) clearTimeout(timer); }
|
||||
if(r.status === 401){
|
||||
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
}
|
||||
return r;
|
||||
}
|
||||
@@ -2154,7 +2154,7 @@ const HRESERVE=['cfg','vram','ram-details','presets','internet-status','mcp-list
|
||||
function reserveHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
try{
|
||||
const h=parseInt(localStorage.getItem('jean-h-'+id)||'0',10);
|
||||
const h=parseInt(localStorage.getItem('ajean-h-'+id)||'0',10);
|
||||
const el=document.getElementById(id);
|
||||
if(el && h>0) el.style.minHeight=h+'px';
|
||||
}catch(e){}
|
||||
@@ -2164,7 +2164,7 @@ function releaseHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
const el=document.getElementById(id); if(!el) return;
|
||||
el.style.minHeight='';
|
||||
try{ localStorage.setItem('jean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', reserveHeights);
|
||||
@@ -2243,7 +2243,7 @@ async function loadPresets(){
|
||||
});
|
||||
}
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « ajean » → « ajean ».
|
||||
async function loadAgent(){
|
||||
const s=await jget('/api/agent');
|
||||
const on = s.enabled;
|
||||
@@ -2662,7 +2662,7 @@ async function populateModelPicker(){
|
||||
const list = await jget('/api/models');
|
||||
const cur = currentModelInTextarea();
|
||||
// Les modèles peuvent venir de plusieurs dossiers (disque externe…) : on les
|
||||
// regroupe par dossier, JEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
// regroupe par dossier, AJEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
const groups = [];
|
||||
for(const m of (list||[])){
|
||||
let g = groups.find(x => x.dir === m.dir);
|
||||
@@ -2681,7 +2681,7 @@ async function populateModelPicker(){
|
||||
opts += '<option value="'+escHtml(m.value)+'"'+on+'>'+escHtml(m.name)+' ('+fmtSize(m.size)+')</option>';
|
||||
}
|
||||
html += groups.length > 1
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier jean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier ajean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
: opts;
|
||||
}
|
||||
// MODEL pointe vers un fichier qu'on ne trouve dans aucun dossier déclaré :
|
||||
@@ -2692,7 +2692,7 @@ async function populateModelPicker(){
|
||||
sel.innerHTML = html;
|
||||
if(cur && !matched) setModelDirsOpen(true);
|
||||
}
|
||||
// ---- Dossiers de modèles : JEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// ---- Dossiers de modèles : AJEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// Réglage rare : replié derrière une ligne, comme l'éditeur de .env brut.
|
||||
function setModelDirsOpen(open){
|
||||
const b = document.getElementById('m-dirs-body');
|
||||
@@ -2721,7 +2721,7 @@ async function populateModelDirs(){
|
||||
info.className = 'muted';
|
||||
const n = x.count >= 0 ? (x.count + ' modèle' + (x.count>1?'s':'')) : 'illisible';
|
||||
const free = x.free >= 0 ? ', ' + fmtSize(x.free) + ' libres' : '';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier jean' : '') + ')';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier ajean' : '') + ')';
|
||||
row.append(p, info);
|
||||
if(!x.home){
|
||||
const del = document.createElement('button');
|
||||
@@ -2792,13 +2792,13 @@ function underDir(p, dir){
|
||||
}
|
||||
let beFastPath = '', beOptPath = '', beFastDir = '';
|
||||
async function populateBackend(){
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier jean).
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier ajean).
|
||||
let lc = {}; try{ lc = await jget('/api/llamacpp'); }catch(_){}
|
||||
beFastPath = (lc.prebuilt && lc.prebuilt.bin) || '';
|
||||
beFastDir = (lc.prebuilt && lc.prebuilt.dir) || '';
|
||||
beOptPath = lc.bin || '';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de jean (l'utilisateur peut y déposer son propre build).
|
||||
// le dossier backends de ajean (l'utilisateur peut y déposer son propre build).
|
||||
const detected = await jget('/api/backends');
|
||||
const sel = document.getElementById('m-backend-detected');
|
||||
let html = '<option value="">— ou choisir un backend détecté —</option>';
|
||||
@@ -3132,7 +3132,7 @@ function populateSettings(){
|
||||
// --- Décodage spéculatif (--spec-type / --spec-draft-n-max) ----------------
|
||||
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
|
||||
// par des virgules ; une valeur composée (ou un type sorti après cette version
|
||||
// de jean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// de ajean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// au premier changement. On l'ajoute donc telle quelle à la liste plutôt que de
|
||||
// la perdre.
|
||||
function setSpecType(v){
|
||||
@@ -3328,7 +3328,7 @@ function watchDownload(fname){
|
||||
e.bar.className = 'pe-bar done'; e.bar.firstElementChild.style.width = '100%';
|
||||
stop();
|
||||
await Promise.all([populateModelPicker(), populateDlDirs()]);
|
||||
// Le modèle a pu atterrir hors du dossier jean : l'option porte alors le
|
||||
// Le modèle a pu atterrir hors du dossier ajean : l'option porte alors le
|
||||
// chemin complet, pas le simple nom de fichier.
|
||||
const sel = document.getElementById('m-model');
|
||||
const opt = Array.from(sel.options).find(o => samePath(baseName(o.value), fname));
|
||||
@@ -3483,7 +3483,7 @@ function setLabelCounts(el, add, del){
|
||||
if(del) cnt.appendChild(Object.assign(document.createElement('span'),{className:'d',textContent:'-'+del}));
|
||||
lab.appendChild(cnt);
|
||||
}
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/AJEAN
|
||||
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
|
||||
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
|
||||
// par la préférence d'affichage « masquer la vitesse de génération » (CSS).
|
||||
@@ -3644,7 +3644,7 @@ async function compactContext(){
|
||||
}
|
||||
// Persistance de la conversation : on garde user+assistant en localStorage pour
|
||||
// survivre à un refresh (les bulles tool/reasoning sont éphémères, non stockées).
|
||||
function saveChat(){ try{ localStorage.setItem('jean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
function saveChat(){ try{ localStorage.setItem('ajean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
// Source de vérité = SERVEUR. Au chargement on ouvre le flux d'abonnement
|
||||
// permanent (connectStream), qui rejoue tout le fil depuis le serveur — texte,
|
||||
// appels d'outils, vitesses, raisonnement — puis suit le direct. Plus de
|
||||
@@ -3723,7 +3723,7 @@ function autoGrow(ta){
|
||||
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
|
||||
// L'historique et la génération vivent sur le serveur ajean. Le client ouvre un
|
||||
// flux d'ABONNEMENT permanent (SSE) qui rejoue le journal depuis lastSeq puis
|
||||
// suit le direct. Fermer l'onglet n'arrête plus la génération (détachée côté
|
||||
// serveur) ; se reconnecter rejoue tout le fil, détails compris.
|
||||
@@ -3921,7 +3921,7 @@ function handleDelta(d){
|
||||
}
|
||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||
// re-téléchargement complet après une coupure / bascule d'appareil).
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet AJEAN laissé
|
||||
// ouvert monopolise une des ~6 connexions simultanées autorisées par domaine :
|
||||
// au-delà, toute requête (journal, installation du moteur…) reste en file
|
||||
// d'attente sans jamais partir ni échouer — un blocage silencieux très
|
||||
@@ -4477,7 +4477,7 @@ async function deleteMcp(){
|
||||
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
|
||||
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
|
||||
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
|
||||
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
|
||||
// l'agent local (/api/link/connect) qui fait le `ajean link` (token + service).
|
||||
|
||||
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
garde `display-mode` en second pour les PWA Android/desktop à manifeste. -->
|
||||
<script>try{var pwa=(window.navigator.standalone===true)||(window.matchMedia&&window.matchMedia('(display-mode:standalone)').matches)||(window.matchMedia&&window.matchMedia('(display-mode:fullscreen)').matches);document.documentElement.setAttribute('data-pwa',pwa?'1':'0');}catch(e){document.documentElement.setAttribute('data-pwa','0');}</script>
|
||||
<!-- Applique le thème AVANT le rendu (pas de flash) : lit le choix persisté. -->
|
||||
<script>try{var t=localStorage.getItem('jean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('jean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<script>try{var t=localStorage.getItem('ajean-theme')||'light';document.documentElement.setAttribute('data-theme',(t==='dark'||t.endsWith('-dark'))?'dark':'light');['hide-reasoning','hide-tools','fold-tools','hide-side','hide-stats'].forEach(function(k){document.documentElement.setAttribute('data-'+k,localStorage.getItem('ajean-'+k)==='1'?'1':'0')})}catch(e){document.documentElement.setAttribute('data-theme','light');}</script>
|
||||
<style>
|
||||
@@CSS@@
|
||||
</style></head><body>
|
||||
@@ -180,7 +180,7 @@
|
||||
trois îlots pour un même réglage. -->
|
||||
<div class="slist">
|
||||
<div class="rowbody">
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>jean</code></div>
|
||||
<div class="muted" style="margin:0 0 6px">Adresse locale (réseau) — modèle <code>ajean</code></div>
|
||||
<div class="row" style="margin:0">
|
||||
<input id="oai-url" readonly class="io-input">
|
||||
<button class="iconbtn" onclick="copyText(document.getElementById('oai-url').value,'adresse copiée')" title="copier"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2.5"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>
|
||||
@@ -210,7 +210,7 @@
|
||||
</div></div>
|
||||
</details>
|
||||
<details id="remote-details"><summary><span class="stitle">Accès distant</span><span id="remote-badge" class="sbadge"></span></summary>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur AJEAN depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
|
||||
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
|
||||
<div id="remote-off" class="slist"><div class="rowbody">
|
||||
<p class="muted" style="margin:0 0 8px">Ouvrez l'accès distant à ce serveur en quelques clics, sans terminal.</p>
|
||||
|
||||
@@ -56,13 +56,13 @@ function toggleSide(){ document.getElementById('side').classList.toggle('open');
|
||||
let _sysT = null;
|
||||
function saveSys(){
|
||||
const v = document.getElementById('sysprompt').value;
|
||||
localStorage.setItem('jean.sys', v);
|
||||
localStorage.setItem('ajean.sys', v);
|
||||
clearTimeout(_sysT);
|
||||
_sysT = setTimeout(()=>{ jpost('/api/sysprompt', {text:v}).catch(()=>{}); }, 600);
|
||||
}
|
||||
async function loadSys(){
|
||||
try{
|
||||
const d = await jget('/api/sysprompt');
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('jean.sys', d.text || ''); }
|
||||
if(d && d.ok){ document.getElementById('sysprompt').value = d.text || ''; localStorage.setItem('ajean.sys', d.text || ''); }
|
||||
}catch(e){}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ function applyTheme(id){
|
||||
// désormais la variante, déduite du suffixe.
|
||||
const dark = id==='dark' || (typeof id==='string' && id.endsWith('-dark'));
|
||||
document.documentElement.setAttribute('data-theme', dark?'dark':'light');
|
||||
try{ localStorage.setItem('jean-theme', dark?'dark':'light'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-theme', dark?'dark':'light'); }catch(e){}
|
||||
const sw=document.getElementById('theme-dark'); if(sw) sw.checked=dark;
|
||||
}
|
||||
function toggleThemeDark(){
|
||||
@@ -17,7 +17,7 @@ function toggleThemeDark(){
|
||||
savePrefs();
|
||||
}
|
||||
function initTheme(){
|
||||
let id='light'; try{ id=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
let id='light'; try{ id=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
applyTheme(id);
|
||||
}
|
||||
// ===== Affichage ============================================================
|
||||
@@ -34,7 +34,7 @@ const VIEW_OPTS=[
|
||||
function viewOn(id){ return document.documentElement.getAttribute('data-'+id)==='1'; }
|
||||
function applyView(id, on){
|
||||
document.documentElement.setAttribute('data-'+id, on?'1':'0');
|
||||
try{ localStorage.setItem('jean-'+id, on?'1':'0'); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-'+id, on?'1':'0'); }catch(e){}
|
||||
const box=document.getElementById('view-'+id); if(box) box.checked=!!on;
|
||||
// La barre latérale peut rester ouverte quand on repasse en mode fixe.
|
||||
if(id==='hide-side' && !on){
|
||||
@@ -46,7 +46,7 @@ function applyView(id, on){
|
||||
function toggleView(id){ applyView(id, document.getElementById('view-'+id).checked); savePrefs(); }
|
||||
function initView(){
|
||||
VIEW_OPTS.forEach(o=>{
|
||||
let v=null; try{ v=localStorage.getItem('jean-'+o.id); }catch(e){}
|
||||
let v=null; try{ v=localStorage.getItem('ajean-'+o.id); }catch(e){}
|
||||
applyView(o.id, v==='1');
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
// ===== Persistance côté serveur (partagée entre appareils) ==================
|
||||
// L'apparence est aussi enregistrée sur la machine jean (/api/prefs) : ainsi le
|
||||
// L'apparence est aussi enregistrée sur la machine ajean (/api/prefs) : ainsi le
|
||||
// thème/affichage choisi sur un appareil se retrouve sur tous les autres. Le
|
||||
// localStorage reste utilisé pour appliquer instantanément au chargement (sans
|
||||
// flash), puis loadPrefs() aligne sur la valeur du serveur (source de vérité).
|
||||
function savePrefs(){
|
||||
let theme='light';
|
||||
try{ theme=localStorage.getItem('jean-theme')||'light'; }catch(e){}
|
||||
try{ theme=localStorage.getItem('ajean-theme')||'light'; }catch(e){}
|
||||
const p={theme};
|
||||
VIEW_OPTS.forEach(o=>{ p[o.id.replace('-','_')] = viewOn(o.id)?'1':'0'; });
|
||||
jpost('/api/prefs', p).catch(()=>{});
|
||||
@@ -27,4 +27,4 @@ async function loadPrefs(){
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
|
||||
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initView(); document.getElementById('sysprompt').value = localStorage.getItem('ajean.sys') || ''; loadSys(); restoreChat(); });
|
||||
@@ -49,7 +49,7 @@ function _askKey(e){
|
||||
else if(e.key==='Enter'){ e.preventDefault(); e.stopPropagation(); askResolve(true); }
|
||||
}
|
||||
function _openAsk(kind, message, opts){
|
||||
// Compat 2 conventions : (message, opts) [UI jean] ET l'objet unique
|
||||
// Compat 2 conventions : (message, opts) [UI ajean] ET l'objet unique
|
||||
// {title,msg,yes,no,placeholder} passé par e2e.js sur ajean.link (server.html,
|
||||
// où cette fonction remplace le window.askConfirm du bootstrap boîte noire).
|
||||
if(message && typeof message==='object'){ const o=message; opts={title:o.title, okText:o.yes, cancelText:o.no, placeholder:o.placeholder, default:o.default, danger:o.danger}; message=o.msg||''; }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Clé de pilotage : envoyée en Authorization: Bearer sur chaque appel /api/*.
|
||||
// Sur un 401 on la (re)demande et on rejoue la requête. Stockée en localStorage.
|
||||
let TOKEN = localStorage.getItem('jean.key') || '';
|
||||
let TOKEN = localStorage.getItem('ajean.key') || '';
|
||||
function authHeaders(h){ h = Object.assign({}, h||{}); if(TOKEN) h['Authorization']='Bearer '+TOKEN; return h; }
|
||||
// Base d'URL : "" en local (page à "/"), "/u/<id>" servie via le relais ajean.link.
|
||||
// Garde les chemins absolus (/api/…) corrects derrière le tunnel.
|
||||
const API_BASE = location.pathname.replace(/\/(index\.html)?$/, '');
|
||||
// Délai maximal d'un appel /api/* ordinaire. Sans lui, une requête que le
|
||||
// navigateur met en file d'attente (plafond de ~6 connexions par domaine, atteint
|
||||
// dès qu'on laisse traîner plusieurs onglets Jean) reste suspendue POUR TOUJOURS :
|
||||
// dès qu'on laisse traîner plusieurs onglets AJEAN) reste suspendue POUR TOUJOURS :
|
||||
// ni réponse, ni erreur, et une UI figée sur « chargement… » sans rien à afficher.
|
||||
// Mieux vaut une erreur franche. Les flux longs (SSE) passent leur propre signal
|
||||
// et ne sont donc jamais concernés.
|
||||
@@ -30,8 +30,8 @@ async function jfetch(u, opts){
|
||||
}
|
||||
finally{ if(timer) clearTimeout(timer); }
|
||||
if(r.status === 401){
|
||||
const k = await askPrompt('Clé de pilotage jean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('jean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
const k = await askPrompt('Clé de pilotage ajean requise :', {title:'Authentification', placeholder:'clé…'});
|
||||
if(k){ TOKEN = k.trim(); localStorage.setItem('ajean.key', TOKEN); opts.headers = authHeaders(opts.headers); r = await fetch(u, opts); }
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ const HRESERVE=['cfg','vram','ram-details','presets','internet-status','mcp-list
|
||||
function reserveHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
try{
|
||||
const h=parseInt(localStorage.getItem('jean-h-'+id)||'0',10);
|
||||
const h=parseInt(localStorage.getItem('ajean-h-'+id)||'0',10);
|
||||
const el=document.getElementById(id);
|
||||
if(el && h>0) el.style.minHeight=h+'px';
|
||||
}catch(e){}
|
||||
@@ -19,7 +19,7 @@ function releaseHeights(){
|
||||
HRESERVE.forEach(id=>{
|
||||
const el=document.getElementById(id); if(!el) return;
|
||||
el.style.minHeight='';
|
||||
try{ localStorage.setItem('jean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
try{ localStorage.setItem('ajean-h-'+id, String(el.offsetHeight||0)); }catch(e){}
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', reserveHeights);
|
||||
@@ -98,7 +98,7 @@ async function loadPresets(){
|
||||
});
|
||||
}
|
||||
// « Mode agent » = accès machine + skills réunis en un seul interrupteur.
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « jean » → « ajean ».
|
||||
// Quand il est actif, un « a » blanc apparaît en fondu devant « ajean » → « ajean ».
|
||||
async function loadAgent(){
|
||||
const s=await jget('/api/agent');
|
||||
const on = s.enabled;
|
||||
|
||||
@@ -194,7 +194,7 @@ async function populateModelPicker(){
|
||||
const list = await jget('/api/models');
|
||||
const cur = currentModelInTextarea();
|
||||
// Les modèles peuvent venir de plusieurs dossiers (disque externe…) : on les
|
||||
// regroupe par dossier, JEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
// regroupe par dossier, AJEAN_HOME d'abord (l'API les renvoie dans cet ordre).
|
||||
const groups = [];
|
||||
for(const m of (list||[])){
|
||||
let g = groups.find(x => x.dir === m.dir);
|
||||
@@ -213,7 +213,7 @@ async function populateModelPicker(){
|
||||
opts += '<option value="'+escHtml(m.value)+'"'+on+'>'+escHtml(m.name)+' ('+fmtSize(m.size)+')</option>';
|
||||
}
|
||||
html += groups.length > 1
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier jean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
? '<optgroup label="'+escHtml(g.home ? 'dossier ajean' : g.dir)+'">'+opts+'</optgroup>'
|
||||
: opts;
|
||||
}
|
||||
// MODEL pointe vers un fichier qu'on ne trouve dans aucun dossier déclaré :
|
||||
@@ -224,7 +224,7 @@ async function populateModelPicker(){
|
||||
sel.innerHTML = html;
|
||||
if(cur && !matched) setModelDirsOpen(true);
|
||||
}
|
||||
// ---- Dossiers de modèles : JEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// ---- Dossiers de modèles : AJEAN_HOME + dossiers ajoutés (disque externe…) ---
|
||||
// Réglage rare : replié derrière une ligne, comme l'éditeur de .env brut.
|
||||
function setModelDirsOpen(open){
|
||||
const b = document.getElementById('m-dirs-body');
|
||||
@@ -253,7 +253,7 @@ async function populateModelDirs(){
|
||||
info.className = 'muted';
|
||||
const n = x.count >= 0 ? (x.count + ' modèle' + (x.count>1?'s':'')) : 'illisible';
|
||||
const free = x.free >= 0 ? ', ' + fmtSize(x.free) + ' libres' : '';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier jean' : '') + ')';
|
||||
info.textContent = '(' + n + free + (x.home ? ', dossier ajean' : '') + ')';
|
||||
row.append(p, info);
|
||||
if(!x.home){
|
||||
const del = document.createElement('button');
|
||||
@@ -324,13 +324,13 @@ function underDir(p, dir){
|
||||
}
|
||||
let beFastPath = '', beOptPath = '', beFastDir = '';
|
||||
async function populateBackend(){
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier jean).
|
||||
// Chemins des deux moteurs gérés + liste des backends détectés (dossier ajean).
|
||||
let lc = {}; try{ lc = await jget('/api/llamacpp'); }catch(_){}
|
||||
beFastPath = (lc.prebuilt && lc.prebuilt.bin) || '';
|
||||
beFastDir = (lc.prebuilt && lc.prebuilt.dir) || '';
|
||||
beOptPath = lc.bin || '';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de jean (l'utilisateur peut y déposer son propre build).
|
||||
// le dossier backends de ajean (l'utilisateur peut y déposer son propre build).
|
||||
const detected = await jget('/api/backends');
|
||||
const sel = document.getElementById('m-backend-detected');
|
||||
let html = '<option value="">— ou choisir un backend détecté —</option>';
|
||||
@@ -664,7 +664,7 @@ function populateSettings(){
|
||||
// --- Décodage spéculatif (--spec-type / --spec-draft-n-max) ----------------
|
||||
// Sélectionne le type courant. --spec-type accepte en réalité une LISTE séparée
|
||||
// par des virgules ; une valeur composée (ou un type sorti après cette version
|
||||
// de jean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// de ajean) ne correspondrait à aucune option et serait silencieusement effacée
|
||||
// au premier changement. On l'ajoute donc telle quelle à la liste plutôt que de
|
||||
// la perdre.
|
||||
function setSpecType(v){
|
||||
@@ -860,7 +860,7 @@ function watchDownload(fname){
|
||||
e.bar.className = 'pe-bar done'; e.bar.firstElementChild.style.width = '100%';
|
||||
stop();
|
||||
await Promise.all([populateModelPicker(), populateDlDirs()]);
|
||||
// Le modèle a pu atterrir hors du dossier jean : l'option porte alors le
|
||||
// Le modèle a pu atterrir hors du dossier ajean : l'option porte alors le
|
||||
// chemin complet, pas le simple nom de fichier.
|
||||
const sel = document.getElementById('m-model');
|
||||
const opt = Array.from(sel.options).find(o => samePath(baseName(o.value), fname));
|
||||
|
||||
@@ -115,7 +115,7 @@ function setLabelCounts(el, add, del){
|
||||
if(del) cnt.appendChild(Object.assign(document.createElement('span'),{className:'d',textContent:'-'+del}));
|
||||
lab.appendChild(cnt);
|
||||
}
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/JEAN
|
||||
// Ligne de mesures sous une réponse (prefill / decode). Les étiquettes VOUS/AJEAN
|
||||
// sont masquées dans cette mise en page, donc les chiffres qu'on y écrivait
|
||||
// avaient disparu : ils ont leur propre ligne, discrète, sous le texte. Masquée
|
||||
// par la préférence d'affichage « masquer la vitesse de génération » (CSS).
|
||||
@@ -276,7 +276,7 @@ async function compactContext(){
|
||||
}
|
||||
// Persistance de la conversation : on garde user+assistant en localStorage pour
|
||||
// survivre à un refresh (les bulles tool/reasoning sont éphémères, non stockées).
|
||||
function saveChat(){ try{ localStorage.setItem('jean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
function saveChat(){ try{ localStorage.setItem('ajean.chat', JSON.stringify(msgs)); }catch(e){} }
|
||||
// Source de vérité = SERVEUR. Au chargement on ouvre le flux d'abonnement
|
||||
// permanent (connectStream), qui rejoue tout le fil depuis le serveur — texte,
|
||||
// appels d'outils, vitesses, raisonnement — puis suit le direct. Plus de
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ===== 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
|
||||
// L'historique et la génération vivent sur le serveur ajean. Le client ouvre un
|
||||
// flux d'ABONNEMENT permanent (SSE) qui rejoue le journal depuis lastSeq puis
|
||||
// suit le direct. Fermer l'onglet n'arrête plus la génération (détachée côté
|
||||
// serveur) ; se reconnecter rejoue tout le fil, détails compris.
|
||||
@@ -197,7 +197,7 @@ function handleDelta(d){
|
||||
}
|
||||
// Flux d'abonnement permanent + reconnexion auto (from=lastSeq → pas de
|
||||
// re-téléchargement complet après une coupure / bascule d'appareil).
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet Jean laissé
|
||||
// Un onglet caché RELÂCHE son flux SSE. Sans ça, chaque onglet AJEAN laissé
|
||||
// ouvert monopolise une des ~6 connexions simultanées autorisées par domaine :
|
||||
// au-delà, toute requête (journal, installation du moteur…) reste en file
|
||||
// d'attente sans jamais partir ni échouer — un blocage silencieux très
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
|
||||
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
|
||||
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
|
||||
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
|
||||
// l'agent local (/api/link/connect) qui fait le `ajean link` (token + service).
|
||||
|
||||
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
.ask-ok.danger{border-color:var(--err);color:var(--err)}
|
||||
#brand-a{display:inline-flex;line-height:0;color:var(--text);margin-right:2px}
|
||||
/* Mode agent : le A passe en couleur d'accent (il était masqué hors agent,
|
||||
la marque se lisant alors « JEAN »). L'état agent reste signalé par sa
|
||||
la marque se lisant alors « AJEAN »). L'état agent reste signalé par sa
|
||||
pastille dans les réglages. */
|
||||
#brand.agent #brand-a{color:var(--accent)}
|
||||
/* interrupteur (switch) */
|
||||
@@ -935,7 +935,7 @@ textarea.sctl{min-height:76px;resize:vertical;line-height:1.5}
|
||||
/* ===== Mise en page de la conversation =============================
|
||||
Colonne de lecture centrée, réponses SANS bulle (texte nu sur le fond),
|
||||
question de l'utilisateur en pastille arrondie à droite, composer en
|
||||
« pilule », pas d'étiquettes VOUS/JEAN. Ces règles surchargent la mise en
|
||||
« pilule », pas d'étiquettes VOUS/AJEAN. Ces règles surchargent la mise en
|
||||
page générique plus haut : elles doivent donc rester APRÈS elle. */
|
||||
body{font-size:15px}
|
||||
h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
@@ -949,7 +949,7 @@ h1{color:var(--text);font-weight:600;letter-spacing:-.01em}
|
||||
/* Question : pastille arrondie à droite, texte lisible sur fond clair OU sombre. */
|
||||
#chat .msg.user{background:var(--bubble);color:var(--text);border-radius:20px;padding:11px 18px;max-width:75%;line-height:1.6}
|
||||
#chat .msg.user .label{color:var(--dim)}
|
||||
/* Étiquettes VOUS / JEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
/* Étiquettes VOUS / AJEAN : inutiles ici (l'alignement suffit), sauf pour les
|
||||
bulles repliables (raisonnement / outils) où le label est le bouton de repli. */
|
||||
#chat .msg .label{display:none}
|
||||
#chat .msg.collapsible .label{display:block}
|
||||
|
||||
Reference in new issue
Block a user