fix: le prompt systeme de l'UI marchait dans le vide depuis la conversation serveur (v0.4.x) - il est desormais persiste cote serveur (/api/sysprompt, JEAN_HOME/sysprompt.txt, partage entre appareils) et injecte a chaque tour de generation (fusionne avec le preambule agent par InjectSkills)

This commit is contained in:
nathaninline committed 2026-07-22 11:17:07 +02:00
1 parent d5e0606dcc
commit 1d2dba6501
6 files changed
+109 -5

No files matched your search

+9 -1
View File
@@ -177,8 +177,16 @@ func (c *Conversation) generate(ctx context.Context, caps Caps, temperature floa
c.appendDelta(map[string]any{"compacted": true})
}
// Prompt système personnalisé (UI → /api/sysprompt, fichier côté serveur).
// Injecté seulement dans la vue envoyée au modèle, jamais persisté dans
// c.Messages : modifiable à chaud, effet dès le tour suivant.
final := msgs
if sp := readSysPrompt(); sp != "" {
final = append([]Message{{Role: "system", Content: sp}}, msgs...)
}
var content strings.Builder
extra, _ := runChat(ctx, InjectSkills(msgs, caps), temperature, caps, func(ev StreamEvent) bool {
extra, _ := runChat(ctx, InjectSkills(final, caps), temperature, caps, func(ev StreamEvent) bool {
switch {
case ev.Err != nil:
c.appendDelta(map[string]any{"error": ev.Err.Error()})
+17 -2
View File
@@ -508,7 +508,22 @@ function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
let msgs = [];
let busy = false;
function toggleSide(){ document.getElementById('side').classList.toggle('open'); document.getElementById('backdrop').classList.toggle('open'); document.body.classList.toggle('drawer-open'); }
function saveSys(){ localStorage.setItem('jean.sys', document.getElementById('sysprompt').value); }
// Prompt système : persisté CÔTÉ SERVEUR (/api/sysprompt, partagé entre
// appareils, utilisé par la génération serveur). localStorage = simple cache
// d'affichage hors-ligne. Debounce : on n'écrit qu'après une pause de frappe.
let _sysT = null;
function saveSys(){
const v = document.getElementById('sysprompt').value;
localStorage.setItem('jean.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 || ''); }
}catch(e){}
}
// ===== Thèmes ================================================================
// 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
@@ -571,7 +586,7 @@ async function loadPrefs(){
}
}catch(e){}
}
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initDisplay(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; restoreChat(); });
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initDisplay(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
function toast(m){ const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),1800); }
// Modales natives (askConfirm/askPrompt/askAlert) — remplacent confirm()/prompt()/
// alert() par de vraies boîtes stylées. Chacune renvoie une Promise : askConfirm →
+16 -1
View File
@@ -18,4 +18,19 @@ function md(src){ return src ? marked.parse(fixMd(src)) : ''; }
let msgs = [];
let busy = false;
function toggleSide(){ document.getElementById('side').classList.toggle('open'); document.getElementById('backdrop').classList.toggle('open'); document.body.classList.toggle('drawer-open'); }
function saveSys(){ localStorage.setItem('jean.sys', document.getElementById('sysprompt').value); }
// Prompt système : persisté CÔTÉ SERVEUR (/api/sysprompt, partagé entre
// appareils, utilisé par la génération serveur). localStorage = simple cache
// d'affichage hors-ligne. Debounce : on n'écrit qu'après une pause de frappe.
let _sysT = null;
function saveSys(){
const v = document.getElementById('sysprompt').value;
localStorage.setItem('jean.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 || ''); }
}catch(e){}
}
+1 -1
View File
@@ -17,4 +17,4 @@ async function loadPrefs(){
}
}catch(e){}
}
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initDisplay(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; restoreChat(); });
document.addEventListener('DOMContentLoaded', ()=>{ initTheme(); initDisplay(); document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; loadSys(); restoreChat(); });
+1
View File
@@ -100,6 +100,7 @@ func newWebMux() *http.ServeMux {
api("/api/internet", handleInternet)
api("/api/memory", handleMemoryMode)
api("/api/prefs", handleWebPrefs)
api("/api/sysprompt", handleSysPrompt)
// Alias rétro-compat : l'ancien portail ajean.link (dépôt jean-relay) pilote
// encore l'agent via /api/tools* et /api/skills/toggle à travers le tunnel E2E.
// On les mappe sur le mode agent unifié le temps que le portail soit mis à jour.
+65
View File
@@ -0,0 +1,65 @@
// web_sysprompt.go — prompt système personnalisé de l'utilisateur, persisté
// CÔTÉ SERVEUR ($JEAN_HOME/sysprompt.txt) et partagé entre appareils, comme la
// conversation elle-même. Historique : avant la conversation serveur (v0.4.x),
// l'UI envoyait son prompt système dans chaque requête /api/chat ; depuis,
// /api/chat/send ne porte que le message → le champ de l'UI n'avait plus aucun
// effet. Il est maintenant lu ici par la génération (chat_conversation.go), et
// InjectSkills (llm_client.go) le fusionne avec le préambule agent.
package jean
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"strings"
)
func sysPromptPath() string { return filepath.Join(JeanHome(), "sysprompt.txt") }
// readSysPrompt renvoie le prompt système personnalisé ("" si absent).
func readSysPrompt() string {
b, err := os.ReadFile(sysPromptPath())
if err != nil {
return ""
}
return strings.TrimSpace(string(b))
}
func saveSysPrompt(text string) error {
text = strings.TrimSpace(text)
if text == "" {
err := os.Remove(sysPromptPath())
if os.IsNotExist(err) {
return nil
}
return err
}
return os.WriteFile(sysPromptPath(), []byte(text+"\n"), 0o644)
}
// handleSysPrompt :
//
// GET → {ok, text}
// POST {text} → enregistre ("" = efface) puis renvoie {ok, text}
func handleSysPrompt(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
sendJSON(w, 200, map[string]any{"ok": true, "text": readSysPrompt()})
case http.MethodPost:
var body struct {
Text string `json:"text"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
sendJSON(w, 400, map[string]any{"ok": false, "error": err.Error()})
return
}
if err := saveSysPrompt(body.Text); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true, "text": readSysPrompt()})
default:
sendJSON(w, 405, map[string]any{"ok": false, "error": "méthode non autorisée"})
}
}