debut illustration
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/* Lightweight preview: pointer-events disabled, no changes to Dona layout */
|
||||
#dona-hermes-live-v1 {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
inset: auto 14px 20px auto;
|
||||
width: min(480px, calc(100vw - 28px));
|
||||
max-height: min(23vh, 185px);
|
||||
padding: 10px 13px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(143,206,246,.32);
|
||||
background: rgba(9, 19, 38, .84);
|
||||
color: #f1f7ff;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 8px 22px rgba(0,0,0,.15);
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
font: 13px/1.4 system-ui, sans-serif;
|
||||
}
|
||||
#dona-hermes-live-v1[hidden] { display: none !important; }
|
||||
#dona-hermes-live-v1 .dona-hlive-title { color: #80d8ff; font-weight: 650; margin-bottom: 3px; }
|
||||
#dona-hermes-live-v1 .dona-hlive-text { font-size: 12px; white-space: pre-wrap; overflow: hidden; overflow-wrap: anywhere; }
|
||||
@media (max-width: 580px) { #dona-hermes-live-v1 { bottom: 10px; right: 10px; max-height: 15vh; } }
|
||||
@media (prefers-reduced-motion: reduce) { #dona-hermes-live-v1 { backdrop-filter: none; } }
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Lightweight non-modal dock: no backdrop, UI remains usable. */
|
||||
#dona-visual-dock{position:fixed;left:18px;bottom:22px;width:min(360px,calc(100vw - 36px));z-index:38;pointer-events:none;font:inherit;color:#f4f6ff;}
|
||||
#dona-visual-dock .dvc-card{position:relative;pointer-events:auto;background:rgba(20,27,47,.94);border:1px solid rgba(134,171,225,.25);box-shadow:0 14px 35px rgba(0,0,0,.38);border-radius:18px;overflow:hidden;max-height:min(70vh,650px);overflow-y:auto;backdrop-filter:blur(12px);padding:16px 17px 15px;margin-top:10px;}
|
||||
#dona-visual-dock .dvc-head{display:flex;align-items:center;gap:12px;padding-right:24px;}
|
||||
#dona-visual-dock .dvc-icon{width:48px;height:48px;display:grid;place-items:center;flex-shrink:0;border-radius:14px;background:rgba(120,148,215,.14);}
|
||||
#dona-visual-dock .dvc-icon svg{width:29px;height:29px;stroke:#b5d5ff;fill:none;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;}
|
||||
#dona-visual-dock .dvc-card[data-status="confirmed"] .dvc-icon svg{stroke:#a9e8c8;}
|
||||
#dona-visual-dock .dvc-card[data-status="error"] .dvc-icon svg{stroke:#ffb3b9;}
|
||||
#dona-visual-dock .dvc-title{margin:0;font-weight:650;font-size:17px;line-height:1.25;overflow-wrap:anywhere;}
|
||||
#dona-visual-dock .dvc-subtitle{margin:4px 0 0;font-size:12px;opacity:.80;line-height:1.4;overflow-wrap:anywhere;}
|
||||
#dona-visual-dock .dvc-close{position:absolute;top:10px;right:10px;width:29px;height:29px;background:rgba(255,255,255,.06);color:white;border:0;border-radius:10px;font-size:20px;cursor:pointer;}
|
||||
#dona-visual-dock .dvc-image{width:100%;max-height:190px;object-fit:cover;border-radius:11px;margin:12px 0 0;display:none;}
|
||||
#dona-visual-dock .dvc-image[src]{display:block;}
|
||||
#dona-visual-dock .dvc-meta{opacity:.75;font-size:12px;margin-top:12px;}
|
||||
#dona-visual-dock .dvc-section{margin-top:12px;font-size:13px;}
|
||||
#dona-visual-dock .dvc-section strong{display:block;margin-bottom:5px;}
|
||||
#dona-visual-dock ul,#dona-visual-dock ol{margin:4px 0 0;padding-left:20px;}
|
||||
#dona-visual-dock li{margin:4px 0;}
|
||||
#dona-visual-dock a{color:#b0d4ff;font-size:11px;}
|
||||
@media(max-width:600px){#dona-visual-dock{left:10px;bottom:12px;width:min(330px,calc(100vw - 20px));}}
|
||||
@media(prefers-reduced-motion:no-preference){#dona-visual-dock .dvc-card{animation:dvc-in .24s ease-out;}@keyframes dvc-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}}
|
||||
@@ -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