/* Interface principale Alfred : statut, scène, onde et transcription Vosk. */ (() => { 'use strict'; const socket = (typeof io === 'function') ? io() : null; window.alfredSocket = socket; const content = document.getElementById('content'); const modeEl = document.getElementById('mode'); const statusDot = document.getElementById('statusDot'); const transcriptText = document.getElementById('transcriptText'); const transcriptState = document.getElementById('transcriptState'); const transcriptPanel = document.querySelector('.transcript-panel'); const canvas = document.getElementById('waveCanvas'); const ctx = canvas.getContext('2d'); let mode = 'idle', level = 0.02, phase = 0, previousContent = ''; const labels = { idle:'En attente', waiting:'En attente', listening:'À l’écoute', thinking:'Réflexion en cours', speaking:'Alfred parle', error:'Erreur' }; function resize() { const dpr = window.devicePixelRatio || 1; canvas.width = Math.round(canvas.clientWidth * dpr); canvas.height = Math.round(canvas.clientHeight * dpr); } window.addEventListener('resize', resize); resize(); function draw() { const dpr = window.devicePixelRatio || 1; phase += 0.035; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = 'rgba(232,241,243,.76)'; ctx.lineWidth = 1.6 * dpr; ctx.beginPath(); const mid = canvas.height / 2; const amp = (mode === 'idle' ? 1.5 : mode === 'thinking' ? 7 : Math.max(5, level * 45)) * dpr; for (let x = 0; x < canvas.width; x += 4 * dpr) { const y = mid + Math.sin(x * 0.018 + phase * 4) * Math.sin(x * 0.006 + phase) * amp * (0.35 + 0.65 * Math.sin(x / canvas.width * Math.PI)); if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); window.requestAnimationFrame(draw); } window.requestAnimationFrame(draw); function makeText(tag, value, className) { const el = document.createElement(tag); if (className) el.className = className; el.textContent = String(value ?? ''); return el; } function render(data) { if (!data || typeof data !== 'object') return; const key = JSON.stringify(data); if (key === previousContent) return; previousContent = key; if (window.AlfredViews && window.AlfredViews.canRender(data)) { window.AlfredViews.render(data); return; } content.classList.remove('alfred-view'); const fragment = document.createDocumentFragment(); if (data.type === 'image' && typeof data.url === 'string') { // Refuser les URL javascript: et les schémas non prévus. if (/^(https?:\/\/|\/[^/])/i.test(data.url)) { const img = document.createElement('img'); img.className = 'hero'; img.src = data.url; img.alt = String(data.alt || 'Image affichée par Alfred'); fragment.appendChild(img); } if (data.caption) fragment.appendChild(makeText('p', data.caption)); } else if (data.type === 'cards' && Array.isArray(data.items)) { const cards = document.createElement('div'); cards.className = 'cards'; data.items.forEach(item => { const card = makeText('article', '', 'card'); card.append(makeText('h2', item.title || ''), makeText('p', item.text || '')); cards.appendChild(card); }); fragment.appendChild(cards); } else { if (data.title) fragment.appendChild(makeText('h1', data.title)); fragment.appendChild(makeText('p', data.text || '')); } content.replaceChildren(fragment); } function updateTranscript(s) { // Une phrase finale reste visible lors du retour en veille. const finalText = String(s.transcript || '').trim(); const partial = String(s.partial || '').trim(); if (finalText) { transcriptText.textContent = finalText; transcriptText.classList.remove('is-placeholder'); transcriptState.textContent = 'Reconnu'; } else if (s.mode === 'listening') { transcriptText.textContent = partial || 'Je vous écoute…'; transcriptText.classList.toggle('is-placeholder', !partial); transcriptState.textContent = partial ? 'En cours' : 'Écoute'; } else if (s.mode === 'error') { transcriptState.textContent = 'Erreur'; } transcriptPanel.dataset.active = String(s.mode === 'listening'); } function updateState(s) { if (!s || typeof s !== 'object') return; mode = String(s.mode || 'idle').toLowerCase(); modeEl.textContent = labels[mode] || mode; modeEl.dataset.mode = mode; statusDot.dataset.mode = mode; updateTranscript(s); if (s.content) render(s.content); } if (socket) { socket.on('alfred_state', updateState); socket.on('alfred_render', render); socket.on('audio_level', data => { level = Math.max(0, Math.min(1, Number(data.level) || 0)); }); socket.on('connect', () => { fetch('/api/state', {cache:'no-store'}).then(r => r.json()).then(updateState).catch(() => {}); }); } fetch('/api/state', {cache:'no-store'}).then(r => r.json()).then(updateState).catch(() => {}); })();