/* 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:'', bulb:'', server:'', brain:'', media:'', more:'', mic:'', micOff:'', speaker:'', speakerOff:'', camera:'', settings:'', calendar:'', clock:'', cloud:'', shield:'', arrow:'', chat:'', close:'', sun:'', spark:'', }; const icon = (name, extra = '') => ``; const initialName = (document.title || 'Alfred').trim(); const dashboard = document.createElement('div'); dashboard.id = 'alfredMainV1'; dashboard.className = 'alfred-main-v1'; dashboard.innerHTML = `
Assistant vocal
--:--—
${icon('more')}
${icon('server')}SystèmeMini-PC Alfred${icon('arrow','av-chevron')}

Charge : —

Mémoire : —

Stockage : —

${icon('brain')}Services IAStatuts en cours…${icon('arrow','av-chevron')}

Ollama : —

Hermes : —

ComfyUI : —

${icon('mic')}
En veille

Dites « Alfred » puis votre demande

Prêt à vous écouter
${icon('chat')} Interactions récentes
Aucune interaction depuis l’ouverture de cette page

Créer une image avec ComfyUI

Décris l’image à générer. Alfred transmettra ta demande à Hermes.

Outils Alfred

${icon('settings')} Configuration${icon('server')} Surveillance
`; 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 */ }); })();