@@ -313,6 +335,48 @@ function toggleSide(){ document.getElementById('side').classList.toggle('open');
function saveSys(){ localStorage.setItem('jean.sys', document.getElementById('sysprompt').value); }
document.addEventListener('DOMContentLoaded', ()=>{ document.getElementById('sysprompt').value = localStorage.getItem('jean.sys') || ''; 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 →
+// bool, askPrompt → string|null (null si annulé), askAlert → void. Échap/clic dehors
+// = annuler ; Entrée = valider (sur prompt aussi).
+let _askResolver=null, _askKind='confirm';
+function askResolve(ok){
+ if(!_askResolver) return;
+ const r=_askResolver; _askResolver=null;
+ document.getElementById('ask-modal').style.display='none';
+ document.removeEventListener('keydown', _askKey, true);
+ if(_askKind==='prompt') r(ok ? document.getElementById('ask-input').value : null);
+ else r(ok);
+}
+function _askKey(e){
+ if(e.key==='Escape'){ e.preventDefault(); e.stopPropagation(); askResolve(false); }
+ 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
+ // {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||''; }
+ opts=opts||{};
+ _askKind=kind;
+ document.getElementById('ask-title').textContent = opts.title || (kind==='alert'?'Info':kind==='prompt'?'Saisie':'Confirmation');
+ document.getElementById('ask-msg').textContent = message||'';
+ const inp=document.getElementById('ask-input');
+ if(kind==='prompt'){ inp.style.display=''; inp.value=opts.default||''; inp.placeholder=opts.placeholder||''; }
+ else inp.style.display='none';
+ const cancel=document.getElementById('ask-cancel'), ok=document.getElementById('ask-ok');
+ cancel.style.display = kind==='alert' ? 'none' : '';
+ cancel.textContent = opts.cancelText || 'Annuler';
+ ok.textContent = opts.okText || (kind==='alert'?'OK':kind==='prompt'?'Valider':'Confirmer');
+ ok.classList.toggle('danger', !!opts.danger);
+ document.getElementById('ask-modal').style.display='flex';
+ document.addEventListener('keydown', _askKey, true);
+ setTimeout(()=>{ const f = kind==='prompt'?inp:ok; f.focus(); if(kind==='prompt') inp.select(); }, 30);
+ return new Promise(res=>{ _askResolver=res; });
+}
+function askConfirm(message,opts){ return _openAsk('confirm',message,opts); }
+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') || '';
@@ -326,7 +390,7 @@ async function jfetch(u, opts){
if(u.charAt(0) === '/') u = API_BASE + u;
let r = await fetch(u, opts);
if(r.status === 401){
- const k = prompt('Clé de pilotage jean requise :', '');
+ 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); }
}
return r;
@@ -466,7 +530,7 @@ function renderMemList(){
const loadMem = 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 (bash) et pourra lire/écrire sa mémoire.")){ document.getElementById('agent-toggle').checked=false; return; }
+ if(on && !await askConfirm("L'IA aura un accès shell complet au serveur (bash) et pourra lire/écrire sa mémoire.", {title:'Activer le mode agent ?', okText:'Activer', danger:true})){ document.getElementById('agent-toggle').checked=false; return; }
await jpost('/api/agent/toggle',{on});
loadAgent();
}
@@ -523,7 +587,7 @@ async function runBenchUI(){
}
}
async function switchTo(n,name){
- if(!confirm('Basculer vers "'+name+'" et redémarrer le service ?')) return;
+ if(!await askConfirm('Basculer vers « '+name+' » et redémarrer le service ?', {title:'Changer de preset', okText:'Basculer'})) return;
toast('switching…');
const r=await jpost('/api/switch',{n:n});
toast(r.ok?'switched':'erreur'); setTimeout(loadAll,2000);
@@ -681,9 +745,9 @@ async function delItem(){
const K = KINDS[editingKind];
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 + '" ?';
+ 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(!confirm(msg)) return;
+ if(!await askConfirm(msg, {title:'Suppression', okText:'Supprimer', danger:true})) return;
const payload = editingKind==='preset'
? {id: editingKey, deleteModel: delModel}
: {name: editingKey};
@@ -857,7 +921,7 @@ function resetChat(){ msgs=[]; document.getElementById('chat').innerHTML=''; sav
async function compactContext(){
if(busy) return;
if(!msgs.length){ toast('rien à compacter'); return; }
- if(!confirm('Compacter le contexte ? L\'IA va résumer la conversation puis repartir d\'une session propre basée sur ce résumé.')) return;
+ if(!await askConfirm('L\'IA va résumer la conversation puis repartir d\'une session propre basée sur ce résumé.', {title:'Compacter le contexte ?', okText:'Compacter'})) return;
busy=true;
const btn=document.getElementById('ctx-compact'); btn.textContent='compaction…'; btn.disabled=true;
const instruction='Résume de façon structurée et complète TOUTE notre conversation jusqu\'ici, dans le but de poursuivre le travail dans une nouvelle session sans rien perdre d\'important : objectifs, décisions prises, état actuel, fichiers/commandes/éléments clés, et ce qu\'il reste à faire. Écris uniquement le résumé, sans préambule.';