/* ALFRED_TIMERS_CONTEXT_V1 — chronos/minuteurs dans un coin, illustrations en popup. */ (() => { 'use strict'; const root = document.getElementById('alfredTimersDock'); const list = document.getElementById('alfredTimersList'); const context = document.getElementById('alfredContextPopup'); const contextImage = document.getElementById('alfredContextImage'); const contextTitle = document.getElementById('alfredContextTitle'); const contextText = document.getElementById('alfredContextText'); const contextSource = document.getElementById('alfredContextSource'); const contextClose = document.getElementById('alfredContextClose'); const toast = document.getElementById('alfredTimerToast'); if (!root || !list) return; let items = []; let anchorMs = Date.now(); let lastSnapshotClientMs = Date.now(); let toastTimeout = null; function format(seconds) { const s = Math.max(0, Math.floor(seconds)); const h = Math.floor(s / 3600); const m = Math.floor(s % 3600 / 60); const sec = s % 60; return h > 0 ? `${h}:${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}` : `${String(m).padStart(2,'0')}:${String(sec).padStart(2,'0')}`; } function textNode(tag, cls, text) { const el = document.createElement(tag); if (cls) el.className = cls; if (text !== undefined) el.textContent = text; return el; } async function action(op, id, seconds) { const payload = {action:op, id}; if (seconds !== undefined) payload.seconds = seconds; const r = await fetch('/api/timers/action', {method:'POST', headers:{'Content-Type':'application/json', 'X-Alfred-Request':'1'}, body:JSON.stringify(payload)}); const data = await r.json(); if (!r.ok) throw new Error(data.error || `HTTP ${r.status}`); update(data.items || []); } function update(next) { items = Array.isArray(next) ? next : []; lastSnapshotClientMs = Date.now(); list.replaceChildren(); root.hidden = !items.length; for (const item of items) { const card = textNode('div','alfred-time-card'); card.dataset.itemId = item.id; const label = textNode('span','alfred-time-label', (item.kind === 'timer' ? '⌛ ' : '⏱ ') + item.label); const counter = textNode('strong','alfred-time-value','00:00'); counter.dataset.kind = item.kind; card.append(label,counter); const actions = textNode('div','alfred-time-actions'); const pause = textNode('button','alfred-time-icon', item.state === 'paused' ? '▶' : 'Ⅱ'); pause.type='button'; pause.title=item.state === 'paused' ? 'Reprendre':'Mettre en pause'; pause.setAttribute('aria-label',pause.title); pause.onclick=()=>action(item.state==='paused'?'resume':'pause',item.id).catch(showError); const stop=textNode('button','alfred-time-icon','×'); stop.type='button'; stop.title='Annuler'; stop.setAttribute('aria-label','Annuler '+item.label); stop.onclick=()=>action('cancel',item.id).catch(showError); actions.append(pause,stop);card.append(actions);list.append(card); } redraw(); } function redraw() { const delta = Math.max(0, (Date.now()-lastSnapshotClientMs)/1000); for (const item of items) { const node=[...list.querySelectorAll('[data-item-id]')].find(el=>el.dataset.itemId===item.id); if (!node) continue; let elapsed=Number(item.elapsed)||0; if(item.state==='running') elapsed+=delta; const seconds = item.kind==='timer' ? Math.max(0,(Number(item.duration)||0)-elapsed) : elapsed; const counter=node.querySelector('.alfred-time-value'); counter.textContent=format(seconds); counter.classList.toggle('is-paused',item.state==='paused'); if(item.kind==='stopwatch' && item.duration!==null) counter.title='Limite '+format(item.duration); } } function notify(message) { if (!toast) return; toast.textContent=message;toast.hidden=false; clearTimeout(toastTimeout); toastTimeout=setTimeout(()=>{toast.hidden=true;},9000); } function showError(error) {notify(error.message || String(error));} async function refresh() { try { const r=await fetch('/api/timers',{cache:'no-store'}); if(!r.ok) throw new Error(`HTTP ${r.status}`); const data=await r.json(); update(data.items || []); } catch (err) { console.warn('[ALFRED TIMERS]',err); } } const socket = window.alfredSocket; socket?.on('alfred_timers_changed', data => update(data.items || [])); socket?.on('alfred_timer_finished', data => { notify(`⏰ ${data.label} : terminé !`); refresh(); }); socket?.on('connect', refresh); refresh(); setInterval(redraw, 250); setInterval(()=>{if(!document.hidden)refresh();}, 15000); // Illustration des demandes « qui est ... ? » : source = Wikipédia, réponse = Hermes. function closeContext() { if (context) context.hidden=true; if (contextImage) contextImage.removeAttribute('src'); } if (context && contextImage && contextTitle) { socket?.on('alfred_context_image', data => { try { const image = new URL(data.image_url); const source = new URL(data.source_url); if (image.protocol !== 'https:' || !['upload.wikimedia.org','commons.wikimedia.org'].includes(image.hostname))return; if (source.protocol !== 'https:' || source.hostname !== 'fr.wikipedia.org')return; contextTitle.textContent = String(data.title || 'Illustration'); contextText.textContent = String(data.text || '').slice(0,500); contextImage.src = image.href; contextSource.href = source.href; contextSource.textContent = String(data.credit || 'Source : Wikipédia'); context.hidden=false; } catch (err) { console.warn('[ALFRED CONTEXT]',err); } }); contextClose?.addEventListener('click', closeContext); context.addEventListener('pointerdown', e => {if(e.target === context)closeContext();}); document.addEventListener('keydown', e=>{if(e.key === 'Escape' && !context.hidden)closeContext();}); } // Masque uniquement les anciennes fenêtres intitulées exactement « Activité Hermes ». // N'interfère ni avec la session Hermes ni avec ses réponses dans la scène. function hideLegacyHermesActivity() { const titlePattern=/^activit[eé]\s+hermes$/i; for (const el of document.querySelectorAll('h1,h2,h3,h4,[role="heading"],.popup-title,.modal-title')) { if (!titlePattern.test((el.textContent || '').trim()))continue; const popup = el.closest('[role="dialog"],.modal,.popup,.popover,.overlay,.hermes-activity-panel,.hermes-activity-popup'); if (popup && popup !== document.body) { popup.hidden = true; popup.style.display = 'none'; } } for (const el of document.querySelectorAll('[aria-label="Activité Hermes"],[aria-label="Activite Hermes"],#hermesActivityPopup,#hermesActivityModal')) { el.hidden=true;el.style.display='none'; } } hideLegacyHermesActivity(); let pending=false; const observer = new MutationObserver(() => { if(pending)return; pending=true; queueMicrotask(()=>{pending=false;hideLegacyHermesActivity();}); }); observer.observe(document.body,{childList:true,subtree:true}); })();