Files
Alfredv2/alfred/app/static/js/alfred-main-v1.js
T

320 lines
23 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.
/* ALFRED_MAIN_VISUAL_V1 — accueil vivant, conservation des contrôleurs existants. */
(() => {
'use strict';
if (document.getElementById('alfredMainV1')) return;
const shell = document.querySelector('.alfred-shell');
if (!shell || !document.getElementById('stage') || !document.getElementById('content')) return;
const I = {
home:'<path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-7H9v7H4a1 1 0 0 1-1-1z"/>',
bulb:'<path d="M9 18h6m-5 3h4m-6-7c-1-1.2-2-2.5-2-5a6 6 0 0 1 12 0c0 2.5-1 3.8-2 5-.8 1-1 1.9-1 2H9c0-.1-.2-1-1-2z"/>',
server:'<rect x="3" y="3" width="18" height="8" rx="2"/><rect x="3" y="13" width="18" height="8" rx="2"/><path d="M7 7h.01M7 17h.01M12 7h6M12 17h6"/>',
brain:'<path d="M12 18V5a3 3 0 0 0-5-2 3 3 0 0 0-3 4 4 4 0 0 0 0 7 3.5 3.5 0 0 0 3.3 5A3.5 3.5 0 0 0 12 18m0-13a3 3 0 0 1 5-2 3 3 0 0 1 3 4 4 4 0 0 1 0 7 3.5 3.5 0 0 1-3.3 5A3.5 3.5 0 0 1 12 18"/><path d="m8 10 4 2 4-2"/>',
media:'<rect x="3" y="4" width="18" height="16" rx="3"/><circle cx="8.5" cy="9" r="1.3"/><path d="m4 17 5-5 4 4 3-3 4 4"/>',
more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
mic:'<rect x="9" y="2.5" width="6" height="13" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4m-4 0h8"/>',
micOff:'<path d="M9 9v3a3 3 0 0 0 5.5 1.7M15 9V5.5a3 3 0 0 0-5.5-1.7M5 11a7 7 0 0 0 11.5 5.4M19 11a7 7 0 0 1-.5 2.6M12 18v4m-4 0h8M3 3l18 18"/>',
speaker:'<path d="M11 5 6 9H3v6h3l5 4V5ZM15 9a4 4 0 0 1 0 6M18 6a8 8 0 0 1 0 12"/>',
speakerOff:'<path d="M11 5 6 9H3v6h3l5 4V5ZM16 9l5 6m0-6-5 6"/>',
camera:'<rect x="2" y="5" width="14" height="14" rx="3"/><path d="m16 10 6-3v10l-6-3"/>',
settings:'<path d="M12 3v2m0 14v2M3 12h2m14 0h2M5.6 5.6 7 7m10 10 1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4"/><circle cx="12" cy="12" r="4"/>',
calendar:'<rect x="3" y="5" width="18" height="16" rx="2"/><path d="M7 3v4m10-4v4M3 10h18"/>',
clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
cloud:'<path d="M7 18h11a4 4 0 0 0 0-8h-1a6 6 0 0 0-11-1 4.5 4.5 0 0 0 1 9Z"/>',
shield:'<path d="m12 2 8 3v6c0 6-3.7 9-8 11-4.3-2-8-5-8-11V5z"/><path d="m9 12 2 2 4-4"/>',
arrow:'<path d="m9 5 7 7-7 7"/>',
chat:'<path d="M5 5h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H9l-5 3v-3a2 2 0 0 1-1-2V7a2 2 0 0 1 2-2z"/>',
close:'<path d="M5 5l14 14M19 5 5 19"/>',
sun:'<circle cx="12" cy="12" r="4"/><path d="M12 1v3m0 16v3M1 12h3m16 0h3M4 4l2 2m12 12 2 2m0-16-2 2M6 18l-2 2"/>',
spark:'<path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5z"/>',
};
const icon = (name, extra = '') => `<svg class="a-icon ${extra}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${I[name] || I.more}</svg>`;
const initialName = (document.title || 'Alfred').trim();
const dashboard = document.createElement('div');
dashboard.id = 'alfredMainV1';
dashboard.className = 'alfred-main-v1';
dashboard.innerHTML = `
<header class="av-head">
<div class="av-brand"><div class="av-logo">A</div><div><strong id="avName"></strong><small>Assistant vocal</small></div></div>
<div class="av-header-right"><div class="av-time"><strong id="avTime">--:--</strong><span id="avDate">—</span></div>
<div class="av-head-actions">
<button type="button" class="av-round" id="avTopCamera" aria-label="Ouvrir la caméra" title="Caméra">${icon('camera')}</button>
<button type="button" class="av-round" id="avTopMic" aria-label="Couper le microphone" aria-pressed="false" title="Couper le micro">${icon('mic')}</button>
<button type="button" class="av-round" id="avTopSpeaker" aria-label="Couper la sortie audio" aria-pressed="false" title="Couper le son">${icon('speaker')}</button>
<button type="button" class="av-round" id="avTopSettings" aria-label="Réglages audio" title="Réglages audio">${icon('settings')}</button>
<a class="av-round" href="/config" aria-label="Configuration d'Alfred" title="Configuration">${icon('more')}</a>
</div>
</div>
</header>
<nav class="av-rail" aria-label="Navigation principale">
<button type="button" class="av-rail-link selected" data-av-action="home">${icon('home')}<span>Accueil</span></button>
<button type="button" class="av-rail-link" data-av-action="house">${icon('bulb')}<span>Maison</span></button>
<a class="av-rail-link" href="/surveillance">${icon('server')}<span>Système</span></a>
<a class="av-rail-link" href="/surveillance">${icon('brain')}<span>IA</span></a>
<button type="button" class="av-rail-link" data-av-action="media">${icon('media')}<span>Médias</span></button>
<button type="button" class="av-rail-link" data-av-action="more">${icon('more')}<span>Plus</span></button>
</nav>
<section class="av-summary" aria-label="État de la maison et des services">
<button class="av-stat-card" type="button" data-av-action="house"><div class="av-card-top"><span class="av-circle home">${icon('home')}</span><span><strong>Maison</strong><small id="avHomeSummary">Chargement…</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p>${icon('bulb')}<span id="avLights">Lumières : —</span></p><p>${icon('home')}<span id="avCovers">Volets : —</span></p><p>${icon('sun')}<span id="avIndoor">Température : —</span></p></div></button>
<a class="av-stat-card" href="/surveillance"><div class="av-card-top"><span class="av-circle system">${icon('server')}</span><span><strong>Système</strong><small>Mini-PC Alfred</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p><span class="av-mini-dot"></span><span id="avLoad">Charge : —</span></p><p><span class="av-mini-dot"></span><span id="avMemory">Mémoire : —</span></p><p><span class="av-mini-dot"></span><span id="avDisk">Stockage : —</span></p></div></a>
<a class="av-stat-card" href="/surveillance"><div class="av-card-top"><span class="av-circle ai">${icon('brain')}</span><span><strong>Services IA</strong><small id="avAiSummary">Statuts en cours…</small></span>${icon('arrow','av-chevron')}</div><div class="av-lines"><p><span class="av-mini-dot" id="avOllamaDot"></span><span id="avOllama">Ollama : —</span></p><p><span class="av-mini-dot" id="avHermesDot"></span><span id="avHermes">Hermes : —</span></p><p><span class="av-mini-dot" id="avComfyDot"></span><span id="avComfy">ComfyUI : —</span></p></div></a>
<button class="av-stat-card av-weather" type="button" data-av-action="weather"><div class="av-card-top"><span class="av-circle weather">${icon('cloud')}</span><span><strong>Météo</strong><small>Home Assistant</small></span>${icon('arrow','av-chevron')}</div><div class="av-weather-body"><span class="av-weather-icon" id="avWeatherIcon">☁️</span><div><strong id="avWeatherTemp">—</strong><span id="avWeatherCondition">Chargement…</span><small id="avWeatherRange"></small></div></div></button>
</section>
<section class="av-voice-hero" aria-label="Commande vocale Alfred">
<div class="av-voice-controls">
<button type="button" id="avMicButton" class="av-sound-control" aria-pressed="false">${icon('mic')}<strong>Micro</strong><small id="avMicLabel">Actif</small></button>
<div class="av-voice-center"><div class="av-mic-orb" id="avOrb">${icon('mic')}</div><strong id="avVoiceTitle">En veille</strong><p id="avVoiceSubtitle">Dites « Alfred » puis votre demande</p><span class="av-status-line"><span class="av-status-dot" id="avStatusDot"></span><span id="avStatusText">Prêt à vous écouter</span></span></div>
<button type="button" id="avSpeakerButton" class="av-sound-control" aria-pressed="false">${icon('speaker')}<strong>Sortie audio</strong><small id="avSpeakerLabel">Activée</small></button>
</div>
<div class="av-wave-host" id="avWaveHost"></div>
</section>
<div class="av-shortcuts" role="group" aria-label="Raccourcis d'Alfred">
<button type="button" data-av-action="time">${icon('clock')}<span>Quelle heure est-il ?</span></button>
<button type="button" data-av-action="weather">${icon('cloud')}<span>Météo</span></button>
<button type="button" data-av-action="house">${icon('home')}<span>État de la maison</span></button>
<button type="button" data-av-action="proxmox">${icon('server')}<span>Vérifier Proxmox</span></button>
<button type="button" data-av-action="wazuh">${icon('shield')}<span>Alertes Wazuh</span></button>
<button type="button" data-av-action="comfyui">${icon('media')}<span>Créer une image</span></button>
<button type="button" data-av-action="more">${icon('more')}<span>Plus d'outils</span></button>
</div>
<footer class="av-history"><span class="av-history-heading">${icon('chat')} <strong>Interactions récentes</strong></span><div id="avHistoryList" aria-live="polite"><span class="av-history-empty">Aucune interaction depuis l’ouverture de cette page</span></div></footer>
<button type="button" class="av-back" id="avBack" hidden>${icon('close')} Retour à l’accueil</button>
<div class="av-toast" id="avToast" role="status" aria-live="polite" hidden></div>
<dialog class="av-image-dialog" id="avComfyDialog"><form method="dialog"><button type="submit" class="av-dialog-close" aria-label="Fermer">${icon('close')}</button></form><h2>Créer une image avec ComfyUI</h2><p>Décris l’image à générer. Alfred transmettra ta demande à Hermes.</p><label for="avComfyPrompt">Description</label><textarea id="avComfyPrompt" rows="4" maxlength="450" placeholder="Une maison futuriste au coucher du soleil…"></textarea><div class="av-dialog-actions"><button id="avComfySubmit" type="button">Envoyer à Alfred</button></div></dialog>
<dialog class="av-image-dialog" id="avMoreDialog"><form method="dialog"><button type="submit" class="av-dialog-close" aria-label="Fermer">${icon('close')}</button></form><h2>Outils Alfred</h2><div class="av-more-grid"><a href="/config">${icon('settings')} Configuration</a><a href="/surveillance">${icon('server')} Surveillance</a><button type="button" data-av-action="calendar">${icon('calendar')} Agenda</button><button type="button" data-av-action="camera">${icon('camera')} Caméra</button><button type="button" data-av-action="audio">${icon('speaker')} Réglages audio</button></div></dialog>
`;
shell.insertBefore(dashboard, shell.firstChild);
document.body.classList.add('alfred-glass-theme');
document.getElementById('avName').textContent = initialName;
const wave = document.getElementById('wave');
document.getElementById('avWaveHost').appendChild(wave);
window.dispatchEvent(new Event('resize'));
const $ = id => document.getElementById(id);
const timeZone = 'Europe/Paris';
let lastStatus = 'idle';
let mute = {mic_muted:false,speaker_muted:false};
let refreshingAudio = false;
let toastTimer;
function toast(message) {
const el = $('avToast'); el.textContent = String(message); el.hidden = false;
clearTimeout(toastTimer); toastTimer = setTimeout(() => { el.hidden = true; }, 5500);
}
function clock() {
const now = new Date();
$('avTime').textContent = new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(now);
const date = new Intl.DateTimeFormat('fr-FR',{timeZone,weekday:'long',day:'numeric',month:'long',year:'numeric'}).format(now);
$('avDate').textContent = date.charAt(0).toUpperCase() + date.slice(1);
}
clock(); setInterval(clock, 15000);
function updateAudio(a) {
mute = {...mute,...a};
const mic = Boolean(mute.mic_muted), speaker = Boolean(mute.speaker_muted);
for (const id of ['avTopMic','avMicButton']) {
const button = $(id); button.setAttribute('aria-pressed',String(mic));
button.setAttribute('aria-label', mic ? 'Réactiver le microphone' : 'Couper le microphone');
button.title = button.getAttribute('aria-label');
button.querySelector('svg').outerHTML = icon(mic?'micOff':'mic');
}
for (const id of ['avTopSpeaker','avSpeakerButton']) {
const button = $(id); button.setAttribute('aria-pressed',String(speaker));
button.setAttribute('aria-label', speaker?'Réactiver la sortie audio':'Couper la sortie audio');
button.title = button.getAttribute('aria-label');
button.querySelector('svg').outerHTML = icon(speaker?'speakerOff':'speaker');
}
$('avMicLabel').textContent = mic?'Coupé':'Actif';
$('avSpeakerLabel').textContent = speaker?'Coupée':'Activée';
dashboard.classList.toggle('av-mic-is-muted',mic);
dashboard.classList.toggle('av-speaker-is-muted',speaker);
renderMode(lastStatus);
}
async function refreshAudio() {
if (refreshingAudio) return;
refreshingAudio = true;
try { const r=await fetch('/api/audio/settings',{cache:'no-store'});
if (!r.ok) throw Error('HTTP '+r.status);
updateAudio(await r.json());
} catch (e) { console.warn('[ALFRED UI] Réglages audio :',e); }
finally {refreshingAudio=false;}
}
async function toggleAudio(kind) {
const prop = kind==='mic'?'mic_muted':'speaker_muted';
try {
const r=await fetch('/api/audio/settings',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({[prop]:!mute[prop]})});
const data=await r.json();
if (!r.ok) throw Error(data.error || 'HTTP '+r.status);
updateAudio(data);
toast(kind==='mic' ? (data.mic_muted?'Microphone coupé':'Microphone réactivé') : (data.speaker_muted?'Sortie audio coupée':'Sortie audio réactivée'));
} catch (e) { toast('Impossible de modifier le son : '+e.message); }
}
for (const id of ['avTopMic','avMicButton']) $(id).addEventListener('click',()=>toggleAudio('mic'));
for (const id of ['avTopSpeaker','avSpeakerButton']) $(id).addEventListener('click',()=>toggleAudio('speaker'));
$('avTopSettings').addEventListener('click',()=>{const btn=$('audioSettingsButton'); if(btn)btn.click();else location.assign('/config');});
refreshAudio(); setInterval(()=>{if(!document.hidden)refreshAudio();},30000);
function serviceText(state) {return state==='ok'?'Connecté':state==='offline'?'Hors ligne':state==='installed'?'Installé (non testé)':state==='unconfigured'?'Non configuré':'Non vérifié';}
function setService(id,dotid,label,state) {
$(id).textContent = `${label} : ${serviceText(state)}`;
$(dotid).className = `av-mini-dot ${state==='ok'?'is-ok':state==='offline'?'is-error':''}`;
}
async function refreshDashboard() {
try {
const r=await fetch('/api/alfred/dashboard',{cache:'no-store'});
if(!r.ok) throw Error('HTTP '+r.status);
const d=await r.json();
const h=d.home||{}, s=d.system||{}, ai=d.services||{}, w=d.weather||{};
$('avHomeSummary').textContent=h.available?'Home Assistant connecté':(h.detail||'Données non disponibles');
$('avLights').textContent = h.available?`${h.lights_on} lumière(s) allumée(s)`:'Lumières : indisponible';
$('avCovers').textContent = h.available?`${h.covers_closed} volet(s) fermé(s)`:'Volets : indisponible';
$('avIndoor').textContent = h.indoor_temperature!=null?`${h.indoor_temperature} °C · intérieur`:'Température : non disponible';
if (window.alfredHomeCardCustomUpdate) window.alfredHomeCardCustomUpdate();
$('avLoad').textContent = s.load_1m!=null?`Charge 1 min : ${s.load_1m}`:'Charge : non disponible';
$('avMemory').textContent = s.memory_percent!=null?`Mémoire : ${s.memory_percent} %`:'Mémoire : non disponible';
$('avDisk').textContent = s.disk_percent!=null?`Stockage : ${s.disk_percent} %`:'Stockage : non disponible';
setService('avOllama','avOllamaDot','Ollama',ai.ollama);
setService('avHermes','avHermesDot','Hermes',ai.hermes);
setService('avComfy','avComfyDot','ComfyUI',ai.comfyui);
$('avAiSummary').textContent='États vérifiés / installation locale';
$('avWeatherTemp').textContent=w.available&&w.temperature!=null?`${w.temperature} ${w.unit||'°C'}`:'—';
$('avWeatherCondition').textContent=w.available?w.condition:'Non disponible';
const sunny=['sunny','clear-night','partlycloudy'].includes(w.condition_id);
$('avWeatherIcon').textContent=sunny?'🌤️':w.condition_id==='rainy'?'🌧️':w.condition_id==='snowy'?'❄️':'☁️';
} catch (e) {
$('avHomeSummary').textContent='Données indisponibles';
$('avAiSummary').textContent='Surveillance indisponible';
$('avWeatherCondition').textContent='Indisponible';
console.warn('[ALFRED UI] Tableau de bord :',e);
}
}
refreshDashboard(); setInterval(()=>{if(!document.hidden)refreshDashboard();},30000);
async function refreshForecast() {
try {
const r=await fetch('/api/ha/weather',{cache:'no-store'});
if(!r.ok)return;
const d=await r.json();
const day=(d.daily||[])[0];
const vals=[];
if(day?.temperature!=null) vals.push('↑ '+day.temperature+'°');
if(day?.templow!=null) vals.push('↓ '+day.templow+'°');
$('avWeatherRange').textContent=vals.join(' ');
} catch (_) { /* Not all installations expose the weather forecast route. */ }
}
refreshForecast();setInterval(()=>{if(!document.hidden)refreshForecast();},180000);
function renderMode(mode) {
lastStatus = mode||'idle';
const states = {idle:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'],
waiting:['En veille','Dites « '+initialName+' » puis votre demande','Prêt'],
listening:['Je vous écoute…','Quelle est votre demande ?','Écoute en cours'],
thinking:['Je réfléchis…','Traitement de la demande','Hermes travaille'],
speaking:['Je vous réponds…','Réponse vocale en cours','Piper parle'],
error:['Un problème est survenu','Consultez la transcription ou la surveillance','Erreur']};
const item=states[mode]||states.idle;
$('avVoiceTitle').textContent=mute.mic_muted?'Microphone coupé':item[0];
$('avVoiceSubtitle').textContent=mute.mic_muted?'Réactivez le micro pour utiliser le mot d’activation':item[1];
$('avStatusText').textContent=mute.mic_muted?'Écoute désactivée':item[2];
dashboard.dataset.mode=mode;
$('avOrb').setAttribute('aria-label',item[0]);
}
renderMode('idle');
const recent=[];
let lastTranscript='';
function pushRecent(text) {
const value=String(text||'').trim();
if(!value || value===lastTranscript)return;
lastTranscript=value;
recent.unshift({text:value,at:new Date()});
if(recent.length>5)recent.pop();
const root=$('avHistoryList');root.replaceChildren();
for(const e of recent) {
const node=document.createElement('span');node.className='av-history-item';
const t=document.createElement('time');t.textContent=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit'}).format(e.at);
const content=document.createElement('span');content.textContent=e.text;
node.append(t,content);root.append(node);
}
}
function focusScene(show) {
dashboard.classList.toggle('is-focused', Boolean(show));
$('avBack').hidden = !show;
}
function onRender(d) { if(!d || typeof d!=='object')return;
if(d.type==='home')focusScene(false);
else if(d.type && d.type!=='text' || (d.text||d.title||d.url))focusScene(true);
}
const socket=window.alfredSocket;
socket?.on('alfred_state',state=>{
renderMode(String(state.mode||'idle').toLowerCase());
if(state.mode==='thinking' && state.transcript)pushRecent(state.transcript);
});
socket?.on('alfred_render',onRender);
socket?.on('audio_settings',updateAudio);
fetch('/api/state',{cache:'no-store'}).then(r=>r.ok?r.json():null).then(s=>{if(s)renderMode(String(s.mode||'idle').toLowerCase());}).catch(()=>{});
async function goHome() {
focusScene(false);
if(window.AlfredViews?.goHome){try{await window.AlfredViews.goHome();}catch(_){}}
else if($('viewHome'))$('viewHome').click();
}
$('avBack').addEventListener('click',goHome);
function openCamera() {
if($('cameraPopupButton')){ $('cameraPopupButton').click();return; }
const camera=document.querySelector('.camera-panel');
if(!camera){ toast('Caméra non installée');return; }
const isOpen=document.body.classList.toggle('av-legacy-camera-open');
const toggle=$('cameraToggle');
if(toggle && (toggle.getAttribute('aria-pressed')==='true')!==isOpen)toggle.click();
if(isOpen && !$('avLegacyCameraClose')){
const close=document.createElement('button');close.id='avLegacyCameraClose';close.type='button';close.textContent='Fermer la caméra';
close.addEventListener('click',openCamera);camera.prepend(close);
}
}
$('avTopCamera').addEventListener('click',openCamera);
function openView(name) {
const f=window.AlfredViews?.[name];
if(typeof f==='function'){ focusScene(true);f();return; }
const fallback={openWeather:'viewWeather',openTemperatures:'viewTemperatures',openCalendar:'viewCalendar'}[name];
if(fallback && $(fallback)){focusScene(true);$(fallback).click();return;}
toast('Cette fonction n’est pas disponible sur cette installation.');
}
async function quickCommand(action,description) {
try {
const r=await fetch('/api/alfred/dashboard/command',{method:'POST',headers:{'Content-Type':'application/json','X-Alfred-Request':'1'},body:JSON.stringify({action,description})});
const data=await r.json(); if(!r.ok)throw Error(data.error||'HTTP '+r.status);
pushRecent(({proxmox:'Vérifier Proxmox',wazuh:'Consulter les alertes Wazuh',comfyui:'Générer une image'})[action]||action);
toast('Demande transmise à Hermes');
} catch(e) {toast('Commande indisponible : '+e.message);}
}
function showTime() {
const current=new Intl.DateTimeFormat('fr-FR',{timeZone,hour:'2-digit',minute:'2-digit',second:'2-digit'}).format(new Date());
const content=$('content');content.replaceChildren();
const title=document.createElement('h1');title.textContent='Il est '+current;
const subtitle=document.createElement('p');subtitle.textContent='Heure de Paris · horloge locale du navigateur';
content.append(title,subtitle); focusScene(true);pushRecent('Quelle heure est-il ?');
}
async function act(name) {
if(name==='home'){await goHome();return;}
if(name==='house'){openView('openTemperatures');return;}
if(name==='weather'){openView('openWeather');return;}
if(name==='calendar'){ $('avMoreDialog').close();openView('openCalendar');return; }
if(name==='camera'){ $('avMoreDialog').close();openCamera();return; }
if(name==='media'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;}
if(name==='time'){showTime();return;}
if(name==='proxmox'||name==='wazuh'){await quickCommand(name);return;}
if(name==='comfyui'){ $('avComfyDialog').showModal();$('avComfyPrompt').focus();return;}
if(name==='more'){ $('avMoreDialog').showModal();return;}
if(name==='audio'){ $('avMoreDialog').close();$('avTopSettings').click();return; }
}
dashboard.addEventListener('click',event=>{
const button=event.target.closest('[data-av-action]');
if(button)act(button.dataset.avAction);
});
$('avComfySubmit').addEventListener('click',()=>{
const text=$('avComfyPrompt').value.trim();
if(text.length<4){toast('Décris l’image à générer.');return;}
$('avComfyDialog').close();quickCommand('comfyui',text);
});
window.addEventListener('resize',()=>{ /* legacy alfred.js recalculates the waveform canvas */ });
})();