mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
v0.2.14 : PWA/UI mobile améliorée (plein écran standalone, gestion de l'encoche/safe-area, interface compacte en paysage, plus de zoom iOS à la saisie) + pastille d'état service (chargement/prêt) + gestion propre des erreurs de stream
This commit is contained in:
1 parent
adcc6548f6
commit
5931bd72a6
7 files changed
+87
-13
No files matched your search
@@ -15,7 +15,7 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const Version = "0.2.13"
|
||||
const Version = "0.2.14"
|
||||
|
||||
func main() {
|
||||
// Migration one-shot des anciens skills (SKILLS/<nom>/SKILL.md) vers la
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -53,6 +53,7 @@ func baseSystemPrompt(caps Caps) string {
|
||||
b.WriteString("- " + l + "\n")
|
||||
}
|
||||
b.WriteString("\nFor anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, then answer. Never end your turn after only thinking. Be concise.\n")
|
||||
b.WriteString("Before answering any question about yourself or this machine, always call mem_search first — even trivial-seeming ones. Testing with a tool never replaces this: memory may hold context the tool won't reveal. Search memory, then verify, then answer.\n")
|
||||
b.WriteString("\nDate: " + time.Now().Format("2006-01-02"))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
+75
-9
@@ -1,18 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="fr"><head>
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||||
<!-- PWA standalone : "Ajouter à l'écran d'accueil" sur iPhone → app plein écran
|
||||
SANS la barre d'onglets Safari (indispensable pour enregistrer des démos). -->
|
||||
<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="ajean">
|
||||
<title>jean</title>
|
||||
<style>
|
||||
:root{--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--dim:#7d8590;--accent:#58a6ff;--ok:#3fb950;--warn:#d29922;--err:#f85149;--mag:#bc8cff}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;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
|
||||
pas passer sous l'encoche / la barre système / l'indicateur d'accueil. Les
|
||||
env(safe-area-inset-*) valent 0 hors PWA, donc aucun impact ailleurs. */
|
||||
body{padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}
|
||||
.side{width:320px;border-right:1px solid var(--border);padding:16px;overflow-y:auto;flex-shrink:0;scrollbar-width:none;-ms-overflow-style:none}
|
||||
.side::-webkit-scrollbar{display:none}
|
||||
.main{flex:1;display:flex;flex-direction:column;min-width:0}
|
||||
#menubtn{display:none;position:fixed;top:10px;left:10px;z-index:20;background:var(--panel);border:1px solid var(--border);color:var(--text);width:40px;height:40px;border-radius:6px;font-size:18px;cursor:pointer}
|
||||
#menubtn{display:none;position:fixed;top:calc(10px + env(safe-area-inset-top));left:calc(10px + env(safe-area-inset-left));z-index:20;background:var(--panel);border:1px solid var(--border);color:var(--text);width:40px;height:40px;border-radius:6px;font-size:18px;cursor:pointer}
|
||||
#backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:10}
|
||||
@media (max-width:720px){
|
||||
.side{position:fixed;inset:0 auto 0 0;width:80%;max-width:320px;z-index:15;transform:translateX(-100%);transition:transform .2s;background:var(--bg)}
|
||||
.side{position:fixed;inset:0 auto 0 0;width:80%;max-width:320px;z-index:15;transform:translateX(-100%);transition:transform .2s;background:var(--bg);padding-top:calc(16px + env(safe-area-inset-top));padding-left:calc(16px + env(safe-area-inset-left))}
|
||||
.side.open{transform:translateX(0)}
|
||||
#menubtn,#backdrop.open{display:block}
|
||||
body.drawer-open #menubtn{display:none}
|
||||
@@ -37,6 +47,9 @@ h1{margin:0;font-size:18px;color:var(--accent)}
|
||||
.statuspill{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:3px 10px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--dim);font-weight:600;white-space:nowrap}
|
||||
.statuspill.ok{color:var(--ok);border-color:rgba(63,185,80,.45);background:rgba(63,185,80,.10)}
|
||||
.statuspill.err{color:var(--err);border-color:rgba(248,81,73,.45);background:rgba(248,81,73,.10)}
|
||||
.statuspill.loading{color:var(--warn);border-color:rgba(210,153,34,.45);background:rgba(210,153,34,.10)}
|
||||
.statuspill.loading .dot{animation:pulse 1s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
.statuspill .dot{width:7px;height:7px;border-radius:50%;background:currentColor}
|
||||
.statuspill .port{opacity:.6;font-weight:400}
|
||||
/* en-tête + bouton d'ajout de skill (toujours visible) */
|
||||
@@ -115,6 +128,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
.msg.user{align-self:flex-end;background:#1f6feb;color:#fff}
|
||||
.msg.user .label{color:rgba(255,255,255,.8)}
|
||||
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border)}
|
||||
.msg.assistant.errmsg{border-color:rgba(248,81,73,.55);background:rgba(248,81,73,.08);color:var(--err)}
|
||||
.msg .body>*{margin:1em 0}
|
||||
.msg .body>*:first-child{margin-top:0}
|
||||
.msg .body>*:last-child{margin-bottom:0}
|
||||
@@ -183,6 +197,35 @@ button:disabled{opacity:.5;cursor:not-allowed}
|
||||
@media (hover:none) and (pointer:coarse){
|
||||
#input,#sysprompt,#modal input,#modal textarea,#modal select{font-size:16px}
|
||||
}
|
||||
/* Appareil TACTILE en PAYSAGE : on réduit nettement tout pour gagner de la place
|
||||
(interface « un peu grosse » en horizontal, utile pour enregistrer des démos).
|
||||
Détection par (hover:none)+(pointer:coarse)+landscape : infaillible sur tout
|
||||
téléphone couché, sans dépendre d'un seuil de largeur/hauteur en pixels (les
|
||||
iPhone Pro Max font 932px de large et ~430px de haut, ce qui piégeait les
|
||||
seuils précédents). Le desktop (souris) n'est jamais touché.
|
||||
NB : la règle iOS 16px anti-zoom plus haut est surchargée ici volontairement
|
||||
pour #input — on garde une saisie compacte (le pinch-zoom manuel reste dispo). */
|
||||
@media (hover:none) and (pointer:coarse) and (orientation:landscape){
|
||||
body{font-size:11.5px}
|
||||
/* Tiroir latéral : on décale le TIROIR ENTIER après l'encoche (left = inset)
|
||||
au lieu de padder à l'intérieur — sinon grosse bordure vide à gauche qui
|
||||
rétrécit le contenu. Padding interne normal → largeur utile pleine. */
|
||||
.side{width:300px;max-width:300px;left:env(safe-area-inset-left);padding:calc(14px + env(safe-area-inset-top)) 14px 14px 14px}
|
||||
#chat{padding:48px 10px 6px;gap:7px}
|
||||
.msg{padding:6px 10px;border-radius:8px;max-width:78%}
|
||||
#chat .msg.assistant,#chat .msg.reasoning{max-width:88%}
|
||||
#inputbar{padding:5px 8px;gap:6px}
|
||||
/* PAS de font-size < 16px ici : iOS Safari zoome à la sélection du champ. On
|
||||
garde 16px (hérité de la règle anti-zoom) et on compacte via hauteur/padding. */
|
||||
#input{min-height:34px;max-height:120px;padding:6px 9px}
|
||||
#send,#stop{padding:0 14px}
|
||||
#composer .muted{display:none}
|
||||
#ctxmeta{padding:3px 10px 0}
|
||||
#ctx-text{font-size:9px}
|
||||
h1{font-size:15px}
|
||||
h2,details>summary{font-size:10px}
|
||||
#menubtn{width:32px;height:32px;font-size:15px;top:calc(6px + env(safe-area-inset-top))}
|
||||
}
|
||||
</style></head><body>
|
||||
<button id="menubtn" onclick="toggleSide()">☰</button>
|
||||
<div id="backdrop" onclick="toggleSide()"></div>
|
||||
@@ -400,13 +443,19 @@ async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Conte
|
||||
async function loadStatus(){
|
||||
const s=await jget('/api/status');
|
||||
const el=document.getElementById('status-svc');
|
||||
el.className='statuspill '+(s.active?'ok':'err');
|
||||
el.innerHTML='<span class="dot"></span>'+(s.active?'active':s.state)+' <span class="port">:'+s.port+'</span>';
|
||||
// Trois états : service coupé (err) · service actif mais modèle pas encore
|
||||
// chargé (loading, llama-server renvoie 503) · modèle prêt (ok).
|
||||
let cls='err', txt=s.state;
|
||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||
else if(s.active){ cls='loading'; txt='chargement…'; }
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt+' <span class="port">:'+s.port+'</span>';
|
||||
MODEL_READY = !!(s.active && s.health);
|
||||
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
||||
}
|
||||
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
||||
// du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter.
|
||||
let CTX_MAX=0, CTX_USED=0;
|
||||
let CTX_MAX=0, CTX_USED=0, MODEL_READY=false;
|
||||
function setCtxUsed(n){ CTX_USED=n||0; updateCtxMeter(); }
|
||||
function updateCtxMeter(){
|
||||
if(!CTX_MAX) return;
|
||||
@@ -993,7 +1042,7 @@ async function send(){
|
||||
let reasonEl=null, contentEl=null, pendingToolEl=null, fullContent='', fullReason='', toolMsgs=null;
|
||||
let turnCollapsibles=[]; // bulles reasoning/tool à replier dès la réponse finale
|
||||
let tokCount=0, reasonTokCount=0, t0=performance.now(), firstTok=0, statsTimer=null;
|
||||
let serverStats=null;
|
||||
let serverStats=null, streamErr=false;
|
||||
const updateStats=(final)=>{
|
||||
if(!contentEl && !reasonEl) return;
|
||||
const el = contentEl || reasonEl;
|
||||
@@ -1038,6 +1087,19 @@ async function send(){
|
||||
try{
|
||||
const o=JSON.parse(data);
|
||||
const d=(o.choices&&o.choices[0]&&o.choices[0].delta)||{};
|
||||
if(d.error){
|
||||
// Erreur serveur (ex : modèle en cours de chargement → 503). On
|
||||
// l'affiche, on retire le message user de l'historique et on le
|
||||
// remet dans l'input pour renvoyer d'un clic une fois prêt.
|
||||
contentEl=null; reasonEl=null;
|
||||
const eb=addMsg('assistant',''); eb.classList.add('errmsg');
|
||||
renderBody(eb, d.error);
|
||||
if(msgs.length && msgs[msgs.length-1].role==='user'){ ta.value=msgs[msgs.length-1].content; msgs.pop(); }
|
||||
saveChat();
|
||||
streamErr=true;
|
||||
try{ reader.cancel(); }catch(_){}
|
||||
break;
|
||||
}
|
||||
if(d.stats){ serverStats = d.stats; }
|
||||
// Tool-turn messages (assistant tool_calls + tool results) sent at the
|
||||
// end of the stream. On les garde pour les réinjecter dans msgs AVANT
|
||||
@@ -1087,10 +1149,14 @@ async function send(){
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
if(streamErr) break;
|
||||
}
|
||||
if(streamErr) break;
|
||||
}
|
||||
if(!streamErr){
|
||||
if(toolMsgs) msgs.push(...toolMsgs);
|
||||
msgs.push({role:'assistant',content:fullContent}); saveChat();
|
||||
}
|
||||
if(toolMsgs) msgs.push(...toolMsgs);
|
||||
msgs.push({role:'assistant',content:fullContent}); saveChat();
|
||||
// Le KV cache s'accumule : chaque tour ajoute les tokens NOUVELLEMENT traités
|
||||
// (prompt_tokens, le préfixe caché n'est pas recompté) + ceux générés. La
|
||||
// taille réelle du contexte = somme cumulée, donc monotone croissante.
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 13,
|
||||
"Patch": 14,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 13,
|
||||
"Patch": 14,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -25,7 +25,7 @@
|
||||
"LegalCopyright": "Copyright (c) 2026 Jean contributors. MIT License.",
|
||||
"OriginalFilename": "jean.exe",
|
||||
"ProductName": "Jean",
|
||||
"ProductVersion": "0.2.13",
|
||||
"ProductVersion": "0.2.14",
|
||||
"Comments": "https://github.com/nathaninline/jean — projet open source (MIT)"
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
@@ -678,6 +678,13 @@ func sseHeartbeat(w http.ResponseWriter, flusher http.Flusher) (*sync.Mutex, fun
|
||||
// runChatStream exécute le chat et pousse chaque événement (delta) via emit, qui
|
||||
// renvoie false pour interrompre. Partagé par handleChat et handleE2EChat.
|
||||
func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any) bool) {
|
||||
// Garde-fou : si le modèle n'est pas encore chargé, llama-server répond 503
|
||||
// ("loading model") et le tour partirait dans le vide (aucune réponse, l'user
|
||||
// renvoie en boucle). On renvoie une erreur explicite affichée dans le chat.
|
||||
if !healthCheck() {
|
||||
emit(map[string]any{"error": "⏳ Le modèle est encore en train de charger — patiente quelques secondes puis renvoie ton message."})
|
||||
return
|
||||
}
|
||||
if body.Temperature == 0 {
|
||||
body.Temperature = 0.7
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user