122 lines
4.8 KiB
JavaScript
122 lines
4.8 KiB
JavaScript
/* 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;
|
|
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(() => {});
|
|
})();
|