Files
Alfredv2/alfred/app/static/js/dona-inline-index-v1.js
T
2026-11-02 10:44:05 +01:00

374 lines
16 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_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();
})();