Files
Alfredv2/alfred/app/static/js/dona-visuals-v1.js
T
2026-11-02 08:50:05 +01:00

96 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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});
})();