154 lines
11 KiB
JavaScript
154 lines
11 KiB
JavaScript
/* DONA_SUITE_V9: all model and user text inserted as textContent, never innerHTML. */
|
||
(()=>{
|
||
'use strict';
|
||
if(window.DonaSuiteV9)return;
|
||
const $=id=>document.getElementById(id);
|
||
const el=(tag,cls,text)=>{const a=document.createElement(tag);if(cls)a.className=cls;if(text!==undefined)a.textContent=String(text);return a;};
|
||
const btn=(label,callback,cls='dv9-button')=>{const b=el('button',cls,label);b.type='button';b.addEventListener('click',callback);return b;};
|
||
let opened=false,current=null,lastPrompt='';
|
||
const status=t=>{if($('dv9Status'))$('dv9Status').textContent=String(t||'');};
|
||
async function api(url,body){
|
||
const r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(body)});
|
||
const j=await r.json().catch(()=>({}));if(!r.ok||j.ok===false)throw Error(j.error||'Service indisponible');return j;
|
||
}
|
||
async function get(url){const r=await fetch(url,{credentials:'same-origin'});if(!r.ok)throw Error('Service indisponible');return r.json();}
|
||
function open(){opened=true;$('donaSuiteV9').hidden=false;document.body.classList.add('dv9-open');loadPins();}
|
||
function close(){opened=false;$('donaSuiteV9').hidden=true;document.body.classList.remove('dv9-open');}
|
||
function section(title,subtitle){const c=el('article','dv9-card');c.append(el('h3','',title));if(subtitle)c.append(el('p','dv9-muted',subtitle));$('dv9Results').append(c);return c;}
|
||
function paragraph(host,text){host.append(el('p','dv9-body',text));}
|
||
function list(host,items){const ul=el('ul','dv9-list');items.forEach(x=>ul.append(el('li','',x)));host.append(ul);}
|
||
function recipeCard(rec){
|
||
const c=section(rec.title,(rec.minutes||'?')+' min');
|
||
const cols=el('div','dv9-columns');
|
||
const ingredients=el('div');ingredients.append(el('h4','','Ingrédients'));list(ingredients,rec.ingredients||[]);
|
||
if(rec.matched?.length)paragraph(ingredients,'Déjà disponibles : '+rec.matched.join(', '));
|
||
if(rec.missing?.length)paragraph(ingredients,'À prévoir : '+rec.missing.join(', '));
|
||
const prep=el('div');prep.append(el('h4','','Préparation'));const ol=el('ol','dv9-list');(rec.steps||[]).forEach(x=>ol.append(el('li','',x)));prep.append(ol);
|
||
cols.append(ingredients,prep);c.append(cols);
|
||
}
|
||
function render(result,prompt){
|
||
if(!result)return;current=result;lastPrompt=prompt||lastPrompt||'';open();
|
||
$('dv9ResultTitle').textContent=result.title||'Résultat';$('dv9Results').replaceChildren();status(result.source||'');
|
||
const kind=result.kind;
|
||
if(kind==='error'){section('Erreur',result.error);return;}
|
||
if(kind==='recipe'){
|
||
(result.recipes||[]).forEach(recipeCard);
|
||
if(!result.recipes?.length)section('Pas de recette locale','Essaie une recette plus générale ou une liste d’ingrédients.');
|
||
}else if(kind==='menu'){
|
||
showMenu(result.menu);
|
||
}else if(kind==='briefing'||kind==='daily'){
|
||
const b=result.briefing||{};
|
||
const c=section('Agenda · '+(b.date||''),b.days+' jour(s)');
|
||
if(b.events?.length)list(c,b.events.map(x=>(x.start?.slice(0,16).replace('T',' ')||'')+' · '+x.title+(x.location?' · '+x.location:'')));
|
||
else paragraph(c,'Aucun événement trouvé pour la période, ou agenda indisponible.');
|
||
const w=b.weather;
|
||
if(w)section('Météo · Home Assistant',w.description+(w.temperature!==null&&w.temperature!==undefined?' · '+w.temperature+' '+w.unit:''));
|
||
(b.errors||[]).forEach(x=>section('Source indisponible',x));
|
||
if(b.suggested_menu)showMenu(b.suggested_menu);
|
||
}else if(kind==='memories'){
|
||
const h=section('Souvenirs enregistrés','Enregistrés uniquement sur demande.');
|
||
if(!result.memories?.length)paragraph(h,'Aucun souvenir.');
|
||
(result.memories||[]).forEach(item=>{
|
||
const row=el('div','dv9-memory');row.append(el('span','',item.text),btn('Effacer',async()=>{
|
||
if(!confirm('Supprimer ce souvenir de Dona ?'))return;
|
||
try{await api('/api/dona/v9/memory/delete',{id:item.id});await ask('Dona, montre mes souvenirs');}catch(e){status(e.message);}
|
||
},'dv9-danger'));h.append(row);
|
||
});
|
||
}else if(kind==='documents'){
|
||
const d=result.documents||{};
|
||
section('Recherche locale',d.note);
|
||
(d.found||[]).forEach(x=>{const c=section(x.file,'Extrait '+x.section);paragraph(c,x.snippet);});
|
||
}else if(kind==='routine'){
|
||
const r=result.routine||{};const c=section(r.name,r.message);
|
||
if(r.lights?.length)list(c,r.lights);
|
||
if(r.executable)c.append(btn('Confirmer et éteindre ces lumières',async()=>{
|
||
if(!confirm('Éteindre ces lumières Home Assistant maintenant ?'))return;
|
||
try{const j=await api('/api/dona/v9/routine/execute',{confirm:'Eteindre les lumieres autorisees'});status(j.result?.message||'Commande envoyée.');}catch(e){status(e.message);}
|
||
},'dv9-danger'));
|
||
}else if(kind==='child'){
|
||
const q=result.quiz||{};
|
||
const c=section('Quiz de découverte',q.note);
|
||
(q.questions||[]).forEach(x=>{
|
||
const row=el('div','dv9-quiz');row.append(el('p','',x.question),btn('Voir la réponse',e=>{const a=el('span','dv9-answer',x.answer);e.currentTarget?.replaceWith(a);},'dv9-button'));
|
||
// Event handlers are attached directly by btn(), no interpolation of model text.
|
||
c.append(row);
|
||
});
|
||
const link=el('a','dv9-link','Accéder à l’espace enfant');link.href='/enfant';c.append(link);
|
||
}else if(kind==='monitor'){
|
||
const d=result.monitor||{};section('Sources Hermes · MCP',d.note);
|
||
const board=d.dashboard;
|
||
if(board?.cards?.length){
|
||
const head=section(board.title,board.subtitle);
|
||
board.cards.forEach(x=>{
|
||
const c=section(x.title||x.type,'');
|
||
if(x.type==='metric')paragraph(c,String(x.value??'—')+(x.unit?' '+x.unit:''));
|
||
if(x.type==='text')paragraph(c,x.text||'');
|
||
if(x.type==='list')list(c,(x.items||[]).map(String));
|
||
if(x.type==='table'){list(c,(x.rows||[]).map(row=>(row||[]).join(' · ')));}
|
||
if(x.type==='bars'){list(c,(x.items||[]).map(item=>item.label+' : '+item.value));}
|
||
});
|
||
}
|
||
}else section('Réponse',result.speech||'');
|
||
const pin=$('dv9PinResult');pin.hidden=!['recipe','menu','briefing','daily','documents','monitor','routine'].includes(kind);
|
||
}
|
||
function showMenu(menu){
|
||
if(!menu)return;
|
||
const c=section('Menus pour '+menu.people+' personnes · '+menu.days+' jours',menu.note);
|
||
(menu.meals||[]).forEach((x,i)=>{const item=el('div','dv9-meal');item.append(el('strong','','Jour '+(i+1)+' · '+x.title));
|
||
item.append(btn('Voir la recette',()=>{ $('dv9Results').replaceChildren();recipeCard(x.recipe);$('dv9ResultTitle').textContent=x.title;}));c.append(item);
|
||
});
|
||
const shop=section('Liste de courses · à vérifier','Les éléments sont proposés sans quantités précises.');list(shop,menu.shopping||[]);
|
||
shop.append(btn('Ajouter à ma liste de courses',async()=>{
|
||
if(!confirm('Ajouter '+(menu.shopping||[]).length+' ingrédients à la liste de courses de Dona ?'))return;
|
||
try{const j=await api('/api/dona/v9/shopping/add',{items:menu.shopping});status(j.added.length+' ajouté(s) à la liste de courses existante d’Alfred'+(j.already_present.length?' · '+j.already_present.length+' déjà présent(s).':'.'));}catch(e){status(e.message);}
|
||
}));
|
||
shop.append(btn('Ouvrir la liste de courses d’Alfred',()=>{
|
||
if(window.AlfredLists?.open){close();window.AlfredLists.open();}else status('La fenêtre Listes d’Alfred est indisponible.');
|
||
},'dv9-button'));
|
||
}
|
||
async function ask(text){
|
||
if(!text.trim())return;status('Dona prépare la réponse…');$('dv9Run').disabled=true;
|
||
try{const x=await api('/api/dona/v9/ask',{text});render(x.result,text);}
|
||
catch(e){status('Erreur : '+e.message);}
|
||
finally{$('dv9Run').disabled=false;}
|
||
}
|
||
async function loadPins(){
|
||
try{
|
||
const x=await get('/api/dona/v9/pins');const node=$('dv9Pins');node.replaceChildren();
|
||
(x.pins||[]).forEach(p=>{const div=el('div','dv9-pin');div.append(btn(p.title,()=>ask(p.prompt)));
|
||
div.append(btn('×',async()=>{try{await api('/api/dona/v9/pins/delete',{id:p.id});loadPins();}catch(e){status(e.message);}},'dv9-pin-del'));node.append(div);});
|
||
}catch(e){status('Épingles indisponibles : '+e.message);}
|
||
}
|
||
function init(){
|
||
if($('donaSuiteV9'))return;
|
||
const overlay=el('div','dv9-overlay');overlay.id='donaSuiteV9';overlay.hidden=true;
|
||
const panel=el('section','dv9-panel');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Dona, quotidien et fonctionnalités');
|
||
const head=el('header','dv9-head');const brand=el('div');brand.append(el('span','dv9-small','DONA · SUITE V9'),el('h2','','Mon majordome'));head.append(brand,btn('×',close,'dv9-close'));panel.append(head);
|
||
const examples=el('nav','dv9-quick');[
|
||
['☀ Briefing','Dona, briefing du jour'],['▦ Organiser','Dona, organise ma journée de demain'],['♨ Cuisine','Dona, montre-moi la recette de la quiche'],
|
||
['▤ Menus','Dona, prépare des menus pour 3 jours'],['⌂ Routine','Dona, bonne nuit'],['▣ Serveurs','Dona, état des serveurs Proxmox et Wazuh'],
|
||
['▤ Documents','Dona, cherche dans mes documents Proxmox'],['✦ Mémoire','Dona, montre mes souvenirs'],['✿ Enfant','Dona, quiz enfant']
|
||
].forEach(([label,prompt])=>examples.append(btn(label,()=>ask(prompt),'dv9-chip')));panel.append(examples);
|
||
const form=el('form','dv9-form');const input=el('input','dv9-input');input.id='dv9Prompt';input.placeholder='Demande une recette, un briefing, un document…';input.maxLength=500;input.required=true;
|
||
const run=btn('Demander',()=>{},'dv9-button dv9-primary');run.type='submit';run.id='dv9Run';form.append(input,run);form.addEventListener('submit',e=>{e.preventDefault();ask(input.value);});panel.append(form);
|
||
const statusLine=el('p','dv9-status');statusLine.id='dv9Status';statusLine.setAttribute('role','status');panel.append(statusLine);
|
||
const pinsWrap=el('div','dv9-pins');pinsWrap.append(el('h3','','Épingles persistantes'));const pins=el('div','dv9-pins-row');pins.id='dv9Pins';pinsWrap.append(pins);panel.append(pinsWrap);
|
||
const subhead=el('div','dv9-subhead');const title=el('h2','','Bienvenue');title.id='dv9ResultTitle';const pin=btn('☆ Épingler cette demande',async()=>{
|
||
if(!current)return;
|
||
try{await api('/api/dona/v9/pins',{kind:current.kind,title:current.title||'Carte',prompt:lastPrompt});await loadPins();status('Demande épinglée sur le serveur.');}catch(e){status(e.message);}
|
||
},'dv9-chip');pin.id='dv9PinResult';pin.hidden=true;subhead.append(title,pin);panel.append(subhead);
|
||
const result=el('main','dv9-results');result.id='dv9Results';result.append(el('p','dv9-welcome','Une seule interface pour les recettes, l’agenda, les souvenirs, les documents et les serveurs.'));panel.append(result);
|
||
const foot=el('p','dv9-footer','Données locales ou Home Assistant. Aucun ordre domotique n’est lancé sans confirmation. Les résultats Hermes sont des indications à vérifier.');panel.append(foot);
|
||
overlay.append(panel);document.body.append(overlay);
|
||
overlay.addEventListener('click',e=>{if(e.target===overlay)close();});document.addEventListener('keydown',e=>{if(e.key==='Escape'&&opened)close();});
|
||
let attempts=0;const interval=setInterval(()=>{const target=document.querySelector('#alfredMainV1 .av-head-actions')||document.querySelector('.header-actions');
|
||
if(target&&!$('dv9Open')){const b=btn('✦',open,'av-round dv9-launch');b.id='dv9Open';b.title='Dona — Quotidien';b.setAttribute('aria-label','Ouvrir le cockpit Dona');target.prepend(b);clearInterval(interval);}
|
||
else if(++attempts>80)clearInterval(interval);
|
||
},150);
|
||
const socket=window.alfredSocket;
|
||
socket?.on('dona_suite_v9',result=>{if(opened)render(result,lastPrompt);});
|
||
}
|
||
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
|
||
window.DonaSuiteV9={open,close,render,ask};
|
||
})();
|