v2 - STT via Whisper for more precision and if nor available, twist to Vox
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/* Additive CSS: activity dock occupies the right side below the audio controls. */
|
||||
.alfred-feature-dock {
|
||||
position: fixed; z-index: 110; top: 104px; right: 16px;
|
||||
width: min(370px, 34vw); max-height: calc(100dvh - 220px);
|
||||
overflow: hidden; display: flex; flex-direction: column;
|
||||
background: rgba(19, 24, 32, .94); color: #e7eef7;
|
||||
border: 1px solid rgba(104, 159, 216, .30); border-radius: 15px;
|
||||
box-shadow: 0 10px 44px rgba(0,0,0,.36);
|
||||
font-family: system-ui, -apple-system, sans-serif; font-size: 13px;
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.alfred-feature-header { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-bottom: 1px solid rgba(255,255,255,.08); }
|
||||
.alfred-feature-toggle { background: transparent; color: #dceafe; border: 0; font-weight: 650; font: inherit; cursor: pointer; text-align: left; flex: 1; padding: 3px; }
|
||||
.alfred-feature-count { color:#92c5fc; border: 1px solid rgba(113,175,255,.3); border-radius: 99px; padding: 1px 7px; font-variant-numeric: tabular-nums; }
|
||||
.alfred-feature-contents { min-height: 44px; max-height: calc(100dvh - 280px); overflow: auto; padding: 8px; }
|
||||
.alfred-feature-collapsed { width: 190px; }
|
||||
.alfred-feature-collapsed .alfred-feature-header { border-bottom: 0; }
|
||||
.alfred-feature-collapsed .alfred-feature-contents { display: none; }
|
||||
.alfred-feature-card { padding: 11px 12px; margin-bottom: 9px; background: rgba(35,44,56,.95); border: 1px solid rgba(160,180,220,.13); border-radius: 10px; overflow-wrap: anywhere; }
|
||||
.alfred-feature-card-header { display: flex; align-items: flex-start; gap: 8px; }
|
||||
.alfred-feature-card-title { flex: 1; font-size: 13px; font-weight: 650; line-height: 1.45; }
|
||||
.alfred-feature-status { flex: none; padding: 2px 6px; font-size: 11px; border-radius: 9px; background: #445065; color:#f5f5f5; }
|
||||
.alfred-feature-status.running { color: #c3e3ff; background:#245074; }
|
||||
.alfred-feature-status.succeeded { color: #b8e7d0; background:#234c3a; }
|
||||
.alfred-feature-status.failed, .alfred-feature-status.interrupted { color:#ffd5d5; background:#75383a; }
|
||||
.alfred-feature-body, .alfred-feature-detail { margin: 9px 0 5px; color:#d9e2ed; line-height: 1.5; white-space: pre-wrap; }
|
||||
.alfred-feature-detail { font-size: 12px; color: #b8d1ec; }
|
||||
.alfred-feature-image { display:block; width: 100%; max-height: 230px; object-fit: contain; margin: 8px 0; border-radius: 8px; }
|
||||
.alfred-feature-weather { color: #f0cd88; font-size: 16px; }
|
||||
.alfred-feature-event { padding: 5px 0; border-top: 1px solid rgba(255,255,255,.07); }
|
||||
.alfred-feature-footer { display:flex; align-items:center; justify-content:space-between; margin-top:8px; color:#91a7c0; font-size: 11px; }
|
||||
.alfred-feature-more, .alfred-feature-speak { background: rgba(70,119,175,.17); border: 1px solid rgba(118,173,230,.35); color:#d2e8ff; border-radius:7px; padding:3px 9px; cursor:pointer; }
|
||||
.alfred-feature-empty { color:#9db2c8; padding: 0 6px; }
|
||||
@media (max-width: 720px) { .alfred-feature-dock { top: 86px; right: 8px; width: min(310px, 82vw); max-height: calc(100dvh - 190px); } .alfred-feature-contents { max-height: calc(100dvh - 255px); } }
|
||||
@@ -0,0 +1,21 @@
|
||||
/* Dock discrets : la caméra reste accessible via son bouton popup. */
|
||||
#alfredTimersDock[hidden],#alfredContextPopup[hidden],#alfredTimerToast[hidden]{display:none!important}
|
||||
#alfredTimersDock{position:fixed;left:36px;bottom:32px;z-index:5;width:min(315px,30vw);max-height:min(46vh,380px);overflow-y:auto;border:1px solid #ffffff25;background:#242a2fe8;border-radius:17px;padding:12px 14px;box-shadow:0 15px 45px #0005;backdrop-filter:blur(14px);color:#eef3f5}
|
||||
.alfred-time-head{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:650;color:#bdd5df;padding:0 0 8px;border-bottom:1px solid #ffffff15}
|
||||
.alfred-time-card{display:grid;grid-template-columns:1fr auto;gap:6px 4px;align-items:center;padding:9px 0;border-bottom:1px solid #ffffff11}
|
||||
.alfred-time-card:last-child{border-bottom:0}
|
||||
.alfred-time-label{font-size:12px;color:#e3ebed;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;grid-column:1/3}
|
||||
.alfred-time-value{font-size:25px;font-variant-numeric:tabular-nums;letter-spacing:.02em;font-weight:550}
|
||||
.alfred-time-value.is-paused{color:#e2c183}
|
||||
.alfred-time-actions{display:flex;gap:5px}
|
||||
.alfred-time-icon{color:#dae4e8;background:#ffffff10;border:1px solid #ffffff1e;border-radius:8px;width:32px;height:31px;cursor:pointer;font-size:18px}
|
||||
.alfred-time-icon:hover{background:#ffffff2a}
|
||||
#alfredTimerToast{position:fixed;bottom:205px;left:36px;max-width:350px;background:#354a4b;border:1px solid #96d3c8;border-radius:12px;padding:13px 16px;color:#f1faf9;z-index:40;box-shadow:0 15px 40px #0008}
|
||||
#alfredContextPopup{position:fixed;inset:0;display:grid;place-items:center;background:#05090dcc;backdrop-filter:blur(4px);z-index:48;padding:20px}
|
||||
.alfred-context-card{position:relative;display:grid;grid-template-columns:minmax(160px,280px) minmax(220px,380px);gap:24px;align-items:center;max-width:min(92vw,740px);max-height:90vh;overflow:auto;border:1px solid #ffffff35;border-radius:20px;background:#252d32;padding:25px;box-shadow:0 26px 80px #000b}
|
||||
#alfredContextImage{display:block;width:100%;height:280px;object-fit:contain;border-radius:12px;background:#14191d}
|
||||
.alfred-context-details h2{font-size:clamp(21px,2vw,31px);line-height:1.2;margin:0 0 15px;color:#f3f7f9}
|
||||
.alfred-context-details p{font-size:14px;color:#d3dfe4;line-height:1.6;max-height:225px;overflow-y:auto;white-space:pre-wrap}
|
||||
.alfred-context-details a{color:#a0d9f0;font-size:12px;text-decoration:underline}
|
||||
.alfred-context-close{position:absolute;right:12px;top:12px;border:1px solid #ffffff2c;background:#414b50;color:#f3f5f6;border-radius:10px;cursor:pointer;width:33px;height:33px;font-size:23px}
|
||||
@media(max-width:850px){#alfredTimersDock{width:255px;left:14px;bottom:15px;max-height:36vh}#alfredTimerToast{left:14px;bottom:180px}.alfred-context-card{grid-template-columns:1fr;gap:12px;max-width:420px}#alfredContextImage{height:220px}}
|
||||
@@ -0,0 +1,172 @@
|
||||
/* Alfred V4+V5: visual activity dock. Does not alter the existing interface. */
|
||||
(() => {
|
||||
'use strict';
|
||||
const CSS = 'alfred-feature-';
|
||||
const state = { cards: [], collapsed: false, lastStatus: new Map(), receivedOnce: false };
|
||||
const el = (tag, className, text) => {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = CSS + className;
|
||||
if (text != null) node.textContent = String(text);
|
||||
return node;
|
||||
};
|
||||
const root = el('aside', 'dock');
|
||||
root.setAttribute('aria-label', 'Activité et informations d’Alfred');
|
||||
const header = el('header', 'header');
|
||||
const toggle = el('button', 'toggle', 'Activité Hermes');
|
||||
toggle.type = 'button';
|
||||
toggle.setAttribute('aria-expanded', 'true');
|
||||
const count = el('span', 'count', '0');
|
||||
const contents = el('section', 'contents');
|
||||
contents.setAttribute('aria-live', 'polite');
|
||||
contents.setAttribute('aria-relevant', 'additions text');
|
||||
header.append(toggle, count);
|
||||
root.append(header, contents);
|
||||
document.body.append(root);
|
||||
|
||||
const toggleDock = (collapse) => {
|
||||
state.collapsed = collapse;
|
||||
root.classList.toggle(CSS + 'collapsed', collapse);
|
||||
toggle.setAttribute('aria-expanded', String(!collapse));
|
||||
toggle.textContent = collapse ? '☰ Activité Hermes' : '⌄ Activité Hermes';
|
||||
};
|
||||
toggle.addEventListener('click', () => toggleDock(!state.collapsed));
|
||||
|
||||
const addText = (parent, cls, text, maxlength=1600) => {
|
||||
if (text === undefined || text === null || String(text).trim() === '') return;
|
||||
const value = String(text);
|
||||
if (value.length <= maxlength) {
|
||||
parent.append(el('p', cls, value));
|
||||
return;
|
||||
}
|
||||
const p = el('p', cls, value.slice(0, maxlength) + '…');
|
||||
const button = el('button', 'more', 'Afficher tout');
|
||||
button.type = 'button';
|
||||
let open = false;
|
||||
button.addEventListener('click', () => {
|
||||
open = !open;
|
||||
p.textContent = open ? value : value.slice(0, maxlength) + '…';
|
||||
button.textContent = open ? 'Réduire' : 'Afficher tout';
|
||||
});
|
||||
parent.append(p, button);
|
||||
};
|
||||
|
||||
const safeImage = url => {
|
||||
if (typeof url !== 'string') return null;
|
||||
try {
|
||||
const parsed = new URL(url, location.origin);
|
||||
return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : null;
|
||||
} catch { return null; }
|
||||
};
|
||||
const displayExtra = (item, display) => {
|
||||
if (!display || typeof display !== 'object') return;
|
||||
const kind = display.type;
|
||||
if (kind === 'image' && display.url) {
|
||||
const src = safeImage(display.url);
|
||||
if (src) {
|
||||
const img = el('img', 'image');
|
||||
img.src = src;
|
||||
img.alt = display.title || 'Illustration Alfred';
|
||||
img.loading = 'lazy';
|
||||
item.append(img);
|
||||
}
|
||||
} else if (kind === 'weather') {
|
||||
const line = [display.temperature, display.condition || display.description].filter(Boolean).join(' · ');
|
||||
addText(item, 'weather', line, 300);
|
||||
} else if (kind === 'calendar' && Array.isArray(display.events)) {
|
||||
for (const event of display.events.slice(0, 6)) {
|
||||
addText(item, 'event', [event.start, event.title].filter(Boolean).join(' — '), 300);
|
||||
}
|
||||
}
|
||||
if (display.text && display.text !== item.dataset.body) addText(item, 'detail', display.text);
|
||||
};
|
||||
const STATUS_LABELS = {
|
||||
queued: 'En attente', running: 'En cours', succeeded: 'Terminé',
|
||||
failed: 'Erreur', interrupted: 'Interrompu'
|
||||
};
|
||||
|
||||
function render() {
|
||||
contents.replaceChildren();
|
||||
count.textContent = String(state.cards.length);
|
||||
if (!state.cards.length) {
|
||||
contents.append(el('p', 'empty', 'Aucune activité pour le moment.'));
|
||||
return;
|
||||
}
|
||||
state.cards.forEach(card => {
|
||||
const item = el('article', 'card');
|
||||
item.dataset.body = card.body || '';
|
||||
const heading = el('div', 'card-header');
|
||||
const title = el('strong', 'card-title', card.title || 'Information');
|
||||
heading.append(title);
|
||||
if (card.status) {
|
||||
const status = el('span', 'status ' + (card.status || ''), STATUS_LABELS[card.status] || card.status);
|
||||
heading.append(status);
|
||||
}
|
||||
item.append(heading);
|
||||
const body = card.body || '';
|
||||
if (body && (!card.display?.text || card.display.text !== body)) addText(item, 'body', body);
|
||||
displayExtra(item, card.display);
|
||||
if (body && card.display?.text === body) addText(item, 'body', body);
|
||||
const footer = el('div', 'footer');
|
||||
if (card.updated_at) {
|
||||
const date = new Date(card.updated_at);
|
||||
footer.append(el('time', 'date', isNaN(date.getTime()) ? '' : date.toLocaleTimeString('fr-FR', {hour:'2-digit', minute:'2-digit'})));
|
||||
}
|
||||
if (card.status === 'succeeded' && body) {
|
||||
const say = el('button', 'speak', 'Lire');
|
||||
say.type = 'button';
|
||||
say.title = 'Faire lire le résultat par la voix Piper configurée';
|
||||
say.addEventListener('click', async () => {
|
||||
say.disabled = true;
|
||||
try {
|
||||
const response = await fetch('/api/tts/say', {
|
||||
method:'POST', headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({text:body.slice(0,1800)})
|
||||
});
|
||||
if (!response.ok) throw new Error('TTS indisponible');
|
||||
} catch (error) { say.title = String(error); }
|
||||
finally { say.disabled = false; }
|
||||
});
|
||||
footer.append(say);
|
||||
}
|
||||
item.append(footer);
|
||||
contents.append(item);
|
||||
});
|
||||
}
|
||||
let fetching = false;
|
||||
async function refresh() {
|
||||
if (fetching) return;
|
||||
fetching = true;
|
||||
try {
|
||||
const response = await fetch('/api/alfred/cards', {cache:'no-store'});
|
||||
if (!response.ok) throw new Error('Cartes indisponibles');
|
||||
const payload = await response.json();
|
||||
const cards = Array.isArray(payload.cards) ? payload.cards : [];
|
||||
const sig = JSON.stringify(cards);
|
||||
if (sig === state.signature) return;
|
||||
state.signature = sig;
|
||||
const hasNewCompletion = cards.some(card =>
|
||||
card.status === 'succeeded' && state.lastStatus.has(card.id) &&
|
||||
state.lastStatus.get(card.id) !== 'succeeded');
|
||||
const hasNewCard = cards.some(card => !state.lastStatus.has(card.id));
|
||||
state.lastStatus = new Map(cards.map(c => [c.id, c.status]));
|
||||
state.cards = cards;
|
||||
render();
|
||||
if (!state.receivedOnce) {
|
||||
state.receivedOnce = true;
|
||||
toggleDock(cards.length === 0);
|
||||
} else if (hasNewCompletion || hasNewCard) {
|
||||
toggleDock(false);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!state.receivedOnce) contents.replaceChildren(el('p', 'empty', 'Connexion aux cartes indisponible.'));
|
||||
} finally { fetching = false; }
|
||||
}
|
||||
refresh();
|
||||
setInterval(refresh, 10000); // safety fallback; Socket.IO wakes immediately
|
||||
if (typeof window.io === 'function') {
|
||||
const socket = window.io();
|
||||
socket.on('alfred_cards_changed', refresh);
|
||||
socket.on('alfred_task', refresh);
|
||||
socket.on('connect', refresh);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,132 @@
|
||||
/* ALFRED_HIDE_HERMES_ACTIVITY_V2
|
||||
Supprime uniquement l'ancienne vue "Activité Hermes" du DOM visuel.
|
||||
N'interrompt ni les taches de fond, ni le MCP, ni les evenements Socket.IO.
|
||||
*/
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const TITLE = /\bactivit[eé]\s+hermes\b/i;
|
||||
const EXACT_TITLE = /^\s*[⌄˅∨▾▿▼v]?\s*activit[eé]\s+hermes\s*(?:\d+)?\s*$/i;
|
||||
const SPECIFIC_SELECTOR = [
|
||||
'#hermesActivityPopup', '#hermesActivityModal',
|
||||
'[aria-label="Activité Hermes"]', '[aria-label="Activite Hermes"]',
|
||||
'[data-component="hermes-activity"]', '[data-widget="hermes-activity"]',
|
||||
'.hermes-activity-panel', '.hermes-activity-popup',
|
||||
'.hermes-activity-widget', '.hermes-activity-dock',
|
||||
'.hermes-task-panel', '.hermes-tasks-panel'
|
||||
].join(',');
|
||||
const CLASS_HINT = /(?:hermes.{0,30}(?:activit|activity|tasks?|jobs?)|(?:activit|activity|tasks?|jobs?).{0,30}hermes)/i;
|
||||
const PANEL_HINT = /(?:activit|activity|tasks?|jobs?|queue|history|panel|widget|dock|drawer|popover|sidebar)/i;
|
||||
|
||||
function markHidden(element) {
|
||||
if (!element || !element.isConnected || element === document.body) return;
|
||||
if (!element.hasAttribute('data-alfred-hermes-hidden')) {
|
||||
element.setAttribute('data-alfred-hermes-hidden', 'true');
|
||||
}
|
||||
// Important : certains widgets reinitialisent leur attribut hidden ou display.
|
||||
if (!element.hidden) element.hidden = true;
|
||||
if (element.style.getPropertyValue('display') !== 'none' ||
|
||||
element.style.getPropertyPriority('display') !== 'important') {
|
||||
element.style.setProperty('display', 'none', 'important');
|
||||
}
|
||||
}
|
||||
|
||||
function isTitle(text) {
|
||||
return EXACT_TITLE.test(String(text || '').replace(/\u00a0/g, ' ').trim());
|
||||
}
|
||||
|
||||
function isSafeCandidate(el) {
|
||||
if (!el || !el.isConnected) return false;
|
||||
if (el.matches('html,body,main,.alfred-shell,#stage,#content')) return false;
|
||||
if (el.contains(document.querySelector('#stage')) && document.querySelector('#stage')) return false;
|
||||
if (el.contains(document.querySelector('#alfredTimersDock')) && document.querySelector('#alfredTimersDock')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function choosePanel(titleNode) {
|
||||
// La capture montre un dock : le titre est accompagne d'un compteur et
|
||||
// d'une carte de tache (badge "Termine", bouton "Lire").
|
||||
const ancestors = [];
|
||||
let el = titleNode.nodeType === 1 ? titleNode : titleNode.parentElement;
|
||||
for (let depth = 0; el && depth < 10; depth++, el = el.parentElement) {
|
||||
if (!isSafeCandidate(el)) break;
|
||||
ancestors.push(el);
|
||||
}
|
||||
if (!ancestors.length) return null;
|
||||
|
||||
// Quand l'identifiant ou la classe indiquent explicitement le widget,
|
||||
// ces conteneurs sont preferes au contenu de la carte.
|
||||
for (const candidate of ancestors) {
|
||||
const identity = `${candidate.id || ''} ${typeof candidate.className === 'string' ? candidate.className : ''}`;
|
||||
if (CLASS_HINT.test(identity)) return candidate;
|
||||
if (candidate.matches('[role="dialog"],[role="complementary"]') && TITLE.test(candidate.textContent || '')) return candidate;
|
||||
}
|
||||
|
||||
// Cas general (et cas de la capture) : choisir le plus petit ancetre
|
||||
// suffisamment grand pour contenir l'entete ET la liste des taches.
|
||||
for (const candidate of ancestors) {
|
||||
const allText = (candidate.innerText || candidate.textContent || '').trim();
|
||||
if (allText.length > 15000) continue; // l'ancetre contient deja le titre detecte
|
||||
const hasTask = /\b(?:termin[eé]|en\s+cours|en\s+attente|erreur|lire|annuler|rapport)\b/i.test(allText);
|
||||
const childCount = candidate.children ? candidate.children.length : 0;
|
||||
if (!hasTask || childCount < 2) continue;
|
||||
const rect = candidate.getBoundingClientRect();
|
||||
if (rect.width >= 180 && rect.height >= 100 &&
|
||||
rect.width <= Math.max(window.innerWidth * 0.82, 600) &&
|
||||
rect.height <= window.innerHeight * 1.5) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Panneau vide : se baser sur la bordure/classe du conteneur, sans cacher
|
||||
// un parent generic (page entiere, scene, panneau de transcription).
|
||||
for (const candidate of ancestors) {
|
||||
const identity = `${candidate.id || ''} ${typeof candidate.className === 'string' ? candidate.className : ''}`;
|
||||
if (!PANEL_HINT.test(identity)) continue;
|
||||
const rect = candidate.getBoundingClientRect();
|
||||
if (rect.width >= 150 && rect.width <= Math.max(window.innerWidth * 0.82, 600)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hideActivity() {
|
||||
for (const el of document.querySelectorAll(SPECIFIC_SELECTOR)) {
|
||||
if (isSafeCandidate(el)) markHidden(el);
|
||||
}
|
||||
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
||||
let textNode;
|
||||
while ((textNode = walker.nextNode())) {
|
||||
if (!textNode.parentElement || textNode.parentElement.closest('[data-alfred-hermes-hidden]')) continue;
|
||||
if (!isTitle(textNode.nodeValue)) continue;
|
||||
const panel = choosePanel(textNode);
|
||||
if (panel) markHidden(panel);
|
||||
}
|
||||
}
|
||||
|
||||
let queued = false;
|
||||
function scheduleHide() {
|
||||
if (queued) return;
|
||||
queued = true;
|
||||
queueMicrotask(() => {
|
||||
queued = false;
|
||||
if (document.body) hideActivity();
|
||||
});
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
hideActivity();
|
||||
observe();
|
||||
}, {once:true});
|
||||
} else {
|
||||
hideActivity();
|
||||
observe();
|
||||
}
|
||||
function observe() {
|
||||
const observer = new MutationObserver(scheduleHide);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,157 @@
|
||||
/* 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});
|
||||
})();
|
||||
Reference in New Issue
Block a user