/* DONA_HOLOGRAPHIC_V1 * Cartes contextuelles issues du TEXTE réellement reçu d'Hermes. * Aucune donnée, état de serveur, distance ou durée n'est calculé/inventé. * Les autres vues (images, calendriers, cartes de renderer) restent inchangées. */ (() => { 'use strict'; const words = Object.freeze({zero:0,un:1,une:1,deux:2,trois:3,quatre:4,cinq:5,six:6,sept:7,huit:8,neuf:9,dix:10,onze:11,douze:12,treize:13,quatorze:14,quinze:15,seize:16,dixsept:17,dixhuit:18,dixneuf:19,vingt:20}); function numberValue(s){ if(!s)return null; const normalized=String(s).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[\s-]/g,''); if(Object.prototype.hasOwnProperty.call(words,normalized))return String(words[normalized]); const str=String(s).trim();return /^\d{1,5}$/.test(str)?str:null; } const number='(\\d{1,5}|vingt|dix[- ]?neuf|dix[- ]?huit|dix[- ]?sept|seize|quinze|quatorze|treize|douze|onze|dix|neuf|huit|sept|six|cinq|quatre|trois|deux|une|un)'; function getFacts(input) { const text=String(input||'').replace(/\u00a0/g,' ').slice(0,16000); const facts=[];const seen=new Set(); function add(key,icon,label,value,note='Mentionné dans la réponse'){ if(!value||seen.has(key)||facts.length>=4)return; facts.push({key,icon,label,value:String(value),note});seen.add(key); } let m; m=text.match(new RegExp('\\b'+number+'\\s+(?:machines?\\s+virtuelles?|VMs?\\b)','i')); if(m)add('vm','⬡','Machines virtuelles',numberValue(m[1]),'Chiffre annoncé par Hermes'); m=text.match(new RegExp('\\b'+number+'\\s+(?:n[œo]uds?|nodes?)(?:\\s|\\.|,|$)','i')); if(m)add('nodes','▦','Nœuds mentionnés',numberValue(m[1]),'Chiffre annoncé par Hermes'); m=text.match(/\b(\d{1,3})\s*\/\s*(\d{1,3})\s*(?:n[œo]uds|serveurs|nodes?)\b/i); if(m)add('online','●','Nœuds disponibles',m[1]+' / '+m[2],'Valeur citée par Hermes'); const route=/\b(trajet|distance|route|entre\s+\S+\s+et\s+\S+|autoroute|en voiture)\b/i.test(text); const building=/\b(hauteur|haut(?:e|s)?|b[aâ]timent|immeuble|tour|monument|construction)\b/i.test(text); if(route){ m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(kilom[eè]tres?|km)\b/i); if(m)add('distance','↗','Distance indiquée',m[1].replace(',', '.')+' km','Distance issue de la réponse'); m=text.match(/\b(\d{1,3})\s*(?:à|a|–|-)\s*(\d{1,3})\s*(minutes?|min|heures?|h)\b/i); if(m)add('duration','◷','Durée annoncée',m[1]+' à '+m[2]+' '+m[3],'Estimation communiquée par Hermes'); else { m=text.match(/\b(\d{1,3})\s*(minutes?|min|heures?|h)\b/i); if(m)add('duration','◷','Durée annoncée',m[1]+' '+m[2],'Estimation communiquée par Hermes'); } } if(building&&!route){ m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(?:m[eè]tres?|m)\b/i); if(m)add('height','♜','Hauteur indiquée',m[1]+' m','Valeur issue de la réponse'); } if(/\b(temp[eé]rature|m[eé]t[eé]o|degr[eé]s)\b/i.test(text)){ m=text.match(/(-?\d{1,3}(?:[,.]\d+)?)\s*°\s*C\b/i); if(m)add('temp','☀','Température citée',m[1]+' °C','Valeur issue de la réponse'); } // Ne jamais induire qu'un nœud est en ligne à partir du seul nom. if(facts.length<4){ m=text.match(/[«"']\s*([\wÀ-ÿ][\wÀ-ÿ_.-]{1,39})\s*[»"'][^.!?\n]{0,90}(arr[eê]t[eé]e?|[eé]teint(?:e)?)(?=[\s,.!?]|$)/i); if(m)add('stopped','⏻','Équipement signalé arrêté',m[1],'État décrit dans la réponse'); } if(facts.length===0){ m=text.match(/\b(?:il est(?: actuellement)?|heure(?: locale)?\s*[::])\s*(\d{1,2}[:h]\d{2})\b/i); if(m)add('time','◷','Heure indiquée',m[1].replace('h',':'),'Issue de la réponse, non vérifiée ici'); } const names=[]; const re=/(?:n[œo]ud|node)\s*['"«]?([\wÀ-ÿ_.-]{2,35})['"»]?\s*:/gi; while((m=re.exec(text))&&names.length<10){if(!names.includes(m[1])) names.push(m[1]);} return {facts,names}; } function summaryOf(text,title){ const clean=String(text||'').trim();const t=String(title||'').trim(); const first=clean.split(/(?<=[.!?])\s+|\n{2,}/)[0]?.trim()||clean; const headline=t && !/^(r[eé]ponse|hermes|texte)$/i.test(t)?t:first; const truncated=headline.length>170?headline.slice(0,167).replace(/\s+\S*$/,'')+'…':headline; let excerpt=clean; if(clean.startsWith(first)&&first.length>35)excerpt=clean.slice(first.length).replace(/^\s+/,''); if(!excerpt)excerpt=clean; if(excerpt.length>290)excerpt=excerpt.slice(0,287).replace(/\s+\S*$/,'')+'…'; return {headline:truncated,excerpt}; } if(typeof module==='object'&&module.exports){module.exports={getFacts,summaryOf,numberValue};} if(typeof document==='undefined')return; const body=document.body; if(!body||body.dataset.donaHoloV1==='active')return; body.dataset.donaHoloV1='active';body.classList.add('dona-holo-v1'); const main=document.getElementById('alfredMainV1'); const content=document.getElementById('content'); if(!main||!content)return; // Pages secondaires : seulement le thème transversal. function el(tag,className,txt){const node=document.createElement(tag);if(className)node.className=className;if(txt!==undefined)node.textContent=String(txt);return node;} // DONA_MARKDOWN_V1 — ne jamais interpréter le Markdown avec innerHTML brut. function rich(tag,className,markdown,inline=false){ const node=el(tag,className); if(window.DonaMarkdown && typeof window.DonaMarkdown.render==='function') window.DonaMarkdown.render(node,markdown,{inline}); else node.textContent=String(markdown??''); return node; } let busy=false;let original=''; function renderCards(){ if(busy || content.firstElementChild?.classList.contains('dh-response'))return; if(content.querySelector('.cards, img.hero, video, canvas, iframe'))return; // N'afficher les cartes que pour une réponse réellement ouverte par Alfred. if(!main.classList.contains('is-focused'))return; const children=[...content.children]; if(!children.length)return; const text=children.filter(n=>n.matches('h1,h2,p,pre')).map(n=>n.textContent.trim()).filter(Boolean).join('\n\n').trim(); if(!text || text.length<13)return; if(/^(bonjour\.|à votre écoute\.|en attente|chargement)/i.test(text))return; if(text===original && content.dataset.holoTransformed==='done')return; const title=content.querySelector('h1,h2')?.textContent||''; const raw=(title && text.startsWith(title+'\n\n'))?text.slice(title.length+2):text; const {facts,names}=getFacts(text); const summary=summaryOf(raw,title); const wrap=el('section','dh-response'); wrap.setAttribute('aria-label','Synthèse contextuelle de la réponse Hermes'); const primary=el('article','dh-primary'); primary.append(el('div','dh-kicker','RÉPONSE · DONA'),rich('div','dh-headline',summary.headline,true)); if(summary.excerpt && summary.excerpt!==summary.headline)primary.append(rich('div','dh-excerpt',summary.excerpt)); else primary.append(el('div','dh-excerpt','Réponse disponible dans son intégralité.')); const more=el('button','dh-more');more.type='button';more.setAttribute('aria-expanded','false');more.append(el('span','', '▤ Afficher la réponse complète'),el('span','','›')); primary.append(more);wrap.append(primary); if(facts.length||names.length){ const grid=el('div','dh-facts'); facts.forEach(f=>{ const c=el('article','dh-fact'); c.append(el('span','dh-fact-icon',f.icon),el('div','dh-fact-label',f.label),el('strong','dh-fact-value',f.value),el('div','dh-fact-note',f.note)); grid.append(c); }); if(names.length && grid.children.length<4){ const c=el('article','dh-fact');c.append(el('span','dh-fact-icon','⬡'),el('div','dh-fact-label','Nœuds cités')); const tags=el('div','dh-tags');names.forEach(n=>tags.append(el('span','dh-tag',n)));c.append(tags,el('div','dh-fact-note','Noms présents dans la réponse'));grid.append(c); } wrap.append(grid); } else { wrap.classList.add('dh-no-facts'); } const full=el('section','dh-full');full.hidden=true;full.id='dhFullReply'; full.append(el('h3','', 'Réponse complète d’Hermes'),rich('div','dh-full-text',text),el('p','dh-source','Affichage fidèle au texte reçu. Les faits n’ont pas été vérifiés séparément.')); more.setAttribute('aria-controls','dhFullReply'); more.addEventListener('click',()=>{ const opened=full.hidden;full.hidden=!opened;more.setAttribute('aria-expanded',String(opened)); more.firstElementChild.textContent=opened?'▤ Masquer la réponse complète':'▤ Afficher la réponse complète'; more.lastElementChild.textContent=opened?'⌃':'›'; }); wrap.append(full); busy=true;content.replaceChildren(wrap);content.dataset.holoTransformed='done';original=text;busy=false; } const obs=new MutationObserver(()=>{if(!busy)renderCards();}); obs.observe(content,{childList:true}); // Accès à l'écran de réponse après mise à jour de la classe focus, sans boucle. main.addEventListener('click',()=>setTimeout(renderCards,0)); // Le renderer/socket réactualise #content : la mutation constitue l'événement canonique. setTimeout(renderCards,100); })();