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