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,108 @@
/* 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);
// Collapsible multi-line composition field. No TTS on typed requests.
const compose=document.createElement('div');compose.className='dona-compose';
compose.innerHTML=`<button class="dona-compose-open" type="button" id="donaComposeToggle" aria-expanded="false" aria-controls="donaComposeBody"><span>✎</span> Écrire une demande à Dona… <span aria-hidden="true">⌃</span></button>
<form class="dona-compose-body" id="donaComposeBody" hidden>
<label for="donaComposeInput">Ta demande écrite à Hermes</label>
<div class="dona-compose-row"><textarea id="donaComposeInput" maxlength="1600" rows="2" placeholder="Ex. Résume mes cinq derniers mails, sans en envoyer." required></textarea><button id="donaComposeSend" type="submit">Envoyer ➤</button></div>
<div class="dona-compose-foot"><small>Entrée pour envoyer · Maj + Entrée pour une nouvelle ligne · réponse sans synthèse vocale</small><span id="donaComposeStatus" aria-live="polite"></span></div>
</form>`;
root.append(compose);
const toggle=$('donaComposeToggle'),body=$('donaComposeBody'),input=$('donaComposeInput'),send=$('donaComposeSend'),status=$('donaComposeStatus');
function openCompose(value){body.hidden=!value;toggle.setAttribute('aria-expanded',String(value));compose.classList.toggle('is-open',value);if(value)input.focus();}
toggle.addEventListener('click',()=>openCompose(body.hidden));
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&!body.hidden&&!root.classList.contains('is-focused'))openCompose(false);});
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,134 @@
/* 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) {
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();
})();