/* 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}); })();