v0.2.10 : mode agent unifie (un seul toggle = shell + skills) + jeu de mot 'ajean' + refonte du header (pastille d'etat, preset, espacements)

This commit is contained in:
nathaninline committed 2026-06-26 12:09:43 +02:00
1 parent 175bbd34d9
commit d3a2f1c428
11 files changed
+199 -207

No files matched your search

+62 -40
View File
@@ -30,10 +30,34 @@ body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;background
.preset-meta .qtag,.preset-meta .rtag,.preset-meta .btag{font-size:11px;padding:2px 9px;border-radius:9px}
.preset-meta .qtag svg,.preset-meta .rtag svg,.preset-meta .btag svg{width:11px;height:11px}
}
h1{margin:0 0 4px;font-size:18px;color:var(--accent)}
h1{margin:0;font-size:18px;color:var(--accent)}
#brand{display:inline-flex;align-items:baseline}
.sidehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:18px}
/* pastille d'état service, en haut à droite de la barre latérale */
.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 .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) */
.skillshead{display:flex;align-items:center;justify-content:space-between;margin:12px 0 5px}
.skillshead>span{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.08em;font-weight:600}
.addbtn{font-size:11px;padding:3px 11px 4px;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;line-height:1.2}
.addbtn:hover{border-color:var(--accent);color:var(--accent)}
#brand-a{display:inline-block;line-height:1;max-width:0;opacity:0;overflow:hidden;color:#fff;transition:max-width .35s ease,opacity .35s ease}
#brand.agent #brand-a{max-width:1ch;opacity:1;margin-right:.04em}
/* interrupteur (switch) */
.switchrow{display:flex;align-items:center;gap:9px;font-size:12px;padding:6px 0;cursor:pointer;user-select:none}
.switch{position:relative;display:inline-block;width:36px;height:20px;flex:none}
.switch input{position:absolute;opacity:0;width:0;height:0}
.switch .slider{position:absolute;inset:0;background:var(--border);border-radius:999px;transition:background .25s ease}
.switch .slider::before{content:"";position:absolute;width:14px;height:14px;left:3px;top:3px;background:#fff;border-radius:50%;transition:transform .25s ease;box-shadow:0 1px 2px rgba(0,0,0,.4)}
.switch input:checked + .slider{background:var(--accent)}
.switch input:checked + .slider::before{transform:translateX(16px)}
.switch input:focus-visible + .slider{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
h2{margin:20px 0 8px;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600}
details{margin:8px 0;border-top:1px solid var(--border);padding-top:6px}
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:4px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
details{margin:7px 0;border-top:1px solid var(--border);padding-top:4px}
details>summary{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-weight:600;cursor:pointer;padding:3px 0;list-style:none;display:flex;justify-content:space-between;align-items:center;user-select:none}
details>summary::-webkit-details-marker{display:none}
details>summary::after{content:"▸";transition:transform .15s;color:var(--dim);font-size:10px}
details[open]>summary::after{transform:rotate(90deg)}
@@ -72,6 +96,7 @@ button:disabled{opacity:.5;cursor:not-allowed}
.msg.assistant,.msg.reasoning{white-space:normal}
.msg .body{overflow-wrap:anywhere;word-break:break-word}
.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 .body>*{margin:1em 0}
.msg .body>*:first-child{margin-top:0}
@@ -145,8 +170,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
<button id="menubtn" onclick="toggleSide()">☰</button>
<div id="backdrop" onclick="toggleSide()"></div>
<div class="side" id="side">
<h1>jean</h1>
<div id="statusline" class="muted"><span id="status-svc">…</span> <span id="status-preset"></span></div>
<div class="sidehead">
<h1 id="brand"><span id="brand-a">a</span>jean</h1>
<span id="status-svc" class="statuspill">…</span>
</div>
<details open><summary>GPU</summary>
<div id="vram" class="muted">…</div>
</details>
@@ -156,12 +183,10 @@ button:disabled{opacity:.5;cursor:not-allowed}
<details><summary>Presets<button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openPreset('')" title="ajouter">+</button></summary>
<div id="presets"></div>
</details>
<details><summary>Accès machine <span id="tools-badge" style="margin-left:auto;font-size:10px"></span></summary>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="tools-toggle" onchange="toggleTools()"> activer l'accès machine</label>
<div class="muted" style="font-size:11px">⚠️ donne à l'IA un accès shell complet au serveur — elle peut inspecter, lancer des scripts et tout faire via le terminal (timeout 30s, max 300s)</div>
</details>
<details><summary>Skills <span id="skills-badge" style="margin-left:auto;font-size:10px"></span><button class="preset-add" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter">+</button></summary>
<label style="display:flex;align-items:center;gap:6px;font-size:12px;padding:4px 0"><input type="checkbox" id="skills-toggle" onchange="toggleSkills()"> activer</label>
<details><summary>Mode agent <span id="agent-badge" style="margin-left:auto;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</label>
<div class="muted" style="font-size:11px">⚠️ active l'<b>accès machine</b> (shell complet sur le serveur) <b>et</b> les <b>skills</b> — l'IA peut inspecter, lancer des scripts et exécuter ses skills (timeout 30s, max 300s)</div>
<div class="skillshead"><span>Skills</span><button class="addbtn" onclick="event.preventDefault();event.stopPropagation();openSkill('')" title="ajouter un skill">+ ajouter</button></div>
<div id="skills-list" class="muted" style="font-size:12px"></div>
</details>
<details><summary>System prompt</summary>
@@ -188,8 +213,11 @@ button:disabled{opacity:.5;cursor:not-allowed}
<div id="composer">
<div id="ctxtrack"><div id="ctx-fill"></div></div>
<div id="ctxmeta">
<span id="ctx-text" class="muted">contexte 0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
<span id="status-preset" class="muted" style="font-size:10px;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
<span style="display:flex;align-items:center;gap:10px;margin-left:auto">
<span id="ctx-text" class="muted">contexte 0 / 0</span>
<button id="ctx-compact" onclick="compactContext()">compacter</button>
</span>
</div>
<div id="inputbar">
<textarea id="input" placeholder="message…" onkeydown="onKey(event)"></textarea>
@@ -298,8 +326,9 @@ async function jget(u){ const r=await jfetch(u); return r.json(); }
async function jpost(u,b){ const r=await jfetch(u,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})}); return r.json(); }
async function loadStatus(){
const s=await jget('/api/status');
const t=s.active?'<span class="tag ok">● active</span>':'<span class="tag err">○ '+s.state+'</span>';
document.getElementById('status-svc').innerHTML = t + ' <span class="muted">:'+s.port+'</span>';
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>';
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
}
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
@@ -338,12 +367,8 @@ async function loadPresets(){
// Build via DOM (not string concat) so preset names can contain anything —
// spaces, accents, quotes, < > & — without breaking markup or handlers.
const sp = document.getElementById('status-preset');
sp.innerHTML='';
if(act){
const tag=document.createElement('span');
tag.className='tag'; tag.style.borderColor='var(--accent)'; tag.style.color='var(--accent)';
tag.textContent=act.name; sp.appendChild(tag);
}
sp.textContent = act ? act.name : '';
sp.title = act ? 'preset actif' : '';
const cont=document.getElementById('presets');
cont.innerHTML='';
if(!p.length){ cont.innerHTML='<span class="muted">(aucun)</span>'; return; }
@@ -383,31 +408,28 @@ async function loadPresets(){
cont.appendChild(row);
});
}
async function loadSkills(){
const s=await jget('/api/skills');
document.getElementById('skills-toggle').checked = s.enabled;
document.getElementById('skills-badge').innerHTML = s.enabled
// « 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 ».
async function loadAgent(){
const s=await jget('/api/agent');
const on = s.enabled;
document.getElementById('agent-toggle').checked = on;
document.getElementById('agent-badge').innerHTML = on
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
document.getElementById('brand').classList.toggle('agent', on);
document.getElementById('skills-list').innerHTML = (s.skills||[]).length
? s.skills.map(x=>'<div class="preset" style="font-size:12px" onclick="openSkill(\''+x.name+'\')"><span><b style="color:var(--text)">'+x.name+'</b> <span class="muted">— '+x.desc+'</span></span><button onclick="event.stopPropagation();openSkill(\''+x.name+'\')" style="margin:0;padding:2px 8px;font-size:11px">edit</button></div>').join('')
: '<div class="muted">(aucun skill)</div>';
}
async function toggleSkills(){
const on=document.getElementById('skills-toggle').checked;
await jpost('/api/skills/toggle',{on}); loadSkills();
// alias : plusieurs appelants rafraîchissent juste la liste des skills
const loadSkills = loadAgent;
async function toggleAgent(){
const on=document.getElementById('agent-toggle').checked;
if(on && !confirm("Activer le mode agent ? L'IA aura un accès shell complet au serveur et pourra exécuter ses skills.")){ document.getElementById('agent-toggle').checked=false; return; }
await jpost('/api/agent/toggle',{on});
loadAgent();
}
async function loadTools(){
const t=await jget('/api/tools');
document.getElementById('tools-toggle').checked = t.enabled;
document.getElementById('tools-badge').innerHTML = t.enabled
? '<span class="tag ok">on</span>' : '<span class="tag" style="opacity:.6">off</span>';
}
async function toggleTools(){
const on=document.getElementById('tools-toggle').checked;
if(on && !confirm("Activer l'accès machine ? L'IA aura un accès shell complet au serveur.")){ document.getElementById('tools-toggle').checked=false; return; }
await jpost('/api/tools/toggle',{on}); loadTools();
}
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadSkills(),loadTools()]); }
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadCfg(),loadPresets(),loadAgent()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
function openBenchModal(){ document.getElementById('bench-modal').style.display = 'flex'; }
function closeBenchModal(){ document.getElementById('bench-modal').style.display = 'none'; }