105 lines
8.2 KiB
JavaScript
105 lines
8.2 KiB
JavaScript
/* 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();
|
|
})();
|