diff --git a/alfred/app/data/alfred_features.sqlite3 b/alfred/app/data/alfred_features.sqlite3 index 28c9d56..c86f813 100644 Binary files a/alfred/app/data/alfred_features.sqlite3 and b/alfred/app/data/alfred_features.sqlite3 differ diff --git a/alfred/app/data/alfred_features.sqlite3-shm b/alfred/app/data/alfred_features.sqlite3-shm index 2e666d4..fe9ac28 100644 Binary files a/alfred/app/data/alfred_features.sqlite3-shm and b/alfred/app/data/alfred_features.sqlite3-shm differ diff --git a/alfred/app/data/alfred_features.sqlite3-wal b/alfred/app/data/alfred_features.sqlite3-wal index 5a10b9d..e69de29 100644 Binary files a/alfred/app/data/alfred_features.sqlite3-wal and b/alfred/app/data/alfred_features.sqlite3-wal differ diff --git a/alfred/app/static/css/dona-inline-index-v1.css b/alfred/app/static/css/dona-inline-index-v1.css new file mode 100644 index 0000000..4d10eb7 --- /dev/null +++ b/alfred/app/static/css/dona-inline-index-v1.css @@ -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; } +} diff --git a/alfred/app/static/js/dona-inline-index-v1.js b/alfred/app/static/js/dona-inline-index-v1.js new file mode 100644 index 0000000..71f41ab --- /dev/null +++ b/alfred/app/static/js/dona-inline-index-v1.js @@ -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(); +})(); diff --git a/alfred/app/static/js/dona-suite-v9.js b/alfred/app/static/js/dona-suite-v9.js index 65fd654..2e57e63 100644 --- a/alfred/app/static/js/dona-suite-v9.js +++ b/alfred/app/static/js/dona-suite-v9.js @@ -146,7 +146,7 @@ function init(){ else if(++attempts>80)clearInterval(interval); },150); 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(); window.DonaSuiteV9={open,close,render,ask}; diff --git a/alfred/app/templates/index.html b/alfred/app/templates/index.html index 8b42681..1f9c2f4 100644 --- a/alfred/app/templates/index.html +++ b/alfred/app/templates/index.html @@ -59,6 +59,8 @@ + +