debut illustration

This commit is contained in:
pporcheret
2026-11-02 08:50:05 +01:00
parent e2340807aa
commit c2a4f7eb26
44 changed files with 6856 additions and 19 deletions
+60 -12
View File
@@ -10,6 +10,27 @@
};
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;
// DONA_DYNAMIC_PERF_V2 — HTTP 202 is queue acceptance, not generation completion.
let creating=false, createTimer=null;
const CREATE_MAX_WAIT_MS=240000;
function updateCreationControls(){
const root=document.getElementById(ID);if(!root)return;
root.querySelectorAll('.d8-submit, .d8-examples button').forEach(button=>{
button.disabled=creating;button.setAttribute('aria-disabled',String(creating));
});
}
function finishCreating(){
if(createTimer!==null){clearTimeout(createTimer);createTimer=null;}
creating=false;updateCreationControls();
}
function startCreating(){
creating=true;updateCreationControls();
if(createTimer!==null)clearTimeout(createTimer);
createTimer=setTimeout(()=>{
finishCreating();
if(opened)status('Confirmation non reçue. Tu peux réessayer ; une génération peut encore être en cours côté serveur.');
},CREATE_MAX_WAIT_MS);
}
const KEY='dona.dynamic.pins.v8';
const safeGetPins=()=>{
try{const pins=JSON.parse(localStorage.getItem(KEY)||'[]');return Array.isArray(pins)?pins.slice(0,8):[];}
@@ -50,10 +71,10 @@
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;
function schedule(){stopTimer();if(!opened||!active||!['spotify','homeassistant','lights','proxmox'].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';}
function originLabel(origin){return ({spotify:'Spotify · direct',homeassistant:'Home Assistant · direct',lights:'Home Assistant · direct',hermes:'Hermes · MCP',fallback:'Hermes · réponse texte',proxmox:'Proxmox · API directe' /* DONA_DYNAMIC_PROXMOX_V3 */})[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);
@@ -128,18 +149,30 @@
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;
const obj=await res.json().catch(()=>({}));if(!res.ok||obj.ok===false){const error=Error(obj.error||'HTTP '+res.status);error.httpStatus=res.status;throw error;}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;}
if(creating){status('Une génération est déjà en cours.');return;}
if(busy){status('Une actualisation est en cours.');return;}
startCreating();status('Dona prépare le tableau…');
try{
await post('/api/dona/dynamic/create',{prompt:query});
// A fast Socket.IO completion may have already cleared creating.
if(creating)status('Dona consulte les données ; la vue apparaîtra ici.');
}catch(e){
if(e.httpStatus===409){
// Another tab or another entry point may have submitted the job.
// Wait for its completion event, or release after the safety timeout.
status('Une génération est déjà en cours côté serveur. Attente du résultat…');
}else{
finishCreating();status(e.message||'Demande impossible.');
}
}
}
async function reloadLive(notify=true){
if(!active)return;
if(['spotify','homeassistant','lights'].includes(active.origin)){
if(['spotify','homeassistant','lights','proxmox'].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');
@@ -163,14 +196,29 @@
primary.prepend(b);
}
let tries=0;const timer=setInterval(()=>{initButton();if($('d8Open')||++tries>70)clearInterval(timer);},150);
// DONA_UNIFIED_ROUTING_V4 — conserver le tableau quand Dona confirme à voix haute.
let v4IgnoreNextTextUntil=0;
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();
if(data?.type==='dona_dynamic' && data.dona_fast_v4===true)
v4IgnoreNextTextUntil=Date.now()+15000;
if(data?.type==='dona_dynamic'){
// Only a server result (not a pinned board or HTTP 202) ends the wait.
if(data.dashboard?.schema==='dona.dashboard.v1'&&Array.isArray(data.dashboard.cards))finishCreating();
render(data.dashboard);
}
else if(opened && data?.type!=='dona_dynamic'){
if(v4IgnoreNextTextUntil && Date.now()<v4IgnoreNextTextUntil){
v4IgnoreNextTextUntil=0; // Ignorer UNE confirmation de la commande directe.
}else{v4IgnoreNextTextUntil=0;close();}
}
});
socket?.on('alfred_state',data=>{
if(opened && data?.mode==='error')status('Erreur lors de la préparation du tableau.');
if(opened && data?.mode==='error'){
if(creating)finishCreating();
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);});
document.addEventListener('visibilitychange',()=>{if(!document.hidden&&opened&&active&&['spotify','homeassistant','lights','proxmox'].includes(active.origin))reloadLive(false);});
window.DonaDynamicV8={open,render,close};
})();
@@ -0,0 +1,56 @@
/* Dona Hermes CLI streaming overlay. Unobtrusive; never modifies existing cards. */
(() => {
'use strict';
if (typeof window.io !== 'function' || window.__donaHermesStreamV1) return;
window.__donaHermesStreamV1 = true;
const el = document.createElement('aside');
el.id = 'dona-hermes-live-v1';
el.setAttribute('aria-live', 'polite');
el.setAttribute('aria-label', 'Progression de la réponse de Dona');
el.hidden = true;
const label = document.createElement('div');
label.className = 'dona-hlive-title';
const body = document.createElement('div');
body.className = 'dona-hlive-text';
el.append(label, body);
document.body.appendChild(el);
let activeId = null, contents = '', closeTimer = null;
const close = (delay = 1000) => {
clearTimeout(closeTimer);
closeTimer = setTimeout(() => { el.hidden = true; activeId = null; }, delay);
};
const socket = window.io({ reconnection: true });
socket.on('dona_hermes_stream', (event) => {
if (!event || !event.type || !event.id) return;
if (event.type === 'start') {
activeId = event.id;
contents = '';
clearTimeout(closeTimer);
label.textContent = 'Dona réfléchit…';
body.textContent = '';
el.hidden = false;
return;
}
if (event.id !== activeId) return;
if (event.type === 'first_text') {
label.textContent = 'Dona répond…';
} else if (event.type === 'delta') {
// Avoid unlimited DOM growth; normal Dona cards remain the final source.
contents = (contents + String(event.text || '')).slice(-1200);
body.textContent = contents;
} else if (event.type === 'tool') {
label.textContent = 'Dona utilise un outil…';
body.textContent = String(event.name || 'Outil en cours');
contents = '';
} else if (event.type === 'tool_done') {
label.textContent = 'Dona prépare sa réponse…';
} else if (event.type === 'end') {
close(1100);
} else if (event.type === 'error') {
label.textContent = 'Traitement interrompu';
close(2300);
}
});
socket.on('disconnect', () => { el.hidden = true; activeId = null; });
})();
+95
View File
@@ -0,0 +1,95 @@
/* Dona Visual Context v1 — safe DOM-only renderer, no HTML from Hermes. */
(() => {
"use strict";
const EVENT = 'dona_visual_card';
const ACCEPT = new Set(['action_state', 'recipe', 'media', 'weather', 'calendar', 'info']);
const PATH = (document.currentScript && document.currentScript.src) || '';
const SVG = {
'lightbulb-on': '<path d="M9 18h6m-5 4h4m-5-7a7 7 0 1 1 6 0c-.6.5-1 1.3-1 2H10c0-.7-.4-1.5-1-2Z"/><path d="M12 1v2M4 4l2 2M20 4l-2 2M1 12h2M21 12h2"/>',
'lightbulb-off': '<path d="M9 18h6m-5 4h4M5 5l14 14M10 15c-2.8-2.8-3.4-6.2-.8-9.1 3.1-3 7.7-2 9.3 1.4"/><path d="M12 1v2M1 12h2M21 12h2"/>',
'chef-hat': '<path d="M6 13a4 4 0 0 1-2-7 4 4 0 0 1 6-2 4.5 4.5 0 0 1 8 2 4 4 0 0 1 0 7v7H6zM6 16h12"/>',
'music': '<path d="M10 19V5l11-2v14M10 9l11-2"/><ellipse cx="7" cy="19" rx="3" ry="2"/><ellipse cx="18" cy="17" rx="3" ry="2"/>',
'cloud-sun': '<circle cx="18" cy="6" r="3"/><path d="M18 1v1M18 10v1M13 6h1M22 6h1M4 18a5 5 0 0 1 5-5 6 6 0 0 1 11 3 4 4 0 0 1-1 7H7a4 4 0 0 1-3-5Z"/>',
'calendar': '<rect x="3" y="5" width="18" height="17" rx="2"/><path d="M7 2v6M17 2v6M3 11h18"/>',
'check': '<path d="m5 12 4 4L19 6"/>',
'power': '<path d="M12 2v10M5 5a10 10 0 1 0 14 0"/>',
'alert': '<path d="M12 2 1 22h22L12 2Zm0 8v5m0 3v1"/>',
'thermometer': '<path d="M10 14V5a3 3 0 0 1 6 0v9a5 5 0 1 1-6 0Z"/>',
'lock': '<rect x="5" y="10" width="14" height="12" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0v3"/>',
'unlock': '<rect x="5" y="10" width="14" height="12" rx="2"/><path d="M8 10V7a4 4 0 0 1 8 0"/>',
'blinds-open': '<rect x="3" y="3" width="18" height="19" rx="2"/><path d="M3 8h18M3 13h18"/>',
'blinds-closed': '<rect x="3" y="3" width="18" height="19" rx="2"/><path d="M3 8h18M3 13h18M3 18h18"/>',
'info': '<circle cx="12" cy="12" r="10"/><path d="M12 11v6M12 7v1"/>'
};
const el = (tag, cls, txt) => { const x = document.createElement(tag); if (cls) x.className = cls; if (txt !== undefined) x.textContent = String(txt); return x; };
const safeUrl = value => {
if (typeof value !== 'string' || value.length > 2048) return null;
try {
const u = new URL(value, location.href);
if (u.origin === location.origin && value.startsWith('/') && !value.startsWith('//')) return u.href;
if (u.protocol === 'https:' && ['upload.wikimedia.org', 'commons.wikimedia.org'].includes(u.hostname)) return u.href;
} catch (_e) { /* ignore */ }
return null;
};
function dock() {
let node = document.getElementById('dona-visual-dock');
if (!node) {node = el('aside'); node.id = 'dona-visual-dock'; node.setAttribute('aria-live', 'polite'); node.setAttribute('aria-label', 'Illustrations Dona'); document.body.appendChild(node);}
return node;
}
function addSection(card, heading, values, ordered) {
if (!Array.isArray(values) || !values.length) return;
const sec = el('div', 'dvc-section'), strong = el('strong', '', heading), list = el(ordered ? 'ol' : 'ul');
sec.appendChild(strong); values.slice(0, ordered ? 15 : 20).forEach(item => {
if (typeof item === 'string') list.appendChild(el('li', '', item));
}); sec.appendChild(list); card.appendChild(sec);
}
function imageFor(card, image) {
if (!image || typeof image !== 'object') return;
const img = el('img', 'dvc-image'); img.alt = String(image.alt || 'Illustration').slice(0, 140); img.loading = 'lazy';
img.referrerPolicy = 'no-referrer';
const apply = url => {const safe = safeUrl(url); if (safe) {img.src = safe; img.onerror = () => img.removeAttribute('src');}};
if (image.source === 'web' && image.query) {
const endpoint = '/api/dona-visuals/image?q=' + encodeURIComponent(String(image.query).slice(0, 80));
fetch(endpoint, {credentials: 'same-origin'}).then(r => r.ok ? r.json() : null).then(data => {
if (!data || !img.isConnected) return;
apply(data.url);
if (data.source_url) {
const source = safeUrl(data.source_url);
if (source) { const a = el('a', '', data.attribution || 'Source : Wikimedia Commons'); a.href = source; a.target = '_blank'; a.rel = 'noopener noreferrer'; card.appendChild(a); }
}
}).catch(() => {});
} else if (image.url && ['local', 'comfyui', 'web'].includes(image.source)) apply(image.url);
card.appendChild(img);
}
function render(payload) {
if (!payload || payload.schema !== 'dona.visual.v1' || !ACCEPT.has(payload.type) || typeof payload.title !== 'string') return;
const root = dock(); root.replaceChildren(); // one context card; no pile of overlays
const card = el('section', 'dvc-card'); card.dataset.status = String(payload.status || 'informational');
const close = el('button', 'dvc-close', '×'); close.type = 'button'; close.setAttribute('aria-label', 'Fermer la carte'); close.addEventListener('click', () => root.replaceChildren()); card.appendChild(close);
const head = el('div', 'dvc-head'), icon = el('div', 'dvc-icon'), svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('aria-hidden', 'true');
svg.innerHTML = SVG[payload.icon] || SVG.info; // only trusted local constant strings
icon.appendChild(svg); head.appendChild(icon);
const texts = el('div'); texts.appendChild(el('h3', 'dvc-title', payload.title));
if (payload.subtitle) texts.appendChild(el('p', 'dvc-subtitle', payload.subtitle));
head.appendChild(texts); card.appendChild(head);
imageFor(card, payload.image);
if (payload.duration) card.appendChild(el('div', 'dvc-meta', 'Durée : ' + payload.duration));
addSection(card, 'Ingrédients', payload.ingredients, false);
addSection(card, 'Préparation', payload.steps, true);
addSection(card, 'Détails', payload.details, false);
root.appendChild(card);
if (payload.type === 'action_state') window.setTimeout(() => { if (root.contains(card)) card.remove(); }, 13000);
}
window.DonaVisuals = Object.freeze({render});
function init() {
if (typeof window.io === 'function') {
try { const socket = window.io(); socket.on(EVENT, render); }
catch (err) { console.warn('[DONA-VISUAL] Cannot attach socket:', err); }
} else console.warn('[DONA-VISUAL] Socket.IO client not loaded');
const params = new URLSearchParams(location.search);
const demo = params.get('dona_visual_demo');
if (['light', 'recipe'].includes(demo)) fetch('/api/dona-visuals/demo/' + demo).then(r => r.json()).then(render).catch(() => {});
}
if (document.readyState === 'complete') init(); else window.addEventListener('load', init, {once: true});
})();