new interface

This commit is contained in:
pporcheret
2026-11-02 10:44:05 +01:00
parent 3fa9a3c75b
commit c3a849e636
7 changed files with 494 additions and 1 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,116 @@
/* DONA_INLINE_INDEX_V2 — rendu natif dans l'index, sans carte opaque. */
#donaInlineScene[hidden] { display:none !important; }
#donaInlineScene {
position:fixed;
z-index:55;
top:clamp(92px,12vh,150px);
right:clamp(18px,4vw,72px);
bottom:clamp(108px,13vh,150px);
left:clamp(43%,46vw,54%);
display:grid;
grid-template-columns:0 minmax(0,1fr);
color:var(--dona-text, #eff3ff);
pointer-events:none;
font-family:inherit;
}
#donaInlineScene .dona-inline-right {
pointer-events:auto;
min-width:0;
max-width:min(44vw, 760px);
overflow:hidden;
display:flex;
flex-direction:column;
}
#donaInlineScene .dona-inline-left { visibility:hidden; }
#donaInlineScene .dona-inline-top {
display:flex;
align-items:center;
justify-content:space-between;
gap:16px;
padding-bottom:2px;
}
#donaInlineScene .dona-inline-eyebrow {
color:#95a9cf;
font-weight:700;
font-size:11px;
letter-spacing:.13em;
opacity:.92;
}
#donaInlineScene .dona-inline-close {
background:rgba(255,255,255,.07);
color:#e7efff;
border:1px solid rgba(175,202,241,.3);
font:inherit;
font-size:24px;
line-height:1;
border-radius:50%;
width:36px;
height:36px;
cursor:pointer;
flex-shrink:0;
}
#donaInlineScene .dona-inline-close:hover,
#donaInlineScene .dona-inline-close:focus-visible {
background:rgba(255,255,255,.14);
outline-color:#9fcafa;
}
#donaInlineScene .dona-inline-heading {
font-size:clamp(24px,2.8vw,38px);
font-weight:700;
line-height:1.22;
margin:6px 0 14px;
}
#donaInlineScene .dona-inline-heading[hidden] { display:none !important; }
#donaInlineScene .dona-inline-content {
overflow-y:auto;
overscroll-behavior:contain;
scrollbar-width:thin;
padding:0 12px 18px 0;
min-height:0;
font-size:clamp(14px,1.04vw,17px);
line-height:1.65;
white-space:normal;
}
#donaInlineScene .dona-inline-paragraph {
margin:0 0 13px;
white-space:pre-wrap;
overflow-wrap:anywhere;
color:var(--dona-text-muted, #d0daeb);
}
#donaInlineScene .dona-inline-meta { color:#9fc9ed; margin:0 0 14px; }
#donaInlineScene .dona-inline-section { padding:0 0 12px; margin:0; }
#donaInlineScene .dona-inline-subheading {
font-size:clamp(15px,1.22vw,19px);
color:#eef5ff;
margin:14px 0 8px;
font-weight:650;
}
#donaInlineScene .dona-inline-list { margin:0 0 12px; padding-left:1.4em; color:#d0daeb; overflow-wrap:anywhere; }
#donaInlineScene .dona-inline-list li { margin:0 0 4px; }
#donaInlineScene .dona-inline-more { margin:10px 0; color:#b7d5ff; }
#donaInlineScene .dona-inline-more summary { cursor:pointer; }
#donaInlineScene code { font-size:.93em; background:rgba(255,255,255,.08); border-radius:4px; padding:1px 4px; }
.dona-inline-identity-shift {
translate:var(--dona-inline-shift-x,0px) var(--dona-inline-shift-y,0px);
scale:var(--dona-inline-scale,1);
transition:translate .55s ease, scale .55s ease;
transform-origin:center center;
will-change:translate, scale;
}
.dona-inline-suppressed-modal { display:none !important; }
body.dona-inline-active #dona-hermes-live-v1 { display:none !important; }
@media(max-width:840px) {
#donaInlineScene {
top:clamp(210px,31vh,370px);
left:18px;
right:18px;
bottom:clamp(95px,11vh,160px);
display:block;
}
#donaInlineScene .dona-inline-right { max-width:none; }
#donaInlineScene .dona-inline-heading { font-size:clamp(21px,4.2vw,30px); }
#donaInlineScene .dona-inline-content { font-size:15px; padding-right:6px; }
}
@media (prefers-reduced-motion:reduce) {
.dona-inline-identity-shift { transition:none; }
}
@@ -0,0 +1,373 @@
/* 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();
})();
+1 -1
View File
@@ -146,7 +146,7 @@ function init(){
else if(++attempts>80)clearInterval(interval); else if(++attempts>80)clearInterval(interval);
},150); },150);
const socket=window.alfredSocket; const socket=window.alfredSocket;
socket?.on('dona_suite_v9',result=>render(result,lastPrompt)); socket?.on('dona_suite_v9',result=>{if(opened)render(result,lastPrompt);});
} }
if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init(); if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init,{once:true});else init();
window.DonaSuiteV9={open,close,render,ask}; window.DonaSuiteV9={open,close,render,ask};
+4
View File
@@ -59,6 +59,8 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/dona-hermes-stream-v1.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/dona-hermes-stream-v1.css') }}">
<!-- DONA VISUAL CSS V1 --> <!-- DONA VISUAL CSS V1 -->
<link rel="stylesheet" href="/static/css/dona-visuals-v1.css"> <link rel="stylesheet" href="/static/css/dona-visuals-v1.css">
<!-- DONA_INLINE_INDEX_V1 CSS -->
<link rel="stylesheet" href="/static/css/dona-inline-index-v1.css?v=2">
</head> </head>
<body> <body>
<main class="alfred-shell"> <main class="alfred-shell">
@@ -270,5 +272,7 @@
<script defer src="{{ url_for('static', filename='js/dona-hermes-stream-v1.js') }}"></script> <script defer src="{{ url_for('static', filename='js/dona-hermes-stream-v1.js') }}"></script>
<!-- DONA VISUAL JS V1 --> <!-- DONA VISUAL JS V1 -->
<script defer src="/static/js/dona-visuals-v1.js"></script> <script defer src="/static/js/dona-visuals-v1.js"></script>
<!-- DONA_INLINE_INDEX_V1 JS -->
<script defer src="/static/js/dona-inline-index-v1.js?v=2"></script>
</body> </body>
</html> </html>