This commit is contained in:
pporcheret
2026-10-30 22:10:03 +01:00
parent 7f26f9b015
commit 90ffa635a0
161 changed files with 19042 additions and 111 deletions
@@ -0,0 +1,62 @@
/* DONA_REPLY_AUTO_CLOSE_V1 — Ferme uniquement la réponse, jamais Hermes/Piper. */
(() => {
'use strict';
if (window.__donaReplyAutoCloseV1) return;
window.__donaReplyAutoCloseV1 = true;
const interval = Number(window.DONA_REPLY_CLOSE_SECONDS || 60);
const delay = Math.min(600, Math.max(10, Number.isFinite(interval) ? interval : 60)) * 1000;
let task = 0;
let speaking = false;
let focused = false;
const $ = (id) => document.getElementById(id);
const isFocused = () => Boolean($('alfredMainV1')?.classList.contains('is-focused'));
const clear = () => { if (task) window.clearTimeout(task); task=0; };
const canClose = () => isFocused() && !speaking && !document.querySelector('dialog[open], [role="dialog"][aria-modal="true"]');
const close = () => {
task = 0;
if (!canClose()) { schedule(); return; }
const back = $('avBack');
if (back) back.click();
};
function schedule() {
clear();
if (canClose()) task = window.setTimeout(close,delay);
}
function stateChanged() {
const now=isFocused();
if (!now) { clear(); focused=false; return; }
if (!focused) { focused=true; schedule(); }
}
function init() {
const main=$('alfredMainV1');
if (!main) { window.setTimeout(init,600); return; }
new MutationObserver(stateChanged).observe(main,{attributes:true,attributeFilter:['class']});
const content=$('content');
if (content) new MutationObserver(() => {if (isFocused()) schedule();})
.observe(content,{childList:true,subtree:true,characterData:true});
const socket=window.alfredSocket;
if (socket?.on) {
socket.on('alfred_state', (state) => {
const mode=String(state?.mode || '').toLowerCase();
const wasSpeaking=speaking;
speaking=(mode==='speaking' || mode==='thinking' || mode==='listening');
if (speaking) clear();
else if (wasSpeaking || isFocused()) schedule();
});
socket.on('alfred_render', () => window.setTimeout(() => {stateChanged();schedule();},0));
}
document.addEventListener('pointerdown',(ev)=>{
if (isFocused() && ev.target?.closest('#stage')) schedule();
},{passive:true});
document.addEventListener('keydown',(ev)=>{
if (isFocused() && ['ArrowUp','ArrowDown','PageUp','PageDown'].includes(ev.key)) schedule();
});
document.addEventListener('visibilitychange',()=>{
if (document.hidden) clear(); else schedule();
});
stateChanged();
console.info('[Dona] Retour automatique à l’accueil activé après',delay/1000,'secondes (hors lecture vocale).');
}
if (document.readyState==='loading') document.addEventListener('DOMContentLoaded',init,{once:true});
else init();
})();
@@ -0,0 +1,104 @@
/* DONA_FAMILY_UI_V1: non-destructive augmentation of the installed Glass V1 dashboard. */
(() => {
'use strict';
let attempts=0;
function init(){
const root=document.getElementById('alfredMainV1');
if(!root){if(++attempts<40)setTimeout(init,150);return;}
if(root.dataset.donaFamily==='ready')return;
const $=id=>document.getElementById(id);
const head=root.querySelector('.av-header-right');
const weather=root.querySelector('.av-weather');
const hero=root.querySelector('.av-voice-hero');
const shortcuts=root.querySelector('.av-shortcuts');
if(!head||!weather||!hero||!shortcuts)return;
root.dataset.donaFamily='ready';document.body.classList.add('dona-family-v1');
// Move the EXISTING weather widget: old JS keeps updating the SAME IDs.
weather.classList.add('dona-inline-weather');
weather.setAttribute('title','Ouvrir les détails météo');
head.prepend(weather);
// Child link is supplementary; all old admin/navigation functions remain.
const kid=document.createElement('a');kid.href='/enfant';kid.className='dona-child-link';kid.textContent='⭐ Espace enfant';
kid.setAttribute('aria-label','Ouvrir le chat enfant, écrit uniquement');
head.insertBefore(kid,head.querySelector('.av-head-actions'));
const toolsGrid=root.querySelector('#avMoreDialog .av-more-grid');
if(toolsGrid&&!toolsGrid.querySelector('a[href="/enfant"]')){
const a=document.createElement('a');a.href='/enfant';a.textContent='⭐ Espace enfant';toolsGrid.append(a);
}
// New expressive centre; old controls stay in DOM so audio event listeners work.
const display=document.createElement('div');display.className='dona-art-center';
display.innerHTML='<div class="dona-organic-wrap"><canvas id="donaOrganicCanvas" aria-label="Orbe animé de Dona" role="img"></canvas><span class="dona-orb-reflection" aria-hidden="true"></span></div><strong class="dona-art-name" id="donaArtName"></strong><span class="dona-art-subtitle" id="donaArtSubtitle">Prêt à vous aider</span>';
hero.prepend(display);
const oldVoice=root.querySelector('.av-voice-center');
if(oldVoice){display.append(oldVoice);}
// Move shortcuts into a collapsed tray, instead of removing their controls.
const tools=document.createElement('details');tools.className='dona-tools-tray';
const summary=document.createElement('summary');summary.textContent='☷ Raccourcis';
shortcuts.parentNode.insertBefore(tools,shortcuts);tools.append(summary,shortcuts);
// Minimal permanent one-row text field: no expandable tray, send at right.
const compose=document.createElement('div');compose.className='dona-compose dona-compose-compact';
compose.innerHTML=`<form class="dona-compose-body" id="donaComposeBody">
<label class="dona-sr-only" for="donaComposeInput">Écrire à Dona</label>
<div class="dona-compose-row"><textarea id="donaComposeInput" maxlength="1600" rows="1" placeholder="Écrire à Dona…" aria-label="Écrire à Dona" required></textarea><button id="donaComposeSend" type="submit" aria-label="Envoyer la demande" title="Envoyer">➤</button></div>
<span id="donaComposeStatus" role="status" aria-live="polite"></span>
</form>`;
root.append(compose);
const body=$('donaComposeBody'),input=$('donaComposeInput'),send=$('donaComposeSend'),status=$('donaComposeStatus');
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();body.requestSubmit();}});
body.addEventListener('submit',async e=>{
e.preventDefault();let message=input.value.trim();if(!message)return;
send.disabled=true;status.textContent='Envoi à Hermes…';
try{
const r=await fetch('/api/dona/texte',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({message})});
let d={};try{d=await r.json();}catch(_){}
if(!r.ok)throw Error(d.error||'Erreur '+r.status);
input.value='';status.textContent='Hermes prépare sa réponse…';
}catch(err){status.textContent=err.message;}
finally{send.disabled=false;}
});
const modeText={idle:'Prêt à vous aider',waiting:'Prêt à vous aider',listening:'Je vous écoute…',thinking:'Je réfléchis…',speaking:'Je vous réponds…',error:'Un problème est survenu'};
function setMode(mode){root.dataset.familyMode=mode;const t=$('donaArtSubtitle');if(t)t.textContent=modeText[mode]||'Prêt à vous aider';if(mode==='idle'&&status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';}
function setName(value){const name=String(value||$('avName')?.textContent||'Dona').trim();$('donaArtName').textContent=name;}
setName();setMode(root.dataset.mode||'idle');
const socket=window.alfredSocket;
socket?.on('alfred_state',s=>{setMode(String(s.mode||'idle').toLowerCase());setName(s.assistant_name||s.title||$('avName')?.textContent);});
socket?.on('alfred_render',()=>{if(status.textContent==='Hermes prépare sa réponse…')status.textContent='Réponse reçue';});
fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s){setMode(s.mode||'idle');setName(s.assistant_name||s.title);}}).catch(()=>{});
// Animated multi-layer liquid orb using canvas, independent of server state.
const canvas=$('donaOrganicCanvas'),ctx=canvas.getContext('2d');
if(!ctx)return;
let w=320,h=320,phase=0,raf=0,last=0;
const reduce=matchMedia('(prefers-reduced-motion: reduce)').matches;
function resize(){const rect=canvas.getBoundingClientRect();const dpr=Math.min(devicePixelRatio||1,2);w=Math.max(150,rect.width);h=Math.max(150,rect.height);canvas.width=Math.floor(w*dpr);canvas.height=Math.floor(h*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);}
window.addEventListener('resize',resize,{passive:true});resize();
function path(radius,layer){const n=130;ctx.beginPath();for(let k=0;k<=n;k++){
const a=k/n*Math.PI*2;const modulation=1+0.067*Math.sin(a*3+phase*.72+layer*.64)+0.044*Math.cos(a*5-phase*.39+layer)+0.021*Math.sin(a*8+phase*.55);
const r=radius*modulation;const x=Math.cos(a)*r,y=Math.sin(a)*r;
if(k===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
}ctx.closePath();}
function frame(now){raf=requestAnimationFrame(frame);if(document.hidden||now-last<30)return;last=now;
const mode=root.dataset.familyMode||'idle';const speed=mode==='listening'?1.7:mode==='thinking'?1.38:mode==='speaking'?1.5:.75;
if(!reduce)phase+=.024*speed;
ctx.clearRect(0,0,w,h);ctx.save();ctx.translate(w/2,h/2);const r=Math.min(w,h)*.33;
const bloom=ctx.createRadialGradient(-r*.2,-r*.15,r*.1,0,0,r*1.45);
bloom.addColorStop(0,'rgba(238,117,160,.11)');bloom.addColorStop(.7,'rgba(139,112,246,.14)');bloom.addColorStop(1,'rgba(37,180,255,0)');ctx.fillStyle=bloom;ctx.beginPath();ctx.arc(0,0,r*1.48,0,Math.PI*2);ctx.fill();
for(let layer=3;layer>=0;layer--){const waveR=r*(1+layer*.025);path(waveR,layer);
const g=ctx.createLinearGradient(-r*.9,r*.8,r*.9,-r*.85);
if(layer%2===0){g.addColorStop(0,'rgba(47,196,244,.76)');g.addColorStop(.36,'rgba(68,94,198,.88)');g.addColorStop(.62,'rgba(244,138,173,.74)');g.addColorStop(1,'rgba(255,176,71,.9)');}
else {g.addColorStop(0,'rgba(59,164,255,.65)');g.addColorStop(.4,'rgba(196,119,245,.48)');g.addColorStop(.7,'rgba(241,125,146,.52)');g.addColorStop(1,'rgba(255,185,95,.76)');}
ctx.globalAlpha=layer===0?.76:.25;ctx.fillStyle=g;ctx.fill();
ctx.globalAlpha=layer===0?.92:.31;ctx.strokeStyle=g;ctx.lineWidth=layer===0?2.5:1.8;ctx.shadowBlur=layer===0?23:13;ctx.shadowColor=layer%2?'#bd80ec':'#ffc685';ctx.stroke();ctx.shadowBlur=0;
}
const ribbon=ctx.createLinearGradient(-r,r*.4,r,-r*.3);ribbon.addColorStop(0,'rgba(67,219,255,.0)');ribbon.addColorStop(.48,'rgba(255,231,197,.56)');ribbon.addColorStop(1,'rgba(255,181,103,.01)');
ctx.beginPath();for(let i=0;i<=100;i++){const x=-r+i/100*r*2;const y=Math.sin(i/100*Math.PI*2+phase*.35)*r*.25+Math.cos(i/100*Math.PI*3-phase*.45)*r*.11;if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);}ctx.lineWidth=r*.26;ctx.strokeStyle=ribbon;ctx.globalAlpha=.38;ctx.stroke();ctx.restore();
}
raf=requestAnimationFrame(frame);window.addEventListener('pagehide',()=>cancelAnimationFrame(raf),{once:true});
}
init();
})();
@@ -0,0 +1,41 @@
/* DONA_V7_CONFIG accordéons : les champs demeurent dans le DOM et leurs événements fonctionnent. */
(() => {
'use strict';
function init(){
const main=document.getElementById('mainArea');
if(!main || document.body.dataset.page!=='config') return;
if(main.dataset.donaAccordions==='ready')return;
main.dataset.donaAccordions='ready';
const panels=[...main.querySelectorAll('section.panel')];
const entries=[];
panels.forEach((panel,index)=>{
let head=panel.querySelector(':scope > .panel-title');
if(!head){
const label=panel.querySelector('h2')?.textContent?.trim()||'Maintenance';
head=document.createElement('div');head.className='panel-title';
const heading=document.createElement('h2');heading.textContent=label;head.append(heading);
panel.prepend(head);
}
const body=document.createElement('div');body.className='dona-accordion-body';body.id='donaConfigBody'+index;
[...panel.childNodes].filter(node=>node!==head).forEach(node=>body.append(node));
const toggle=document.createElement('button');toggle.type='button';toggle.className='dona-accordion-toggle';
toggle.setAttribute('aria-controls',body.id);toggle.setAttribute('aria-expanded','false');
toggle.setAttribute('aria-label','Déplier la section '+(head.querySelector('h2')?.textContent||''));
toggle.textContent='⌄';head.append(toggle);panel.append(body);
panel.classList.add('dona-collapsible');
const setOpen = open=>{body.hidden=!open;toggle.textContent=open?'⌃':'⌄';toggle.setAttribute('aria-expanded',String(open));};
setOpen(false);
toggle.addEventListener('click',e=>{e.stopPropagation();setOpen(body.hidden);});
head.addEventListener('click',e=>{if(!e.target.closest('button,a,input,select'))setOpen(body.hidden);});
entries.push(setOpen);
});
if(entries.length){
const tools=document.createElement('div');tools.className='dona-config-tools';
const open=document.createElement('button');open.type='button';open.textContent='Tout déplier';open.addEventListener('click',()=>entries.forEach(fn=>fn(true)));
const close=document.createElement('button');close.type='button';close.textContent='Tout replier';close.addEventListener('click',()=>entries.forEach(fn=>fn(false)));
tools.append(open,close);
const grid=main.querySelector('.two-col');if(grid)grid.before(tools);else main.prepend(tools);
}
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
})();
+176
View File
@@ -0,0 +1,176 @@
/* DONA_V8_DYNAMIC — data-only dashboards. All user/model text is textContent, never HTML. */
(() => {
'use strict';
const ID='donaDynamicV8';
if(document.getElementById(ID))return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{
const item=document.createElement(tag);if(cls)item.className=cls;
if(text!==undefined)item.textContent=String(text);return item;
};
const btn=(text,fn,cls='d8-btn')=>{const b=el('button',cls,text);b.type='button';b.addEventListener('click',fn);return b;};
let active=null,opened=false,busy=false,ticker=null;
const KEY='dona.dynamic.pins.v8';
const safeGetPins=()=>{
try{const pins=JSON.parse(localStorage.getItem(KEY)||'[]');return Array.isArray(pins)?pins.slice(0,8):[];}
catch(_){return []}
};
const safeSavePins=list=>{try{localStorage.setItem(KEY,JSON.stringify(list.slice(0,8)));}catch(_){status('Stockage local indisponible.');}};
function status(text){$('d8Status').textContent=text||'';}
function makeModal(){
const root=el('div','d8-overlay');root.id=ID;root.hidden=true;
root.setAttribute('role','dialog');root.setAttribute('aria-modal','true');root.setAttribute('aria-label','Tableaux dynamiques Dona');
const panel=el('section','d8-panel');
const head=el('header','d8-head');
const ident=el('div','d8-brand');ident.append(el('span','d8-eyebrow','DONA · INTERFACE DYNAMIQUE'),el('h1','d8-title','Tableaux de bord'));
head.append(ident,btn('×',close,'d8-close'));panel.append(head);
const bar=el('form','d8-compose');bar.id='d8Compose';
const input=el('input','d8-input');input.id='d8Input';input.required=true;input.maxLength=500;
input.placeholder='Ex. Tableau de bord Proxmox, Wazuh, lumières…';input.setAttribute('aria-label','Demande de tableau dynamique');
bar.append(input,btn('Créer',()=>{},'d8-submit'));bar.lastChild.type='submit';
bar.addEventListener('submit',e=>{e.preventDefault();create(input.value);});panel.append(bar);
const examples=el('div','d8-examples');
[['Spotify','Tableau de bord Spotify'],['Lumières','Tableau de bord des lumières'],
['Températures','Tableau de bord des températures'],['Proxmox','Tableau de bord Proxmox'],
['Wazuh','Tableau de bord Wazuh']].forEach(([text,prompt])=>examples.append(btn(text,()=>{input.value=prompt;create(prompt)},'d8-chip')));
panel.append(examples);
const actions=el('div','d8-toolbar');
actions.append(btn('↻ Actualiser',refresh,'d8-btn'),btn('☆ Épingler',pin,'d8-btn'));
actions.children[0].id='d8Refresh'; actions.children[1].id='d8Pin';panel.append(actions);
const caption=el('div','d8-note');caption.id='d8Status';caption.setAttribute('role','status');caption.setAttribute('aria-live','polite');panel.append(caption);
const pinned=el('div','d8-pinned');pinned.id='d8Pinned';panel.append(pinned);
const grid=el('main','d8-grid');grid.id='d8Grid';panel.append(grid);
const foot=el('p','d8-footer','Données provenant de Spotify, Home Assistant ou des outils Hermes. Aucun code produit par l’IA n’est exécuté.');panel.append(foot);
root.append(panel);document.body.append(root);
root.addEventListener('click',e=>{if(e.target===root)close()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&opened)close();});
return root;
}
makeModal();
function open(){opened=true;$(ID).hidden=false;document.body.classList.add('d8-open');$('d8Input').focus();renderPins();schedule();}
function close(){opened=false;$(ID).hidden=true;document.body.classList.remove('d8-open');stopTimer();}
function stopTimer(){if(ticker){clearInterval(ticker);ticker=null;}}
function schedule(){stopTimer();if(!opened||!active||!['spotify','homeassistant','lights'].includes(active.origin))return;
ticker=setInterval(()=>{if(!document.hidden&&!busy)reloadLive(false);},active.origin==='spotify'?15000:30000);
}
function originLabel(origin){return ({spotify:'Spotify · direct',homeassistant:'Home Assistant · direct',lights:'Home Assistant · direct',hermes:'Hermes · MCP',fallback:'Hermes · réponse texte'})[origin]||'Données contextuelles';}
const tagText=(parent,tag,cls,text)=>{parent.append(el(tag,cls,text));};
function drawCard(c){
const section=el('article','d8-card d8-'+c.type);
if(c.title)tagText(section,'h2','d8-card-title',c.title);
if(c.type==='metric'){
const value=el('div','d8-metric-value',c.value??'—');
if(c.unit)value.append(el('small','d8-unit',c.unit));section.append(value);
if(c.detail)tagText(section,'p','d8-detail',c.detail);
} else if(c.type==='text'){
tagText(section,'p','d8-text',c.text||'—');
} else if(c.type==='list'){
const list=el('ul','d8-list');(c.items||[]).forEach(x=>list.append(el('li','',x)));
if(!list.childNodes.length)list.append(el('li','','Aucune donnée.'));section.append(list);
} else if(c.type==='table'){
const scroll=el('div','d8-table-scroll'),table=el('table','d8-table'),thead=el('thead'),headrow=el('tr');
(c.columns||[]).forEach(x=>headrow.append(el('th','',x)));thead.append(headrow);table.append(thead);
const tbody=el('tbody');(c.rows||[]).forEach(r=>{const tr=el('tr');(r||[]).forEach(v=>tr.append(el('td','',v)));tbody.append(tr);});
table.append(tbody);scroll.append(table);section.append(scroll);
} else if(c.type==='bars'){
const values=(c.items||[]).map(x=>Number(x.value)).filter(x=>Number.isFinite(x));
const max=Math.max(1,...values.map(x=>Math.abs(x)));
(c.items||[]).forEach(item=>{
const row=el('div','d8-bar-row'),line=el('div','d8-bar-line');
line.append(el('span','',item.label),el('strong','',String(item.value)+(item.unit?' '+item.unit:'')));
const track=el('div','d8-track'),fill=el('div','d8-fill');
fill.style.width=Math.max(0,Math.min(100,Math.abs(Number(item.value))/max*100))+'%';
track.append(fill);row.append(line,track);section.append(row);
});
} else if(c.type==='light' && active?.origin==='lights'){
const state=String(c.state||'unknown');tagText(section,'p','d8-light-state',state==='on'?'Allumée':state==='off'?'Éteinte':'Indisponible');
if(state==='on'||state==='off'){
const b=btn(state==='on'?'Éteindre':'Allumer',()=>toggleLight(c.entity_id,c.title),'d8-btn d8-action');section.append(b);
}
}
return section;
}
function render(board){
if(!board||board.schema!=='dona.dashboard.v1'||!Array.isArray(board.cards))return;
active=board;open();
$('d8Input').value=board.prompt||$('d8Input').value;
$('d8Grid').replaceChildren();
const sub=el('div','d8-heading');sub.append(el('h2','',board.title||'Tableau de bord'));
sub.append(el('p','',board.subtitle||originLabel(board.origin)));
if(board.generated_at){const d=new Date(board.generated_at);sub.append(el('small','',`Mis à jour : ${Number.isNaN(d.getTime())?'—':d.toLocaleTimeString('fr-FR')}`));}
$('d8Grid').append(sub);
if(board.origin==='spotify'){
const controls=el('div','d8-player-controls');
[['⏮ Précédent','previous'],['▶ Lecture','play'],['Ⅱ Pause','pause'],['⏭ Suivant','next']].forEach(([label,action])=>{
controls.append(btn(label,async()=>{
try{await post('/api/dona/spotify/control',{action});await reloadLive(false);}
catch(e){status('Spotify : '+e.message)}
},'d8-btn'));
});
$('d8Grid').append(controls);
}
board.cards.slice(0,16).forEach(c=>{if(c&&typeof c==='object')$('d8Grid').append(drawCard(c));});
status(originLabel(board.origin)+(board.origin==='hermes'?' · contenu communiqué par l’agent':''));
renderPins();schedule();
}
function renderPins(){
const host=$('d8Pinned');host.replaceChildren();
const pins=safeGetPins();if(!pins.length)return;
host.append(el('span','d8-pins-label','Épinglés :'));
pins.forEach((p,i)=>{
const item=el('span','d8-pin-item');
item.append(btn(p.title||'Tableau '+(i+1),()=>render(p),'d8-chip'));
item.append(btn('×',()=>{const rest=safeGetPins().filter((_,j)=>j!==i);safeSavePins(rest);renderPins();},'d8-pin-remove'));
host.append(item);
});
}
function pin(){if(!active){status('Crée d’abord un tableau.');return;}
const pins=safeGetPins();pins.unshift(active);safeSavePins(pins);renderPins();status('Tableau épinglé dans ce navigateur.');}
async function post(url,body){
const res=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(body)});
const obj=await res.json().catch(()=>({}));if(!res.ok||obj.ok===false)throw Error(obj.error||'HTTP '+res.status);return obj;
}
async function create(prompt){
const query=String(prompt||'').trim();if(query.length<2||query.length>500){status('Décris le tableau en 2 à 500 caractères.');return;}
if(busy)return;busy=true;status('Dona prépare le tableau…');
try{await post('/api/dona/dynamic/create',{prompt:query});status('Dona consulte les données ; la vue apparaîtra ici.');}
catch(e){status(e.message||'Demande impossible.');}
finally{busy=false;}
}
async function reloadLive(notify=true){
if(!active)return;
if(['spotify','homeassistant','lights'].includes(active.origin)){
if(busy)return;const current=active;busy=true;
try{const r=await fetch('/api/dona/dynamic/live?source='+encodeURIComponent(active.origin),{cache:'no-store',credentials:'same-origin'});
const j=await r.json();if(!r.ok||!j.ok)throw Error(j.error||'Source indisponible');
if(current===active)render(j.dashboard);
}catch(e){status('Actualisation impossible : '+e.message);}finally{busy=false;}
} else if(notify && active.prompt){create(active.prompt);}
}
function refresh(){if(!active){status('Aucun tableau à actualiser.');return;}reloadLive(true);}
async function toggleLight(entity,name){
if(!active||active.origin!=='lights')return;
if(!/^light\.[a-z0-9_]{1,100}$/.test(String(entity)))return;
if(!confirm('Changer l’état de « '+String(name).slice(0,100)+' » ?'))return;
try{await post('/api/dona/dynamic/lights/toggle',{entity_id:entity});await reloadLive();}
catch(e){status('Commande refusée : '+e.message);}
}
function initButton(){
const primary=document.querySelector('#alfredMainV1 .av-head-actions')||document.querySelector('.header-actions');
if(!primary)return;
if($('d8Open'))return;
const b=btn('▦',open,'d8-open-button');b.id='d8Open';b.title='Tableaux dynamiques';b.setAttribute('aria-label','Ouvrir les tableaux de bord dynamiques');
primary.prepend(b);
}
let tries=0;const timer=setInterval(()=>{initButton();if($('d8Open')||++tries>70)clearInterval(timer);},150);
const socket=window.alfredSocket;
socket?.on('alfred_render',data=>{
if(data?.type==='dona_dynamic')render(data.dashboard);
else if(opened && data?.type!=='dona_dynamic')close();
});
socket?.on('alfred_state',data=>{
if(opened && data?.mode==='error')status('Erreur lors de la préparation du tableau.');
});
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&opened&&active&&['spotify','homeassistant','lights'].includes(active.origin))reloadLive(false);});
window.DonaDynamicV8={open,render,close};
})();
@@ -0,0 +1,176 @@
/* DONA_V8_DYNAMIC — data-only dashboards. All user/model text is textContent, never HTML. */
(() => {
'use strict';
const ID='donaDynamicV8';
if(document.getElementById(ID))return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{
const item=document.createElement(tag);if(cls)item.className=cls;
if(text!==undefined)item.textContent=String(text);return item;
};
const btn=(text,fn,cls='d8-btn')=>{const b=el('button',cls,text);b.type='button';b.addEventListener('click',fn);return b;};
let active=null,opened=false,busy=false,ticker=null;
const KEY='dona.dynamic.pins.v8';
const safeGetPins=()=>{
try{const pins=JSON.parse(localStorage.getItem(KEY)||'[]');return Array.isArray(pins)?pins.slice(0,8):[];}
catch(_){return []}
};
const safeSavePins=list=>{try{localStorage.setItem(KEY,JSON.stringify(list.slice(0,8)));}catch(_){status('Stockage local indisponible.');}};
function status(text){$('d8Status').textContent=text||'';}
function makeModal(){
const root=el('div','d8-overlay');root.id=ID;root.hidden=true;
root.setAttribute('role','dialog');root.setAttribute('aria-modal','true');root.setAttribute('aria-label','Tableaux dynamiques Dona');
const panel=el('section','d8-panel');
const head=el('header','d8-head');
const ident=el('div','d8-brand');ident.append(el('span','d8-eyebrow','DONA · INTERFACE DYNAMIQUE'),el('h1','d8-title','Tableaux de bord'));
head.append(ident,btn('×',close,'d8-close'));panel.append(head);
const bar=el('form','d8-compose');bar.id='d8Compose';
const input=el('input','d8-input');input.id='d8Input';input.required=true;input.maxLength=500;
input.placeholder='Ex. Tableau de bord Proxmox, Wazuh, lumières…';input.setAttribute('aria-label','Demande de tableau dynamique');
bar.append(input,btn('Créer',()=>{},'d8-submit'));bar.lastChild.type='submit';
bar.addEventListener('submit',e=>{e.preventDefault();create(input.value);});panel.append(bar);
const examples=el('div','d8-examples');
[['Spotify','Tableau de bord Spotify'],['Lumières','Tableau de bord des lumières'],
['Températures','Tableau de bord des températures'],['Proxmox','Tableau de bord Proxmox'],
['Wazuh','Tableau de bord Wazuh']].forEach(([text,prompt])=>examples.append(btn(text,()=>{input.value=prompt;create(prompt)},'d8-chip')));
panel.append(examples);
const actions=el('div','d8-toolbar');
actions.append(btn('↻ Actualiser',refresh,'d8-btn'),btn('☆ Épingler',pin,'d8-btn'));
actions.children[0].id='d8Refresh'; actions.children[1].id='d8Pin';panel.append(actions);
const caption=el('div','d8-note');caption.id='d8Status';caption.setAttribute('role','status');caption.setAttribute('aria-live','polite');panel.append(caption);
const pinned=el('div','d8-pinned');pinned.id='d8Pinned';panel.append(pinned);
const grid=el('main','d8-grid');grid.id='d8Grid';panel.append(grid);
const foot=el('p','d8-footer','Données provenant de Spotify, Home Assistant ou des outils Hermes. Aucun code produit par l’IA n’est exécuté.');panel.append(foot);
root.append(panel);document.body.append(root);
root.addEventListener('click',e=>{if(e.target===root)close()});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&opened)close();});
return root;
}
makeModal();
function open(){opened=true;$(ID).hidden=false;document.body.classList.add('d8-open');$('d8Input').focus();renderPins();schedule();}
function close(){opened=false;$(ID).hidden=true;document.body.classList.remove('d8-open');stopTimer();}
function stopTimer(){if(ticker){clearInterval(ticker);ticker=null;}}
function schedule(){stopTimer();if(!opened||!active||!['spotify','homeassistant','lights'].includes(active.origin))return;
ticker=setInterval(()=>{if(!document.hidden&&!busy)reloadLive(false);},active.origin==='spotify'?15000:30000);
}
function originLabel(origin){return ({spotify:'Spotify · direct',homeassistant:'Home Assistant · direct',lights:'Home Assistant · direct',hermes:'Hermes · MCP',fallback:'Hermes · réponse texte'})[origin]||'Données contextuelles';}
const tagText=(parent,tag,cls,text)=>{parent.append(el(tag,cls,text));};
function drawCard(c){
const section=el('article','d8-card d8-'+c.type);
if(c.title)tagText(section,'h2','d8-card-title',c.title);
if(c.type==='metric'){
const value=el('div','d8-metric-value',c.value??'—');
if(c.unit)value.append(el('small','d8-unit',c.unit));section.append(value);
if(c.detail)tagText(section,'p','d8-detail',c.detail);
} else if(c.type==='text'){
tagText(section,'p','d8-text',c.text||'—');
} else if(c.type==='list'){
const list=el('ul','d8-list');(c.items||[]).forEach(x=>list.append(el('li','',x)));
if(!list.childNodes.length)list.append(el('li','','Aucune donnée.'));section.append(list);
} else if(c.type==='table'){
const scroll=el('div','d8-table-scroll'),table=el('table','d8-table'),thead=el('thead'),headrow=el('tr');
(c.columns||[]).forEach(x=>headrow.append(el('th','',x)));thead.append(headrow);table.append(thead);
const tbody=el('tbody');(c.rows||[]).forEach(r=>{const tr=el('tr');(r||[]).forEach(v=>tr.append(el('td','',v)));tbody.append(tr);});
table.append(tbody);scroll.append(table);section.append(scroll);
} else if(c.type==='bars'){
const values=(c.items||[]).map(x=>Number(x.value)).filter(x=>Number.isFinite(x));
const max=Math.max(1,...values.map(x=>Math.abs(x)));
(c.items||[]).forEach(item=>{
const row=el('div','d8-bar-row'),line=el('div','d8-bar-line');
line.append(el('span','',item.label),el('strong','',String(item.value)));
const track=el('div','d8-track'),fill=el('div','d8-fill');
fill.style.width=Math.max(0,Math.min(100,Math.abs(Number(item.value))/max*100))+'%';
track.append(fill);row.append(line,track);section.append(row);
});
} else if(c.type==='light' && active?.origin==='lights'){
const state=String(c.state||'unknown');tagText(section,'p','d8-light-state',state==='on'?'Allumée':state==='off'?'Éteinte':'Indisponible');
if(state==='on'||state==='off'){
const b=btn(state==='on'?'Éteindre':'Allumer',()=>toggleLight(c.entity_id,c.title),'d8-btn d8-action');section.append(b);
}
}
return section;
}
function render(board){
if(!board||board.schema!=='dona.dashboard.v1'||!Array.isArray(board.cards))return;
active=board;open();
$('d8Input').value=board.prompt||$('d8Input').value;
$('d8Grid').replaceChildren();
const sub=el('div','d8-heading');sub.append(el('h2','',board.title||'Tableau de bord'));
sub.append(el('p','',board.subtitle||originLabel(board.origin)));
if(board.generated_at){const d=new Date(board.generated_at);sub.append(el('small','',`Mis à jour : ${Number.isNaN(d.getTime())?'—':d.toLocaleTimeString('fr-FR')}`));}
$('d8Grid').append(sub);
if(board.origin==='spotify'){
const controls=el('div','d8-player-controls');
[['⏮ Précédent','previous'],['▶ Lecture','play'],['Ⅱ Pause','pause'],['⏭ Suivant','next']].forEach(([label,action])=>{
controls.append(btn(label,async()=>{
try{await post('/api/dona/spotify/control',{action});await reloadLive(false);}
catch(e){status('Spotify : '+e.message)}
},'d8-btn'));
});
$('d8Grid').append(controls);
}
board.cards.slice(0,16).forEach(c=>{if(c&&typeof c==='object')$('d8Grid').append(drawCard(c));});
status(originLabel(board.origin)+(board.origin==='hermes'?' · contenu communiqué par l’agent':''));
renderPins();schedule();
}
function renderPins(){
const host=$('d8Pinned');host.replaceChildren();
const pins=safeGetPins();if(!pins.length)return;
host.append(el('span','d8-pins-label','Épinglés :'));
pins.forEach((p,i)=>{
const item=el('span','d8-pin-item');
item.append(btn(p.title||'Tableau '+(i+1),()=>render(p),'d8-chip'));
item.append(btn('×',()=>{const rest=safeGetPins().filter((_,j)=>j!==i);safeSavePins(rest);renderPins();},'d8-pin-remove'));
host.append(item);
});
}
function pin(){if(!active){status('Crée d’abord un tableau.');return;}
const pins=safeGetPins();pins.unshift(active);safeSavePins(pins);renderPins();status('Tableau épinglé dans ce navigateur.');}
async function post(url,body){
const res=await fetch(url,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify(body)});
const obj=await res.json().catch(()=>({}));if(!res.ok||obj.ok===false)throw Error(obj.error||'HTTP '+res.status);return obj;
}
async function create(prompt){
const query=String(prompt||'').trim();if(query.length<2||query.length>500){status('Décris le tableau en 2 à 500 caractères.');return;}
if(busy)return;busy=true;status('Dona prépare le tableau…');
try{await post('/api/dona/dynamic/create',{prompt:query});status('Dona consulte les données ; la vue apparaîtra ici.');}
catch(e){status(e.message||'Demande impossible.');}
finally{busy=false;}
}
async function reloadLive(notify=true){
if(!active)return;
if(['spotify','homeassistant','lights'].includes(active.origin)){
if(busy)return;const current=active;busy=true;
try{const r=await fetch('/api/dona/dynamic/live?source='+encodeURIComponent(active.origin),{cache:'no-store',credentials:'same-origin'});
const j=await r.json();if(!r.ok||!j.ok)throw Error(j.error||'Source indisponible');
if(current===active)render(j.dashboard);
}catch(e){status('Actualisation impossible : '+e.message);}finally{busy=false;}
} else if(notify && active.prompt){create(active.prompt);}
}
function refresh(){if(!active){status('Aucun tableau à actualiser.');return;}reloadLive(true);}
async function toggleLight(entity,name){
if(!active||active.origin!=='lights')return;
if(!/^light\.[a-z0-9_]{1,100}$/.test(String(entity)))return;
if(!confirm('Changer l’état de « '+String(name).slice(0,100)+' » ?'))return;
try{await post('/api/dona/dynamic/lights/toggle',{entity_id:entity});await reloadLive();}
catch(e){status('Commande refusée : '+e.message);}
}
function initButton(){
const primary=document.querySelector('#alfredMainV1 .av-head-actions')||document.querySelector('.header-actions');
if(!primary)return;
if($('d8Open'))return;
const b=btn('▦',open,'d8-open-button');b.id='d8Open';b.title='Tableaux dynamiques';b.setAttribute('aria-label','Ouvrir les tableaux de bord dynamiques');
primary.prepend(b);
}
let tries=0;const timer=setInterval(()=>{initButton();if($('d8Open')||++tries>70)clearInterval(timer);},150);
const socket=window.alfredSocket;
socket?.on('alfred_render',data=>{
if(data?.type==='dona_dynamic')render(data.dashboard);
else if(opened && data?.type!=='dona_dynamic')close();
});
socket?.on('alfred_state',data=>{
if(opened && data?.mode==='error')status('Erreur lors de la préparation du tableau.');
});
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&opened&&active&&['spotify','homeassistant','lights'].includes(active.origin))reloadLive(false);});
window.DonaDynamicV8={open,render,close};
})();
+64
View File
@@ -0,0 +1,64 @@
/* Espace enfant : dialogue texte uniquement. Aucun WebSocket/audio/MCP. */
(() => {
'use strict';
const $ = id => document.getElementById(id);
const session = JSON.parse($('dona-child-session').textContent);
const messages = $('child-messages');
const form = $('child-form');
const input = $('child-input');
const send = $('child-send');
let busy = false;
function bubble(text, role, loading=false) {
const article=document.createElement('article');article.className='chat-turn '+role+(loading?' loading':'');
if (role==='scratch') {
const pic=document.createElement('img');pic.src='/static/img/scratch-cat-user.png';pic.alt='Le chat Scratch';pic.className='chat-avatar scratch-avatar';article.append(pic);
} else {
const avatar=document.createElement('div');avatar.className='user-avatar';avatar.textContent='🙂';avatar.setAttribute('aria-hidden','true');article.append(avatar);
}
const copy=document.createElement('div');copy.className='turn-copy';
const speaker=document.createElement('span');speaker.className='speaker';speaker.textContent=role==='scratch'?'Scratch':'Moi';
const textBox=document.createElement('div');textBox.className='chat-bubble';textBox.textContent=text;
copy.append(speaker,textBox);article.append(copy);messages.append(article);
messages.scrollTop=messages.scrollHeight;
return article;
}
async function post(url,data) {
const r=await fetch(url,{method:'POST',credentials:'same-origin',headers:{
'Content-Type':'application/json','X-Dona-Enfant':'1'
},body:JSON.stringify(data)});
let payload={};try{payload=await r.json();}catch(_){}
if(!r.ok)throw new Error(payload.error||'Connexion impossible');
return payload;
}
form.addEventListener('submit',async event=>{
event.preventDefault();
if(busy)return;
const text=input.value.trim();if(!text)return;
busy=true;send.disabled=true;input.disabled=true;
input.value='';bubble(text,'you');
const pending=bubble('Scratch réfléchit…','scratch',true);
try {
const result=await post('/api/enfant/message',{session,message:text});
pending.querySelector('.chat-bubble').textContent=result.reply||'Je n’ai pas compris, on réessaie ?';
pending.classList.remove('loading');
} catch(err) {
pending.querySelector('.chat-bubble').textContent=err.message;
pending.classList.remove('loading');
} finally {
busy=false;send.disabled=false;input.disabled=false;input.focus();
messages.scrollTop=messages.scrollHeight;
}
});
input.addEventListener('keydown',event=>{
if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();form.requestSubmit();}
});
$('child-clear').addEventListener('click',async()=>{
if(busy)return;
try {
await post('/api/enfant/clear',{session});
messages.replaceChildren();
bubble('Coucou ! 😺 On commence une nouvelle histoire ?','scratch');
} catch(err){bubble(err.message,'scratch');}
input.focus();
});
})();
+53
View File
@@ -0,0 +1,53 @@
/* DONA_V7_FACE — capture ponctuelle, aucun flux continu ni enrôlement. */
(() => {
'use strict';
if (document.getElementById('donaFaceButton')) return;
function init() {
const camera = document.getElementById('avTopCamera') || document.getElementById('cameraPopupButton');
if (!camera) return;
const button = document.createElement('button');
button.id = 'donaFaceButton';
button.className = camera.className;
button.type = 'button';
button.title = 'Reconnaître la personne devant la caméra';
button.setAttribute('aria-label', button.title);
button.innerHTML = '<svg aria-hidden="true" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m13-5h3a2 2 0 0 1 2 2v3M3 16v3a2 2 0 0 0 2 2h3m13-5v3a2 2 0 0 1-2 2h-3"/><circle cx="12" cy="10" r="3"/><path d="M6.7 18c.7-2.8 3-4.2 5.3-4.2s4.6 1.4 5.3 4.2"/></svg>';
camera.after(button);
const box = document.createElement('div');
box.id = 'donaFacePanel'; box.className = 'dona-face-panel';
box.hidden = true;box.setAttribute('role','dialog');
box.setAttribute('aria-label','Reconnaissance faciale Dona');
box.innerHTML = '<div class="dona-face-card"><div class="dona-face-head"><strong>Reconnaissance faciale</strong><button type="button" id="donaFaceClose" aria-label="Fermer">×</button></div><p id="donaFaceText" role="status" aria-live="polite"></p></div>';
document.body.append(box);
const text = box.querySelector('#donaFaceText');
let pending = false;
const close = () => {box.hidden = true;};
box.querySelector('#donaFaceClose').addEventListener('click',close);
box.addEventListener('click',e=>{if(e.target===box)close();});
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!box.hidden)close();});
button.addEventListener('click',async()=>{
if(pending)return;
pending = true;button.disabled = true;
const preview=document.getElementById('cameraPopup');
if(preview && !preview.hidden){document.getElementById('cameraPopupClose')?.click();await new Promise(resolve=>setTimeout(resolve,300));}
box.hidden = false;text.textContent = 'Capture et identification en cours…';
try {
const r = await fetch('/api/dona/face/identify',{
method:'POST',credentials:'same-origin',headers:{'X-Alfred-Request':'1'},cache:'no-store'
});
const data = await r.json();
if(!r.ok)throw new Error(data.error||`Erreur HTTP ${r.status}`);
} catch(err) { pending=false;button.disabled=false;text.textContent=err.message||'Identification indisponible.'; }
});
window.alfredSocket?.on('dona_face_status',data=>{
if(pending && !box.hidden) text.textContent=String(data?.message||'Analyse en cours…');
});
window.alfredSocket?.on('dona_face_result',data=>{
if(!pending)return;
pending=false;button.disabled=false;
text.textContent=String(data?.text||'Aucun résultat exploitable.');
});
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});
else init();
})();
+48
View File
@@ -0,0 +1,48 @@
/* DONA_V8_1_ISOLATION : placement uniquement. Les contrôleurs météo/heure restent identiques. */
(() => {
'use strict';
function init() {
const root = document.getElementById('alfredMainV1');
const head = root?.querySelector('.av-head');
const right = head?.querySelector('.av-header-right');
const clock = right?.querySelector('.av-time') || head?.querySelector('.av-time');
const weather = right?.querySelector('.dona-inline-weather');
const kid = right?.querySelector('a.dona-child-link[href="/enfant"]');
if (!root || !head || !right || !clock || !weather || !kid) return;
if (root.dataset.donaHeader81 === 'ready') return;
root.dataset.donaHeader81 = 'ready';
// Horloge en haut à gauche : déplacer le nœud d'origine, sans perturber
// le timer qui met à jour #avTime et #avDate.
head.insertBefore(clock, head.firstElementChild);
clock.classList.add('dona-left-clock');
// Météo reste cliquable et continue à être actualisée par les JS existants.
weather.classList.add('dona-weather-transparent');
// Remplacer uniquement l'apparence du lien enfant : conserver href et ses
// événements. Icône SVG autonome, sans ressource externe ni police tierce.
kid.textContent = '';
kid.classList.add('dona-child-icon-only');
kid.setAttribute('aria-label', 'Espace enfant');
kid.setAttribute('title', 'Espace enfant');
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '1.9');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('aria-hidden', 'true');
// Icône baby de la famille Lucide.
svg.innerHTML = '<path d="M9 12h.01M15 12h.01"/><path d="M10 16c.5.5 1.2 1 2 1s1.5-.5 2-1"/>'+
'<path d="M12 2a2 2 0 0 0-2 2c0 1.1.9 2 2 2s2-.9 2-2"/>'+
'<circle cx="12" cy="13" r="9"/>';
kid.appendChild(svg);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, {once: true});
} else {
init();
}
})();
+11 -3
View File
@@ -81,6 +81,14 @@
const content=document.getElementById('content');
if(!main||!content)return; // Pages secondaires : seulement le thème transversal.
function el(tag,className,txt){const node=document.createElement(tag);if(className)node.className=className;if(txt!==undefined)node.textContent=String(txt);return node;}
// DONA_MARKDOWN_V1 — ne jamais interpréter le Markdown avec innerHTML brut.
function rich(tag,className,markdown,inline=false){
const node=el(tag,className);
if(window.DonaMarkdown && typeof window.DonaMarkdown.render==='function')
window.DonaMarkdown.render(node,markdown,{inline});
else node.textContent=String(markdown??'');
return node;
}
let busy=false;let original='';
function renderCards(){
if(busy || content.firstElementChild?.classList.contains('dh-response'))return;
@@ -100,8 +108,8 @@
const wrap=el('section','dh-response');
wrap.setAttribute('aria-label','Synthèse contextuelle de la réponse Hermes');
const primary=el('article','dh-primary');
primary.append(el('div','dh-kicker','RÉPONSE · DONA'),el('div','dh-headline',summary.headline));
if(summary.excerpt && summary.excerpt!==summary.headline)primary.append(el('div','dh-excerpt',summary.excerpt));
primary.append(el('div','dh-kicker','RÉPONSE · DONA'),rich('div','dh-headline',summary.headline,true));
if(summary.excerpt && summary.excerpt!==summary.headline)primary.append(rich('div','dh-excerpt',summary.excerpt));
else primary.append(el('div','dh-excerpt','Réponse disponible dans son intégralité.'));
const more=el('button','dh-more');more.type='button';more.setAttribute('aria-expanded','false');more.append(el('span','', '▤ Afficher la réponse complète'),el('span','','›'));
primary.append(more);wrap.append(primary);
@@ -121,7 +129,7 @@
wrap.classList.add('dh-no-facts');
}
const full=el('section','dh-full');full.hidden=true;full.id='dhFullReply';
full.append(el('h3','', 'Réponse complète d’Hermes'),el('div','dh-full-text',text),el('p','dh-source','Affichage fidèle au texte reçu. Les faits n’ont pas été vérifiés séparément.'));
full.append(el('h3','', 'Réponse complète d’Hermes'),rich('div','dh-full-text',text),el('p','dh-source','Affichage fidèle au texte reçu. Les faits n’ont pas été vérifiés séparément.'));
more.setAttribute('aria-controls','dhFullReply');
more.addEventListener('click',()=>{
const opened=full.hidden;full.hidden=!opened;more.setAttribute('aria-expanded',String(opened));
@@ -0,0 +1,122 @@
/* Dona Markdown V1 : rendu DOM sécurisé, sans injection de HTML depuis Hermes.
* Nécessite uniquement les API standard du navigateur (aucun CDN).
*/
(() => {
'use strict';
function normalize(value) {
return String(value ?? '')
.replace(/\r\n?/g, '\n')
.replace(/\u00a0/g, ' ')
.replace(/&#(?:x20|32);|&nbsp;/gi, ' ')
.replace(/\\\n/g, '\n')
// Certains formats rééchappent les délimiteurs Markdown en \\* ou \\`.
.replace(/\\([*`_])/g, '$1');
}
function element(tag, parent, cls) {
const node = document.createElement(tag);
if (cls) node.className = cls;
parent.appendChild(node);
return node;
}
function appendInline(parent, input, depth = 0) {
const str = String(input ?? '');
if (!str || depth > 5) { parent.appendChild(document.createTextNode(str)); return; }
const tokens = /`([^`\n]+)`|\*\*([^*\n]+?)\*\*|__([^_\n]+?)__|~~([^~\n]+?)~~|\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|\*([^*\n]+?)\*|_([^_\n]+?)_/g;
let last = 0, m;
while ((m = tokens.exec(str)) !== null) {
if (m.index > last) parent.appendChild(document.createTextNode(str.slice(last, m.index)));
if (m[1] !== undefined) element('code', parent).textContent = m[1];
else if (m[2] !== undefined || m[3] !== undefined) appendInline(element('strong', parent), m[2] ?? m[3], depth + 1);
else if (m[4] !== undefined) appendInline(element('del', parent), m[4], depth + 1);
else if (m[5] !== undefined) {
const a = element('a', parent);
a.textContent = m[5];
a.href = m[6]; // Seulement http(s), filtré par la regexp.
a.rel = 'noopener noreferrer';
a.target = '_blank';
}
else if (m[7] !== undefined || m[8] !== undefined) appendInline(element('em', parent), m[7] ?? m[8], depth + 1);
last = tokens.lastIndex;
}
if (last < str.length) parent.appendChild(document.createTextNode(str.slice(last)));
}
function render(target, value, options = {}) {
if (!target || typeof target.replaceChildren !== 'function') return;
const source = normalize(value);
const fragment = document.createDocumentFragment();
if (options.inline) {
appendInline(fragment, source);
target.replaceChildren(fragment);
return;
}
const lines = source.split('\n');
let paragraph = [];
let codeLines = null;
let lists = [];
function flushParagraph() {
if (!paragraph.length) return;
const p = element('p', fragment);
paragraph.forEach((line, i) => {
if (i) p.appendChild(document.createElement('br'));
appendInline(p, line);
});
paragraph = [];
}
for (let index = 0; index < lines.length; index++) {
const line = lines[index];
const trimmed = line.trim();
if (codeLines !== null) {
if (/^\s*```\s*$/.test(line)) {
const pre = element('pre', fragment);
element('code', pre).textContent = codeLines.join('\n');
codeLines = null;
} else codeLines.push(line);
continue;
}
if (/^\s*```(?:[\w+-]+)?\s*$/.test(line)) {
flushParagraph(); lists = []; codeLines = []; continue;
}
if (!trimmed) { flushParagraph(); lists = []; continue; }
const list = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.+)$/);
if (list) {
flushParagraph();
const indent = list[1].replace(/\t/g, ' ').length;
const type = /\d/.test(list[2][0]) ? 'ol' : 'ul';
while (lists.length && (indent < lists[lists.length - 1].indent ||
(indent === lists[lists.length - 1].indent && type !== lists[lists.length - 1].type))) lists.pop();
if (!lists.length || indent > lists[lists.length - 1].indent) {
const parent = lists.length && lists[lists.length - 1].lastItem
? lists[lists.length - 1].lastItem : fragment;
const node = element(type, parent);
lists.push({node, type, indent, lastItem: null});
}
const current = lists[lists.length - 1];
current.lastItem = element('li', current.node);
appendInline(current.lastItem, list[3]);
continue;
}
lists = [];
const heading = line.match(/^\s*(#{1,6})\s+(.+)$/);
if (heading) {
flushParagraph();
appendInline(element('h' + Math.min(heading[1].length + 2, 6), fragment), heading[2]);
continue;
}
const quote = line.match(/^\s*>\s?(.*)$/);
if (quote) {
flushParagraph();
appendInline(element('blockquote', fragment), quote[1]);
continue;
}
if (/^\s*(?:-{3,}|\*{3,})\s*$/.test(line)) {
flushParagraph(); element('hr', fragment); continue;
}
paragraph.push(line.trim());
}
if (codeLines !== null) { const pre = element('pre', fragment); element('code', pre).textContent = codeLines.join('\n'); }
flushParagraph();
target.replaceChildren(fragment);
}
window.DonaMarkdown = Object.freeze({ render, normalize });
})();
@@ -0,0 +1,121 @@
/* DONA_NAV_V83 — navigation interactive et commandes vocales, sans clé API. */
(()=>{
'use strict';
if(window.DonaNavigationV83)return;
const $=id=>document.getElementById(id);
const el=(tag,cls,text)=>{const n=document.createElement(tag);if(cls)n.className=cls;if(text!==undefined)n.textContent=String(text);return n;};
const button=(label,fn,cls='dn-btn')=>{const b=el('button',cls,label);b.type='button';b.addEventListener('click',fn);return b;};
const safeUrl=(raw,host)=>{try{const u=new URL(String(raw));return u.protocol==='https:'&&(u.hostname===host||u.hostname==='www.'+host)?u.href:'';}catch(_){return '';}};
let active=false,loading=false,selected=null,map=null,leaflet=null,readyPromise=null,headButton=null;
const status=msg=>{if($('dnStatus'))$('dnStatus').textContent=String(msg||'');};
function initialize(){
const root=$('alfredMainV1');if(!root){setTimeout(initialize,200);return;}
const head=root.querySelector('.av-head-actions');
if(head){headButton=button('⌖',open,'av-round dn-open-button');headButton.title='Navigation';headButton.setAttribute('aria-label','Ouvrir la navigation');head.prepend(headButton);}
const overlay=el('div','dn-overlay');overlay.id='donaNavigationV83';overlay.hidden=true;
const panel=el('section','dn-panel');panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Navigation Dona');
const header=el('header','dn-header');const tit=el('div');tit.append(el('span','dn-eyebrow','DONA · NAVIGATION'),el('h2','','Carte et itinéraires'));
header.append(tit,button('×',close,'dn-close'));panel.append(header);
const form=el('form','dn-form');form.id='dnSearchForm';
const dst=el('input','dn-input');dst.id='dnDestination';dst.name='destination';dst.placeholder='Destination : Metz, Luxembourg…';dst.required=true;dst.maxLength=180;dst.autocomplete='off';dst.setAttribute('aria-label','Destination');
const org=el('input','dn-input');org.id='dnOrigin';org.name='origin';org.placeholder='Départ (facultatif)';org.maxLength=180;org.autocomplete='off';org.setAttribute('aria-label','Point de départ');
const submit=button('Afficher',()=>{},'dn-btn dn-primary');submit.type='submit';submit.id='dnSubmit';form.append(dst,org,submit);form.addEventListener('submit',event=>{event.preventDefault();search(dst.value,org.value);});panel.append(form);
const message=el('p','dn-status');message.id='dnStatus';message.setAttribute('role','status');message.setAttribute('aria-live','polite');panel.append(message);
const layout=el('div','dn-layout');const main=el('div','dn-map-area');const mapbox=el('div','dn-map');mapbox.id='dnMap';main.append(mapbox);
main.append(el('p','dn-credit','© OpenStreetMap contributors · Temps de trajet indicatifs, hors trafic temps réel'));
const aside=el('aside','dn-aside');aside.append(el('h3','','Destination'));
const address=el('p','dn-destination-title','Choisis une destination.');address.id='dnPlace';aside.append(address);
const details=el('div','dn-route-details');details.id='dnDetails';aside.append(details);
const actions=el('div','dn-actions');
const wa=el('a','dn-btn dn-waze','Ouvrir Waze');wa.target='_blank';wa.rel='noopener noreferrer';wa.id='dnWaze';wa.hidden=true;
const gm=el('a','dn-btn','Google Maps');gm.target='_blank';gm.rel='noopener noreferrer';gm.id='dnMaps';gm.hidden=true;
const phone=button('Envoyer à mon téléphone',sendPhone,'dn-btn dn-primary');phone.id='dnSend';phone.disabled=true;
actions.append(wa,gm,phone);aside.append(actions);
aside.append(el('p','dn-help','Dona ne connaît pas ta position. Le départ est facultatif ; pour obtenir un temps de trajet, renseigne-le. Pour l’envoi au téléphone, configure d’abord Home Assistant.'));
layout.append(main,aside);panel.append(layout);overlay.append(panel);document.body.append(overlay);
overlay.addEventListener('click',event=>{if(event.target===overlay)close();});
window.addEventListener('keydown',event=>{if(event.key==='Escape'&&active)close();});
fetch('/api/dona/navigation/config',{credentials:'same-origin'}).then(r=>r.ok?r.json():null).then(data=>{if(data&&data.origin&&!org.value)org.value=data.origin;}).catch(()=>{});
connectSocket();
}
function open(){if(!$( 'donaNavigationV83'))return;active=true;$('donaNavigationV83').hidden=false;document.body.classList.add('dn-active');if(map)setTimeout(()=>map.invalidateSize(),80);}
function close(){active=false;$('donaNavigationV83').hidden=true;document.body.classList.remove('dn-active');}
function loadLeaflet(){
if(window.L)return Promise.resolve(window.L);
if(readyPromise)return readyPromise;
readyPromise=new Promise((resolve,reject)=>{
const style=document.createElement('link');style.rel='stylesheet';style.href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';document.head.append(style);
const script=document.createElement('script');script.src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';script.async=true;
script.onload=()=>window.L?resolve(window.L):reject(new Error('Bibliothèque de cartes indisponible.'));
script.onerror=()=>reject(new Error('Carte interactive indisponible.'));
document.head.append(script);
setTimeout(()=>{if(!window.L)reject(new Error('Carte interactive non chargée.'));},12000);
});return readyPromise;
}
function drawFallback(place){
const box=$('dnMap');if(map){try{map.remove();}catch(_){}map=null;}
box.replaceChildren();
if(!place){box.append(el('p','dn-map-fallback','Destination à rechercher.'));return;}
const lat=Number(place.lat),lon=Number(place.lon);if(!Number.isFinite(lat)||!Number.isFinite(lon))return;
const iframe=el('iframe','dn-osm-frame');iframe.title='Carte OpenStreetMap de la destination';iframe.loading='lazy';iframe.referrerPolicy='strict-origin-when-cross-origin';
const delta=.035;
iframe.src='https://www.openstreetmap.org/export/embed.html?bbox='+encodeURIComponent([lon-delta,lat-delta,lon+delta,lat+delta].join(','))+'&layer=mapnik&marker='+encodeURIComponent(lat+','+lon);
box.append(iframe);
}
async function drawMap(data){
const place=data.place;const route=data.route;
if(!place){drawFallback(null);return;}
try{
const L=await loadLeaflet();if(!active)return;
if(map){map.remove();map=null;}
$('dnMap').replaceChildren();map=L.map('dnMap',{scrollWheelZoom:false});
const tileUrl='https://tile.openstreetmap.org/{z}/{x}/{y}.png';
L.tileLayer(tileUrl,{attribution:'© OpenStreetMap contributors',maxZoom:19}).addTo(map);
const loc=L.latLng(Number(place.lat),Number(place.lon));L.marker(loc).addTo(map).bindPopup(String(data.destination||'Destination').slice(0,120));
if(route&&Array.isArray(route.line)&&route.line.length>1){
const pts=route.line.filter(p=>Array.isArray(p)&&p.length===2&&p.every(Number.isFinite));
if(pts.length>1){const line=L.polyline(pts,{color:'#64d8ff',weight:5,opacity:.85}).addTo(map);map.fitBounds(line.getBounds(),{padding:[28,28]});}
else map.setView(loc,12);
if(data.origin_place){const start=L.latLng(Number(data.origin_place.lat),Number(data.origin_place.lon));L.circleMarker(start,{radius:8,color:'#93ffc9',fillColor:'#93ffc9',fillOpacity:.9}).addTo(map).bindPopup('Départ');}
}else map.setView(loc,12);
setTimeout(()=>map?.invalidateSize(),100);
}catch(_){drawFallback(place);status((data.note?data.note+' ':'')+'Carte simplifiée affichée : le module interactif n’a pas pu être chargé.');}
}
function show(data){
if(!data||data.type!=='dona_navigation')return;
selected=data.destination||null;open();
$('dnDestination').value=String(data.destination||'').slice(0,180);
if(data.origin)$('dnOrigin').value=String(data.origin).slice(0,180);
$('dnPlace').textContent=data.place?.name||String(data.destination||'Destination indisponible');
const info=$('dnDetails');info.replaceChildren();
if(data.route){info.append(el('strong','',data.route.distance_km+' km · '+data.route.duration_min+' min'),
el('p','','Durée estimée, sans trafic temps réel.'));}
else info.append(el('p','','Itinéraire non calculé — ouvre Waze pour le guidage.'));
const waze=safeUrl(data.links?.waze,'waze.com'),maps=safeUrl(data.links?.google_maps,'google.com');
for(const [id,url] of [['dnWaze',waze],['dnMaps',maps]]){const a=$(id);a.hidden=!url;if(url)a.href=url;else a.removeAttribute('href');}
$('dnSend').disabled=!selected;
status(data.error||data.send_error||(data.sent?'Itinéraire envoyé sur ton téléphone.':data.note||'Destination trouvée.'));
drawMap(data);
}
async function search(destination,origin=''){
if(loading)return;destination=String(destination||'').trim();origin=String(origin||'').trim();
if(destination.length<2||destination.length>180){status('Indique une destination valide.');return;}
loading=true;$('dnSubmit').disabled=true;open();status('Recherche de la destination et de l’itinéraire…');
try{
const res=await fetch('/api/dona/navigation/search',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({destination,origin})});
const data=await res.json();if(!res.ok||!data.ok)throw new Error(data.error||'Recherche indisponible.');
show({...data,type:'dona_navigation'});
}catch(e){status(e.message||'Erreur de navigation.');}
finally{loading=false;$('dnSubmit').disabled=false;}
}
async function sendPhone(){
if(!selected){status('Choisis d’abord une destination.');return;}
const b=$('dnSend');b.disabled=true;status('Envoi vers Home Assistant…');
try{
const resp=await fetch('/api/dona/navigation/send',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({destination:selected})});
const data=await resp.json();if(!resp.ok||!data.ok)throw new Error(data.error||'Envoi impossible.');status(data.message);
}catch(e){status(e.message||'Impossible d’envoyer le trajet.');}finally{b.disabled=false;}
}
function connectSocket(){const sk=window.alfredSocket;if(!sk){setTimeout(connectSocket,250);return;}sk.on('alfred_render',p=>{if(p?.type==='dona_navigation')show(p);});}
window.DonaNavigationV83={open,close,show,search,sendPhone};initialize();
})();
@@ -0,0 +1,63 @@
/* DONA_V7_AUTO_CLOSE : cinq secondes après FIN audio, pas la fin LLM.
Les demandes écrites silencieuses gardent le délai de 60s. */
(() => {
'use strict';
if(window.__donaReplyAutoCloseV7) return;
window.__donaReplyAutoCloseV7=true;
let timer=0,mode='idle',serial=0,hasSpeech=false,ttsFinished=false;
const main=()=>document.getElementById('alfredMainV1');
const focused=()=>main()?.classList.contains('is-focused');
const textResponse=()=>!document.getElementById('content')?.classList.contains('alfred-view');
const clear=()=>{if(timer)clearTimeout(timer);timer=0;};
const modal=()=>document.querySelector('dialog[open], [role="dialog"][aria-modal="true"],.dona-spotify-overlay:not([hidden]),.dona-face-panel:not([hidden])');
const schedule=ms=>{
clear();const mine=serial;
if(!focused()||!textResponse()||(mode==='speaking'&&!ttsFinished)||mode==='thinking'||mode==='listening')return;
timer=setTimeout(()=>{
timer=0;
if(mine!==serial||!focused()||!textResponse()||(mode==='speaking'&&!ttsFinished)||mode==='thinking'||mode==='listening')return;
if(modal()){schedule(1500);return;}
document.getElementById('avBack')?.click();
},ms);
};
const startInteraction=()=>{serial++;clear();ttsFinished=false;hasSpeech=false;};
const socket=window.alfredSocket;
socket?.on('alfred_state',data=>{
const next=String(data?.mode||'idle').toLowerCase();
if((next==='thinking'||next==='listening'||next==='speaking') && mode!==next){
if(next==='thinking'||next==='listening')startInteraction();
clear();
}
if(next==='speaking')hasSpeech=true;
mode=next;
if(next==='idle'&&focused()&&!timer){
// Fin audio: on_end a déjà eu lieu ou l'événement peut arriver un peu après idle.
if(ttsFinished)schedule(5000);
else if(!hasSpeech)schedule(60000);
}
});
socket?.on('alfred_tts_finished',()=>{
ttsFinished=true;hasSpeech=true;
// Piper est terminé ; si le mode conversation reste actif, une nouvelle demande annule le délai.
if(focused() && mode!=='thinking' && mode!=='listening')schedule(5000);
});
socket?.on('alfred_render',data=>{
if(data?.type==='home'){clear();serial++;return;}
if(focused() && !hasSpeech && mode==='idle')schedule(60000);
});
const onFocused=()=>{
if(!focused()){clear();serial++;return;}
if(mode==='idle'&&!hasSpeech)schedule(60000);
};
const init=()=>{
if(!main())return;
new MutationObserver(onFocused).observe(main(),{attributes:true,attributeFilter:['class']});
document.addEventListener('pointerdown',e=>{
// L'utilisateur explore la carte : repousser la fermeture sans l'empêcher indéfiniment.
if(focused()&&e.target?.closest('#stage'))schedule(ttsFinished?5000:60000);
},{passive:true});
document.addEventListener('visibilitychange',()=>{if(document.hidden)clear();else if(focused()&&mode==='idle')schedule(ttsFinished?5000:60000);});
onFocused();
};
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
})();
+94
View File
@@ -0,0 +1,94 @@
/* Dona V6: Spotify Connect — live player, REST-only polling; voice/text keep Hermes. */
(() => {
'use strict';
const $ = id => document.getElementById(id);
const state = { data: null, expanded: false, loaded: false, busy: false, timestamp: 0 };
const icons = {play:'▶',pause:'Ⅱ',prev:'⏮',next:'⏭',music:'♫'};
function node(tag,cls,txt){const e=document.createElement(tag); if(cls)e.className=cls;if(txt)e.textContent=txt;return e;}
function fmt(ms){const seconds=Math.floor(Math.max(0,ms)/1000);return Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0');}
function progress(){
const d=state.data;if(!d||!d.available)return 0;
return Math.min(d.duration_ms||0, Math.max(0,(d.progress_ms||0)+(d.is_playing?Date.now()-state.timestamp:0)));
}
function init(){
const main=$('alfredMainV1');if(!main){setTimeout(init,180);return;}
if($('donaSpotifyPanel'))return;
const head=main.querySelector('.av-head-actions');
if(head){const open=node('button','av-round dona-music-open',icons.music);open.type='button';open.title='Lecteur Spotify';open.setAttribute('aria-label','Ouvrir le lecteur Spotify');open.id='donaSpotifyOpen';head.prepend(open);open.addEventListener('click',()=>toggle(true));}
const mini=node('button','dona-spotify-mini');mini.id='donaSpotifyMini';mini.type='button';mini.hidden=true;mini.innerHTML='<span class="dona-mini-note">♫</span><span id="donaMiniTitle">Spotify</span><span id="donaMiniArtist"></span><span class="dona-mini-arrow">⌃</span>';
mini.addEventListener('click',()=>toggle(true));document.body.append(mini);
const overlay=node('div','dona-spotify-overlay');overlay.id='donaSpotifyOverlay';overlay.hidden=true;
overlay.innerHTML=`<section id="donaSpotifyPanel" role="dialog" aria-modal="true" aria-label="Lecteur Spotify" class="dona-spotify-panel">
<div class="dona-spotify-head"><span>♫ &nbsp;SPOTIFY CONNECT</span><button type="button" id="donaSpotifyClose" aria-label="Fermer le lecteur">×</button></div>
<div class="dona-spotify-art"><img id="donaSpotifyCover" alt="Pochette de l’album" hidden><span id="donaSpotifyFallback">♫</span></div>
<div class="dona-spotify-track"><strong id="donaSpotifyTitle">Aucune musique en cours</strong><span id="donaSpotifyArtist">Lance un morceau sur un appareil Spotify.</span><small id="donaSpotifyAlbum"></small></div>
<div class="dona-spotify-progress"><input id="donaSpotifySeek" type="range" min="0" max="100" value="0" step="1000" aria-label="Avancer dans le morceau"><div class="dona-spotify-timing"><span id="donaSpotifyElapsed">0:00</span><span id="donaSpotifyDuration">0:00</span></div></div>
<div class="dona-spotify-actions"><button type="button" data-saction="previous" aria-label="Morceau précédent">⏮</button><button type="button" data-saction="toggle" id="donaSpotifyPause" class="dona-spotify-primary" aria-label="Lecture ou pause">▶</button><button type="button" data-saction="next" aria-label="Morceau suivant">⏭</button></div>
<div class="dona-spotify-settings"><label for="donaSpotifyDevice">Sortie audio</label><select id="donaSpotifyDevice" aria-label="Appareil Spotify"></select><label for="donaSpotifyVolume">Volume <span id="donaSpotifyVolumeNum">—</span></label><input type="range" min="0" max="100" id="donaSpotifyVolume" aria-label="Volume Spotify"></div>
<p class="dona-spotify-hint">Tu peux aussi dire « Dona, mets en pause », « morceau suivant » ou écrire ta demande dans le champ principal.</p>
<p id="donaSpotifyMessage" class="dona-spotify-message" role="status" aria-live="polite"></p>
</section>`;
overlay.addEventListener('click',e=>{if(e.target===overlay)toggle(false);});document.body.append(overlay);
$('donaSpotifyClose').addEventListener('click',()=>toggle(false));
overlay.querySelectorAll('[data-saction]').forEach(b=>b.addEventListener('click',()=>{
const action=b.dataset.saction==='toggle'?(state.data?.is_playing?'pause':'play'):b.dataset.saction;
send(action);
}));
$('donaSpotifySeek').addEventListener('change',e=>send('seek',{position_ms:Number(e.target.value)}));
$('donaSpotifyVolume').addEventListener('change',e=>send('volume',{volume_percent:Number(e.target.value)}));
$('donaSpotifyDevice').addEventListener('change',e=>{if(e.target.value)send('transfer',{device_id:e.target.value});});
window.addEventListener('keydown',e=>{if(e.key==='Escape'&&state.expanded)toggle(false);});
setInterval(updateBar,1000);
setInterval(()=>{if(!document.hidden)load();},12000);
document.addEventListener('visibilitychange',()=>{if(!document.hidden)load();});
load();
}
function toggle(flag){state.expanded=flag;const overlay=$('donaSpotifyOverlay');overlay.hidden=!flag;document.body.classList.toggle('dona-spotify-open',flag);if(flag)load();}
function message(txt){const m=$('donaSpotifyMessage');if(m)m.textContent=txt;}
function updateBar(){
const d=state.data;if(!d)return;
$('donaSpotifyElapsed').textContent=fmt(progress());$('donaSpotifyDuration').textContent=fmt(d.duration_ms||0);
const seek=$('donaSpotifySeek');if(document.activeElement!==seek){seek.max=Math.max(1,d.duration_ms||1);seek.value=Math.min(seek.max,progress());seek.disabled=!d.available;}
}
function redraw(){
const d=state.data;if(!d)return;
const active=Boolean(d.available);
$('donaSpotifyMini').hidden=!active;
$('donaMiniTitle').textContent=d.title||'Spotify';$('donaMiniArtist').textContent=d.artist||'';
$('donaSpotifyTitle').textContent=d.title||(d.devices?.length?'Aucune lecture active':'Aucun appareil connecté');
$('donaSpotifyArtist').textContent=d.artist||(d.devices?.length?'Choisis un appareil et lance un morceau.':'Ouvre Spotify sur un appareil.');
$('donaSpotifyAlbum').textContent=d.album||'';
const cover=$('donaSpotifyCover');cover.hidden=!d.cover;if(d.cover)cover.src=d.cover;else cover.removeAttribute('src');
$('donaSpotifyFallback').hidden=!!d.cover;
$('donaSpotifyPause').textContent=d.is_playing?icons.pause:icons.play;
$('donaSpotifyPause').setAttribute('aria-label',d.is_playing?'Mettre en pause':'Reprendre la lecture');
const select=$('donaSpotifyDevice');const chosen=select.value;
select.replaceChildren();
const option=node('option','',d.device_name||'Choisir un appareil');option.value='';select.append(option);
(d.devices||[]).forEach(x=>{const o=node('option','',x.name||x.id);o.value=x.id;select.append(o);});
select.value=(d.devices||[]).some(x=>x.id===d.device_id)?d.device_id:(chosen&&[...select.options].some(x=>x.value===chosen)?chosen:'');
select.disabled=!d.devices?.length;
const volume=$('donaSpotifyVolume');if(document.activeElement!==volume && Number.isFinite(d.volume_percent))volume.value=d.volume_percent;
$('donaSpotifyVolumeNum').textContent=Number.isFinite(d.volume_percent)?d.volume_percent+' %':'—';
updateBar();
}
async function load(){
if(state.busy)return;
state.busy=true;
try{
const r=await fetch('/api/dona/spotify/state',{cache:'no-store',credentials:'same-origin'});
const d=await r.json();if(!r.ok||!d.ok)throw Error(d.error||'Spotify non disponible');
state.data=d;state.timestamp=Date.now();state.loaded=true;redraw();message('');
}catch(e){if(state.expanded||!state.loaded)message(e.message||'Spotify indisponible');}
finally{state.busy=false;}
}
async function send(action,extra={}){
message('Commande en cours…');
try{
const r=await fetch('/api/dona/spotify/control',{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,...extra})});
const data=await r.json();if(!r.ok||!data.ok)throw Error(data.error||'Commande refusée');
message('');await new Promise(resolve=>setTimeout(resolve,450));await load();
}catch(e){message(e.message||'Commande indisponible');}
}
init();
})();
+153
View File
@@ -0,0 +1,153 @@
/* 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=>render(result,lastPrompt));
}
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
window.DonaSuiteV9={open,close,render,ask};
})();
+137
View File
@@ -0,0 +1,137 @@
/* Correctif visuel Dona : titre d'accueil et dégagement de la reconnaissance vocale.
Ne touche ni à Hermes, ni à Vosk, ni aux réglages du mot d'appel. */
(() => {
'use strict';
const LABEL = 'RECONNAISSANCE VOCALE';
const COMPOSER = 'Écrire une demande à';
let pending = false;
let lastStatus = null;
let lastDock = null;
let resizeObserver = null;
const offsets = new WeakMap();
function visible(el) {
if (!el || el.nodeType !== 1) return false;
const s = getComputedStyle(el);
const r = el.getBoundingClientRect();
return s.display !== 'none' && s.visibility !== 'hidden' &&
r.width > 0 && r.height > 0;
}
function textNodes() {
const root = document.body;
if (!root) return [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(n) {
const p = n.parentElement;
if (!p || /^(SCRIPT|STYLE|NOSCRIPT|TEXTAREA)$/i.test(p.tagName))
return NodeFilter.FILTER_REJECT;
return n.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
});
const nodes = [];
while (walker.nextNode()) nodes.push(walker.currentNode);
return nodes;
}
function correctTitle(nodes) {
for (const n of nodes) {
if (n.textContent.trim() !== 'Alfred' || !visible(n.parentElement)) continue;
const el = n.parentElement;
const size = parseFloat(getComputedStyle(el).fontSize) || 0;
const heading = el.closest('h1,h2,[role="heading"]');
// Uniquement le grand titre d'accueil ; ne pas renommer les autres mentions.
if (size >= 32 || (heading && parseFloat(getComputedStyle(heading).fontSize) >= 32)) {
n.textContent = n.textContent.replace(/Alfred/, 'Dona');
}
}
}
function findDock(nodes) {
// V6: the compose label is intentionally hidden; locate the compact bar by ID.
const compact = document.querySelector('.dona-compose-compact');
if (visible(compact)) return compact;
const found = nodes.find(n => n.textContent.includes(COMPOSER) && visible(n.parentElement));
if (!found) return null;
let best = null;
let bestScore = -Infinity;
let el = found.parentElement;
for (let i = 0; i < 8 && el && el !== document.body; i++, el = el.parentElement) {
const r = el.getBoundingClientRect();
if (!visible(el) || r.height < 45 || r.height > 265 || r.width < 250 ||
r.top < window.innerHeight * 0.45) continue;
const css = getComputedStyle(el);
const fixed = /^(fixed|sticky)$/.test(css.position);
const radius = parseFloat(css.borderTopLeftRadius) || 0;
const score = (fixed ? 30 : 0) + (radius > 12 ? 14 : 0) +
(r.width > window.innerWidth * 0.55 ? 10 : 0) -
Math.abs(r.bottom - (window.innerHeight - 16)) / 12 -
Math.abs(r.height - 85) / 10;
if (score > bestScore) { bestScore = score; best = el; }
}
return best;
}
function findVoiceRegion(nodes, dock) {
const found = nodes.find(n => n.textContent.includes(LABEL) && visible(n.parentElement));
if (!found) return null;
let el = found.parentElement;
let candidate = el;
for (let i = 0; i < 9 && el && el !== document.body; i++, el = el.parentElement) {
const r = el.getBoundingClientRect();
if (dock && el.contains(dock)) break;
const text = (el.innerText || '').trim();
if (r.height > 300 || text.includes('Prêt à vous aider') || text.length > 1100) break;
// Prendre le groupe entier (statut Vosk + transcription), pas la seule étiquette.
if (r.height >= 25 && visible(el)) candidate = el;
}
return candidate;
}
function update() {
pending = false;
const nodes = textNodes();
correctTitle(nodes);
const dock = findDock(nodes);
const status = findVoiceRegion(nodes, dock);
if (resizeObserver) {
if (lastStatus && lastStatus !== status) resizeObserver.unobserve(lastStatus);
if (lastDock && lastDock !== dock) resizeObserver.unobserve(lastDock);
if (status && lastStatus !== status) resizeObserver.observe(status);
if (dock && lastDock !== dock) resizeObserver.observe(dock);
}
lastStatus = status;
lastDock = dock;
if (!status) return;
const prev = offsets.get(status) || 0;
const rect = status.getBoundingClientRect();
const originalBottom = rect.bottom + prev;
const topLimit = dock ? dock.getBoundingClientRect().top - 24 : window.innerHeight - 22;
const offset = dock ? Math.max(0, Math.ceil(originalBottom - topLimit)) : 0;
const safeOffset = Math.min(offset, Math.max(0, rect.top + prev - 70));
if (Math.abs(safeOffset - prev) >= 1) {
status.style.translate = safeOffset ? `0 -${safeOffset}px` : '';
offsets.set(status, safeOffset);
}
}
function schedule() {
if (pending) return;
pending = true;
requestAnimationFrame(update);
}
function init() {
if ('ResizeObserver' in window) resizeObserver = new ResizeObserver(schedule);
const observer = new MutationObserver(schedule);
observer.observe(document.body, { subtree: true, childList: true, characterData: true });
window.addEventListener('resize', schedule, {passive: true});
window.addEventListener('orientationchange', schedule, {passive: true});
if (window.visualViewport) window.visualViewport.addEventListener('resize', schedule, {passive:true});
schedule();
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init, {once:true});
else init();
})();
+185
View File
@@ -0,0 +1,185 @@
/* DONA_YOUTUBE_V82: public YouTube tutorial viewer; no API key by default. */
(() => {
'use strict';
if (window.DonaYouTubeV82) return;
const ID = 'donaYoutubeV82';
const videoIdOK = id => typeof id === 'string' && /^[A-Za-z0-9_-]{11}$/.test(id);
const $ = id => document.getElementById(id);
const el = (tag, cls, value) => {
const n = document.createElement(tag);
if (cls) n.className = cls;
if (value !== undefined) n.textContent = String(value);
return n;
};
const button = (title, fn, cls='dy-btn') => {
const b = el('button', cls, title); b.type='button'; b.addEventListener('click',fn); return b;
};
let player=null, ready=false, active=false, items=[], chosen='', requestBusy=false;
let apiPromise=null, playerVersion=0;
const label = s => { if($('dyStatus')) $('dyStatus').textContent=s||''; };
const postState = state => fetch('/api/dona/youtube/player-state', {
method:'POST', credentials:'same-origin',
headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},
body:JSON.stringify({active:state})
}).catch(()=>{});
function initialize(){
const root = document.getElementById('alfredMainV1');
if(!root){setTimeout(initialize,200);return;}
const head = root.querySelector('.av-head-actions');
if(head){
const trigger=button('▶',()=>open(),'av-round dy-open-button');
trigger.title='Ouvrir YouTube';trigger.setAttribute('aria-label','Ouvrir le lecteur YouTube');
trigger.id='donaYouTubeOpen';head.prepend(trigger);
}
const overlay=el('div','dy-overlay');overlay.id=ID;overlay.hidden=true;
const panel=el('section','dy-panel');
panel.setAttribute('role','dialog');panel.setAttribute('aria-modal','true');panel.setAttribute('aria-label','Lecteur YouTube de Dona');
const top=el('header','dy-header');
const h=el('div');h.append(el('span','dy-eyebrow','DONA · YOUTUBE'),el('h1','','Tutoriels et vidéos'));
top.append(h,button('×',close,'dy-close'));panel.append(top);
const search=el('form','dy-search');search.id='dySearchForm';
const input=el('input','dy-input');input.id='dyQuery';input.type='search';input.maxLength=200;
input.placeholder='Chercher une vidéo YouTube…';input.setAttribute('aria-label','Rechercher une vidéo');input.required=true;
const submit=button('Rechercher',()=>{},'dy-btn dy-primary');submit.type='submit';submit.id='dySubmit';
search.append(input,submit);search.addEventListener('submit',e=>{e.preventDefault();searchFor(input.value);});panel.append(search);
const status=el('p','dy-status');status.id='dyStatus';status.setAttribute('role','status');status.setAttribute('aria-live','polite');panel.append(status);
const body=el('div','dy-body');
const viewer=el('div','dy-viewer');
const stage=el('div','dy-stage');stage.id='dyStage';
stage.append(el('div','dy-placeholder','Sélectionne un tutoriel pour afficher sa vidéo.'));
viewer.append(stage);
const controls=el('div','dy-controls');
controls.append(button('↶ 30 s',()=>control({action:'seek',seconds:-30})),
button('▶',()=>control({action:'play'})),button('Ⅱ',()=>control({action:'pause'})),
button('30 s ↷',()=>control({action:'seek',seconds:30})),
button('⛶',()=>control({action:'fullscreen'})));
viewer.append(controls);
const link=el('a','dy-external','Voir sur YouTube ↗');link.id='dyExternal';link.hidden=true;link.target='_blank';link.rel='noopener noreferrer';
viewer.append(link);
const results=el('div','dy-results');
results.append(el('h2','','Vidéos trouvées'));
const cards=el('div','dy-results-list');cards.id='dyResults';results.append(cards);
body.append(viewer,results);panel.append(body);
panel.append(el('p','dy-note','Vidéos publiques YouTube · Lecture depuis le site officiel · Sans connexion à un compte. Certaines vidéos peuvent refuser l’intégration.'));
overlay.append(panel);document.body.append(overlay);
overlay.addEventListener('click',e=>{if(e.target===overlay)close();});
window.addEventListener('keydown',e=>{if(e.key==='Escape'&&active)close();});
bindSocket();
}
function open(){active=true;$(ID).hidden=false;document.body.classList.add('dy-active');postState(true);}
function close(){
active=false;$(ID).hidden=true;document.body.classList.remove('dy-active');postState(false);
// Stop playback when leaving YouTube; avoid hidden audio.
try { if(player&&typeof player.stopVideo==='function')player.stopVideo(); } catch(_){}
}
function loadAPI(){
if(window.YT && typeof window.YT.Player==='function')return Promise.resolve();
if(apiPromise)return apiPromise;
apiPromise=new Promise((resolve,reject)=>{
const previous=window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady=()=>{try{if(typeof previous==='function')previous();}catch(_){ } resolve();};
const script=document.createElement('script');script.src='https://www.youtube.com/iframe_api';
script.async=true;script.onerror=()=>reject(new Error('Lecteur YouTube inaccessible.'));
document.head.append(script);
setTimeout(()=>{if(!(window.YT&&window.YT.Player))reject(new Error('Le lecteur YouTube ne répond pas.'));},15000);
});
return apiPromise;
}
async function loadVideo(id){
if(!videoIdOK(id))return;
chosen=id;open();
const item=items.find(x=>x.id===id);
$('dyExternal').href='https://www.youtube.com/watch?v='+id;
$('dyExternal').hidden=false;
$('dyStage').replaceChildren();
const mount=el('div','dy-frame');mount.id='dyPlayer';$('dyStage').append(mount);
const version=++playerVersion;
ready=false;
try { if(player&&typeof player.destroy==='function')player.destroy(); }catch(_){ }
player=null;
label(item ? 'Vidéo sélectionnée : '+item.title : 'Chargement de la vidéo…');
try {
await loadAPI();if(version!==playerVersion)return;
player=new window.YT.Player('dyPlayer', {
width:'100%', height:'100%', videoId:id,
playerVars:{autoplay:0,controls:1,playsinline:1,rel:0,origin:window.location.origin},
events:{
onReady:()=>{ready=true;label('Appuie sur Lecture pour démarrer la vidéo.');},
onError:(event)=>label('Cette vidéo ne peut pas être lue ici (code '+event.data+'). Utilise « Voir sur YouTube ».')
}
});
} catch(err) {
if(version===playerVersion){
$('dyStage').replaceChildren(el('p','dy-placeholder','Le lecteur intégré est indisponible. Ouvre cette vidéo sur YouTube.'));
label(err.message||'Lecteur indisponible.');
}
}
}
function control(command){
if(!active||!command||!command.action)return;
if(command.action==='close'){close();return;}
if(!ready||!player){label('Le lecteur n’est pas encore prêt.');return;}
try {
switch(command.action){
case 'play':player.playVideo();break;
case 'pause':player.pauseVideo();break;
case 'seek':{
const offset=Number(command.seconds)||0;
player.seekTo(Math.max(0,(Number(player.getCurrentTime())||0)+offset),true);break;
}
case 'fullscreen':{
const frame=$('dyStage').querySelector('iframe');
if(frame&&frame.requestFullscreen)frame.requestFullscreen();break;
}
}
} catch(e){label('Commande vidéo indisponible : '+e.message);}
}
function showResults(payload){
if(!payload||payload.type!=='dona_youtube')return;
items=(Array.isArray(payload.videos)?payload.videos:[]).filter(v=>v&&videoIdOK(v.id)).slice(0,6);
$('dyQuery').value=String(payload.query||'').slice(0,200);
const list=$('dyResults');list.replaceChildren();
for(const v of items){
const b=button('',()=>loadVideo(v.id),'dy-video-item');b.textContent='';
const img=el('img');img.loading='lazy';img.alt='Miniature : '+String(v.title||'Vidéo');
img.src='https://i.ytimg.com/vi/'+v.id+'/hqdefault.jpg';
const txt=el('span','dy-video-title');txt.append(el('strong','',v.title||'Vidéo YouTube'));
if(v.channel)txt.append(el('small','',v.channel));b.append(img,txt);list.append(b);
}
open();
if(payload.error)label(payload.error);
else if(items.length)label(items.length+' vidéo(s) trouvée(s). Choisis celle qui t’intéresse.');
else label('Aucune vidéo exploitable trouvée. Essaie une autre formulation.');
if(items.length)loadVideo(items[0].id);
else {chosen='';ready=false;playerVersion++;try{player?.destroy?.()}catch(_){ }player=null;
$('dyStage').replaceChildren(el('p','dy-placeholder','Aucun résultat : tente une autre recherche.'));
$('dyExternal').hidden=true;
}
}
async function searchFor(query){
query=String(query||'').trim();
if(query.length<2||query.length>200){label('Saisis entre 2 et 200 caractères.');return;}
if(requestBusy)return;
requestBusy=true;$('dySubmit').disabled=true;open();label('Dona recherche des vidéos via Hermes…');
try {
const res=await fetch('/api/dona/youtube/search',{
method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},
body:JSON.stringify({query})
});
const data=await res.json();
if(!res.ok||!data.ok)throw new Error(data.error||'Recherche impossible');
showResults({type:'dona_youtube',query:data.query,videos:data.videos});
}catch(e){label(e.message||'Recherche indisponible.');}
finally{requestBusy=false;$('dySubmit').disabled=false;}
}
function bindSocket(){
const socket=window.alfredSocket;
if(!socket){setTimeout(bindSocket,250);return;}
socket.on('alfred_render',payload=>{if(payload?.type==='dona_youtube')showResults(payload);});
socket.on('dona_youtube_control',command=>control(command));
}
window.DonaYouTubeV82={open,close,control,showResults,search:searchFor};
initialize();
})();