v3
This commit is contained in:
@@ -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();
|
||||
})();
|
||||
Reference in New Issue
Block a user