374 lines
16 KiB
JavaScript
374 lines
16 KiB
JavaScript
/* DONA_INLINE_INDEX_V2 - Réponses dans l'index, sans modal ni carte opaque.
|
||
* V2 : orbe/vagues plus petits et à gauche, statut unifié dans la zone existante.
|
||
*/
|
||
(() => {
|
||
'use strict';
|
||
if (window.__donaInlineIndexV2) return;
|
||
window.__donaInlineIndexV2 = true;
|
||
|
||
const MAX = 24000;
|
||
const $ = id => document.getElementById(id);
|
||
const node = (tag, cls, value) => {
|
||
const n = document.createElement(tag);
|
||
if (cls) n.className = cls;
|
||
if (value !== undefined && value !== null) n.textContent = String(value);
|
||
return n;
|
||
};
|
||
const cap = (text, len = MAX) => String(text ?? '').slice(0, len);
|
||
const state = { kind: '', id: null, text: '', rich: false, recentRich: 0,
|
||
shifted: null, socket: null, active: false,
|
||
displayObserver: null, scanTimer: 0,
|
||
statusNode: null, statusOriginal: '', statusCaptured: false };
|
||
|
||
function setup() {
|
||
if ($('donaInlineScene')) return;
|
||
const scene = node('section', 'dona-inline-scene');
|
||
scene.id = 'donaInlineScene';
|
||
scene.hidden = true;
|
||
scene.setAttribute('aria-label', 'Réponse de Dona dans l’accueil');
|
||
const side = node('div', 'dona-inline-left');
|
||
side.append(node('div', 'dona-inline-left-label', 'Dona'));
|
||
const right = node('article', 'dona-inline-right');
|
||
const top = node('div', 'dona-inline-top');
|
||
const eyebrow = node('span', 'dona-inline-eyebrow', 'RÉPONSE · DONA');
|
||
const close = node('button', 'dona-inline-close', '×');
|
||
close.type = 'button';
|
||
close.title = 'Masquer la réponse';
|
||
close.setAttribute('aria-label', 'Masquer la réponse et revenir à l’accueil');
|
||
close.addEventListener('click', hide);
|
||
top.append(eyebrow, close);
|
||
const heading = node('h2', 'dona-inline-heading');
|
||
heading.id = 'donaInlineHeading';
|
||
const content = node('div', 'dona-inline-content');
|
||
content.id = 'donaInlineContent';
|
||
content.setAttribute('role', 'region');
|
||
content.setAttribute('aria-label', 'Contenu de la réponse');
|
||
right.append(top, heading, content);
|
||
scene.append(side, right);
|
||
document.body.append(scene);
|
||
window.addEventListener('resize', () => { if (state.active) positionIdentity(); }, {passive:true});
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Escape' && state.active && !document.querySelector('[role="dialog"]:not([hidden])')) hide();
|
||
});
|
||
attachEvents();
|
||
observeStandaloneResponse();
|
||
}
|
||
|
||
function setHeading(title) {
|
||
const h = $('donaInlineHeading');
|
||
if (!h) return;
|
||
const clean = cap(title || '', 140).trim();
|
||
h.textContent = clean;
|
||
h.hidden = !clean;
|
||
}
|
||
function statusCandidates() {
|
||
const selectors = [
|
||
'#donaStatusText', '#hermesStatus', '#voiceStatus', '#assistantStatus', '#statusText',
|
||
'.dona-status-text', '.assistant-status', '.status-text', '.voice-status', '.hermes-status',
|
||
'[data-dona-status]', '[data-hermes-status]', '[data-status-role="assistant"]'
|
||
];
|
||
for (const sel of selectors) {
|
||
const el = document.querySelector(sel);
|
||
if (el && !el.closest('#donaInlineScene, .dv9-overlay, .d8-overlay') && getComputedStyle(el).display !== 'none') return el;
|
||
}
|
||
const all = [...document.querySelectorAll('div,span,p,small,strong')];
|
||
return all.find(el => {
|
||
if (el.closest('#donaInlineScene, .dv9-overlay, .d8-overlay')) return false;
|
||
if (getComputedStyle(el).display === 'none') return false;
|
||
const t = (el.textContent || '').trim();
|
||
return /Hermes prépare sa réponse|Dona répond|Dona parle|Réponse reçue|Prête?/i.test(t);
|
||
}) || null;
|
||
}
|
||
function setStatus(text) {
|
||
const el = statusCandidates();
|
||
if (!el) return;
|
||
if (!state.statusCaptured || state.statusNode !== el) {
|
||
state.statusNode = el;
|
||
state.statusOriginal = el.textContent || '';
|
||
state.statusCaptured = true;
|
||
}
|
||
el.textContent = String(text || '').trim();
|
||
}
|
||
function restoreStatus() {
|
||
if (state.statusNode && state.statusCaptured) {
|
||
state.statusNode.textContent = state.statusOriginal;
|
||
}
|
||
}
|
||
|
||
function show(kind, title) {
|
||
const scene = $('donaInlineScene');
|
||
if (!scene) return;
|
||
scene.hidden = false;
|
||
state.active = true;
|
||
state.kind = kind;
|
||
document.body.classList.add('dona-inline-active');
|
||
setHeading(title);
|
||
requestAnimationFrame(positionIdentity);
|
||
}
|
||
function clearContent() { $('donaInlineContent').replaceChildren(); }
|
||
function hide() {
|
||
state.active = false;
|
||
$('donaInlineScene').hidden = true;
|
||
document.body.classList.remove('dona-inline-active');
|
||
if (state.shifted) {
|
||
state.shifted.classList.remove('dona-inline-identity-shift');
|
||
state.shifted.style.removeProperty('--dona-inline-shift-x');
|
||
state.shifted.style.removeProperty('--dona-inline-shift-y');
|
||
state.shifted.style.removeProperty('--dona-inline-scale');
|
||
state.shifted = null;
|
||
}
|
||
restoreStatus();
|
||
for (const el of document.querySelectorAll('.dona-inline-suppressed-modal')) el.classList.remove('dona-inline-suppressed-modal');
|
||
}
|
||
|
||
function identityCandidate() {
|
||
const root = $('alfredMainV1') || document.body;
|
||
const preferred = [
|
||
'#donaWave', '#donaWaves', '#donaOrb', '#alfredOrb', '#alfredWave',
|
||
'.av-orb-wrap', '.av-orb', '.av-wave-wrap', '.av-waves',
|
||
'.dona-wave-stage', '.dona-waves', '.dona-orb', '.orb-container',
|
||
'[data-dona-visualizer]', '[data-dona-orb]', '[data-dona-waves]'
|
||
];
|
||
const candidates = preferred.flatMap(sel => [...root.querySelectorAll(sel)]);
|
||
if (!candidates.length) {
|
||
const all = [...root.querySelectorAll('[class], [id]')].slice(0, 800);
|
||
candidates.push(...all.filter(el => {
|
||
const label = String(el.className?.baseVal || el.className || '') + ' ' + (el.id || '');
|
||
return /(?:^|[\s_-])(?:dona|alfred|av)?[\s_-]?(?:orb|orbe|waves?|waveform)(?:[\s_-]|$)/i.test(label);
|
||
}));
|
||
}
|
||
for (const el of candidates) {
|
||
if (el.closest('#donaInlineScene, .dv9-overlay, .d8-overlay, #dona-hermes-live-v1')) continue;
|
||
if (el.matches('button, input, textarea, dialog')) continue;
|
||
const rect = el.getBoundingClientRect();
|
||
if (rect.width < 50 || rect.height < 20 || rect.width > innerWidth * .98) continue;
|
||
if (getComputedStyle(el).display === 'none') continue;
|
||
return el;
|
||
}
|
||
return null;
|
||
}
|
||
function positionIdentity() {
|
||
if (!state.active) return;
|
||
const candidate = identityCandidate();
|
||
if (!candidate) return;
|
||
if (state.shifted && state.shifted !== candidate) {
|
||
state.shifted.classList.remove('dona-inline-identity-shift');
|
||
state.shifted.style.removeProperty('--dona-inline-shift-x');
|
||
state.shifted.style.removeProperty('--dona-inline-shift-y');
|
||
state.shifted.style.removeProperty('--dona-inline-scale');
|
||
}
|
||
const label = String(candidate.className?.baseVal || candidate.className || '') + ' ' + (candidate.id || '');
|
||
const waveLike = /wave|vague/i.test(label);
|
||
const oldX = parseFloat(candidate.style.getPropertyValue('--dona-inline-shift-x')) || 0;
|
||
const oldY = parseFloat(candidate.style.getPropertyValue('--dona-inline-shift-y')) || 0;
|
||
const rect = candidate.getBoundingClientRect();
|
||
const centreX = rect.left + rect.width / 2 - oldX;
|
||
const centreY = rect.top + rect.height / 2 - oldY;
|
||
const portrait = matchMedia('(max-width: 840px)').matches;
|
||
const desiredX = portrait ? innerWidth * .50 : innerWidth * .18;
|
||
const desiredY = portrait ? innerHeight * .27 : innerHeight * .50;
|
||
const x = Math.max(-innerWidth, Math.min(innerWidth, desiredX - centreX));
|
||
const y = Math.max(-innerHeight, Math.min(innerHeight, desiredY - centreY));
|
||
const scale = portrait ? (waveLike ? .46 : .34) : (waveLike ? .54 : .32);
|
||
candidate.style.setProperty('--dona-inline-shift-x', x.toFixed(1) + 'px');
|
||
candidate.style.setProperty('--dona-inline-shift-y', y.toFixed(1) + 'px');
|
||
candidate.style.setProperty('--dona-inline-scale', String(scale));
|
||
candidate.classList.add('dona-inline-identity-shift');
|
||
state.shifted = candidate;
|
||
}
|
||
|
||
function paragraph(text, cls) {
|
||
const p = node('p', cls || 'dona-inline-paragraph');
|
||
const chunks = cap(text, 12000).split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
|
||
for (const chunk of chunks) {
|
||
if (/^\*\*[^*]+\*\*$/.test(chunk)) p.append(node('strong', '', chunk.slice(2, -2)));
|
||
else if (/^`[^`]+`$/.test(chunk)) p.append(node('code', '', chunk.slice(1, -1)));
|
||
else p.append(document.createTextNode(chunk));
|
||
}
|
||
return p;
|
||
}
|
||
function appendProse(text) {
|
||
const container = $('donaInlineContent');
|
||
const lines = cap(text).replace(/\r/g, '').split(/\n{2,}/);
|
||
for (const item of lines.slice(0, 90)) {
|
||
const p = item.trim();
|
||
if (!p) continue;
|
||
if (/^#{1,3}\s+/.test(p)) container.append(node('h3', 'dona-inline-subheading', p.replace(/^#{1,3}\s+/, '')));
|
||
else container.append(paragraph(p));
|
||
}
|
||
}
|
||
function renderText(text, heading = '') {
|
||
state.rich = false;
|
||
state.text = cap(text);
|
||
show('text', heading);
|
||
clearContent();
|
||
appendProse(state.text);
|
||
setStatus('Dona répond…');
|
||
}
|
||
function renderStream(event) {
|
||
if (!event || !event.type || featureOverlayOpen()) return;
|
||
if (event.type === 'start') {
|
||
state.id = event.id;
|
||
state.rich = false;
|
||
state.text = '';
|
||
show('stream', '');
|
||
clearContent();
|
||
setStatus('Hermes prépare sa réponse…');
|
||
} else if (state.id && event.id !== state.id) return;
|
||
else if (event.type === 'delta') {
|
||
if (state.rich) return;
|
||
state.text = cap(state.text + String(event.text || ''));
|
||
show('stream', '');
|
||
setStatus('Dona répond…');
|
||
clearContent();
|
||
appendProse(state.text);
|
||
} else if (event.type === 'tool') {
|
||
if (state.rich) return;
|
||
show('stream', '');
|
||
setStatus('Hermes prépare sa réponse…');
|
||
if (!state.text) {
|
||
clearContent();
|
||
$('donaInlineContent').append(paragraph('Consultation : ' + cap(event.name, 90)));
|
||
}
|
||
} else if (event.type === 'error') {
|
||
if (!state.rich) {
|
||
show('stream', '');
|
||
setStatus('Une erreur est survenue');
|
||
}
|
||
}
|
||
}
|
||
function list(title, items, ordered = false) {
|
||
if (!Array.isArray(items) || !items.length) return;
|
||
const wrap = node('section', 'dona-inline-section');
|
||
wrap.append(node('h3', 'dona-inline-subheading', title));
|
||
const ul = node(ordered ? 'ol' : 'ul', 'dona-inline-list');
|
||
items.slice(0, 50).forEach(item => ul.append(node('li', '', typeof item === 'string' ? cap(item, 500) : cap(item?.title || item?.name || item?.label || JSON.stringify(item), 500))));
|
||
wrap.append(ul);
|
||
$('donaInlineContent').append(wrap);
|
||
}
|
||
function featureOverlayOpen() {
|
||
const ids = ['donaSuiteV9', 'donaDynamicV8'];
|
||
return ids.some(id => {
|
||
const el = $(id);
|
||
return el && !el.hidden && getComputedStyle(el).display !== 'none';
|
||
});
|
||
}
|
||
function renderRecipe(recipe) {
|
||
const name = cap(recipe.title || recipe.name || 'Recette', 160);
|
||
show('suite', name);
|
||
setStatus('Dona répond…');
|
||
clearContent();
|
||
const d = recipe.duration || recipe.time || recipe.minutes || recipe.duration_min || recipe.duration_minutes;
|
||
if (d) $('donaInlineContent').append(paragraph('Durée : ' + cap(d, 50) + (typeof d === 'number' ? ' min' : ''), 'dona-inline-meta'));
|
||
list('Ingrédients', recipe.ingredients);
|
||
let steps = recipe.steps || recipe.preparation || recipe.instructions || recipe.method;
|
||
if (typeof steps === 'string') steps = steps.split(/\n+/).filter(Boolean);
|
||
list('Préparation', steps, true);
|
||
}
|
||
function renderSuite(result) {
|
||
if (!result || typeof result !== 'object' || featureOverlayOpen()) return;
|
||
state.recentRich = Date.now();
|
||
state.rich = true;
|
||
const kind = String(result.kind || '');
|
||
if (kind === 'recipe') {
|
||
const recipes = Array.isArray(result.recipes) ? result.recipes : [];
|
||
if (recipes.length) {
|
||
renderRecipe(recipes[0]);
|
||
if (recipes.length > 1) {
|
||
const summary = node('details', 'dona-inline-more');
|
||
summary.append(node('summary', '', `Voir ${recipes.length - 1} autre(s) recette(s)`));
|
||
const ul = node('ul', 'dona-inline-list');
|
||
recipes.slice(1, 12).forEach(rec => ul.append(node('li', '', cap(rec.title || rec.name || 'Recette', 140))));
|
||
summary.append(ul); $('donaInlineContent').append(summary);
|
||
}
|
||
} else renderText(result.speech || 'Aucune recette trouvée.', result.title || 'Recettes');
|
||
return;
|
||
}
|
||
show('suite', result.title || 'Réponse de Dona');
|
||
setStatus('Dona répond…');
|
||
clearContent();
|
||
if (result.speech) appendProse(result.speech);
|
||
if (kind === 'menu' && result.menu) {
|
||
const m = result.menu;
|
||
if (Array.isArray(m)) list('Menus', m);
|
||
else {
|
||
list('Menus', m.days || m.meals || m.menu || []);
|
||
list('Liste de courses', m.shopping_list || m.groceries || []);
|
||
}
|
||
} else if (kind === 'briefing' || kind === 'daily') {
|
||
list('Événements', result.briefing?.events);
|
||
if (result.briefing?.weather) appendProse(JSON.stringify(result.briefing.weather));
|
||
} else if (kind === 'documents') list('Documents', result.documents?.found);
|
||
else if (kind === 'memories') list('Souvenirs', result.memories);
|
||
else if (kind === 'monitor') list('Supervision', result.monitor?.items || result.monitor?.warnings || []);
|
||
else if (kind === 'child') list('Questions', result.quiz?.questions || result.quiz || []);
|
||
else if (kind === 'routine') list('Actions prévues', result.routine?.actions || result.routine || []);
|
||
}
|
||
function renderVisual(card) {
|
||
if (!card || card.schema !== 'dona.visual.v1' || featureOverlayOpen()) return;
|
||
if (Date.now() - state.recentRich < 1200 && card.type === 'recipe') return;
|
||
if (card.type === 'recipe') { state.rich = true; renderRecipe(card); }
|
||
else {
|
||
state.rich = true;
|
||
show('visual', card.title || 'Dona');
|
||
setStatus('Dona répond…');
|
||
clearContent();
|
||
if (card.subtitle) appendProse(card.subtitle);
|
||
list('Informations', card.details);
|
||
}
|
||
}
|
||
function renderFinal(data) {
|
||
if (!data || typeof data !== 'object' || featureOverlayOpen()) return;
|
||
if (data.type === 'dona_dynamic') return;
|
||
if (data.type === 'dona_visual_card') return renderVisual(data);
|
||
if (Date.now() - state.recentRich < 2800) return;
|
||
const body = data.content && typeof data.content === 'object' ? data.content : data;
|
||
const text = body.text || body.speech || (typeof body.content === 'string' ? body.content : '');
|
||
if (typeof text !== 'string' || !text.trim()) return;
|
||
if (state.active && state.text && text.trim() === state.text.trim()) return;
|
||
renderText(text, '');
|
||
}
|
||
|
||
function observeStandaloneResponse() {
|
||
if (state.displayObserver) return;
|
||
function inspect() {
|
||
for (const el of document.querySelectorAll('[role="dialog"], .dona-response-overlay, .dona-display-overlay, [class*="response-overlay"], [class*="dona-display"], [id*="donaDisplay"], [id*="donaResponse"]')) {
|
||
if (el.closest('#donaInlineScene') || el.classList.contains('dona-inline-suppressed-modal')) continue;
|
||
if (el.hidden || getComputedStyle(el).display === 'none') continue;
|
||
const text = (el.textContent || '').slice(0, 3000);
|
||
if (!/R[ÉE]PONSE\s*[·•-]\s*DONA/i.test(text) || !/Hermes|réponse complète/i.test(text)) continue;
|
||
if (!state.active) {
|
||
const parts = [...el.querySelectorAll('h1,h2,h3,p')].map(e => e.textContent.trim()).filter(Boolean);
|
||
renderText(parts.join('\n\n').slice(0, MAX), '');
|
||
}
|
||
el.classList.add('dona-inline-suppressed-modal');
|
||
}
|
||
}
|
||
state.displayObserver = new MutationObserver(() => {
|
||
if (state.scanTimer) return;
|
||
state.scanTimer = setTimeout(() => { state.scanTimer = 0; inspect(); }, 300);
|
||
});
|
||
state.displayObserver.observe(document.body, {childList:true, subtree:true});
|
||
inspect();
|
||
}
|
||
function attachEvents() {
|
||
const start = Date.now();
|
||
function connect() {
|
||
const socket = window.alfredSocket || (typeof window.io === 'function' ? window.io({reconnection:true}) : null);
|
||
if (!socket) {
|
||
if (Date.now() - start < 15000) setTimeout(connect, 150);
|
||
return;
|
||
}
|
||
state.socket = socket;
|
||
socket.on('dona_suite_v9', renderSuite);
|
||
socket.on('dona_visual_card', renderVisual);
|
||
socket.on('alfred_render', renderFinal);
|
||
socket.on('dona_hermes_stream', renderStream);
|
||
}
|
||
connect();
|
||
}
|
||
window.DonaInlineIndexV2 = {hide, showText: renderText};
|
||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', setup, {once:true});
|
||
else setup();
|
||
})();
|