Files
Alfredv2/alfred/app/static/js/dona-holographic-v1.js
T
2026-10-30 22:10:03 +01:00

149 lines
9.1 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_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);
})();