/* 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': '',
'lightbulb-off': '',
'chef-hat': '',
'music': '',
'cloud-sun': '',
'calendar': '',
'check': '',
'power': '',
'alert': '',
'thermometer': '',
'lock': '',
'unlock': '',
'blinds-open': '',
'blinds-closed': '',
'info': ''
};
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});
})();