import { DotLottie } from 'https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web@0.38.0/+esm'; // ==================== КОНФИГУРАЦИЯ ==================== const WS_URL = 'ws://192.168.200.111:8675'; const NUM_PARTICLES = 30; const LOTTIE_SRC = 'animation.lottie'; // ==================== СЕГМЕНТЫ АНИМАЦИИ ==================== const segments = { idle: { start: 0, end: 29 }, yes: { start: 31, end: 105 }, no: { start: 106, end: 180 }, alert: { start: 181, end: 270 }, thinking: { start: 271, end: 390 }, jump: { start: 391, end: 479 } }; // ==================== СОСТОЯНИЕ ==================== let ws = null; let isConnected = false; let currentVolume = 0.8; let dotLottie = null; let currentState = 'idle'; let animationReady = false; // ==================== DOM ==================== const statusDot = document.getElementById('statusDot'); const statusLabel = document.getElementById('statusLabel'); const wrapper = document.getElementById('animationWrapper'); const statusText = document.getElementById('statusText'); const volumeSlider = document.getElementById('volumeSlider'); const volumeValue = document.getElementById('volumeValue'); const volUpBtn = document.getElementById('volUpBtn'); const volDownBtn = document.getElementById('volDownBtn'); const transcriptText = document.getElementById('transcriptText'); // ==================== ЧАСТИЦЫ ==================== function initParticles() { const particles = document.getElementById('particles'); for (let i = 0; i < NUM_PARTICLES; i++) { const particle = document.createElement('div'); particle.className = 'particle'; particle.style.left = Math.random() * 100 + '%'; particle.style.animationDelay = Math.random() * 20 + 's'; particle.style.animationDuration = (15 + Math.random() * 10) + 's'; particles.appendChild(particle); } } // ==================== УПРАВЛЕНИЕ АНИМАЦИЕЙ ==================== window.goToState = function(stateName, statusLabelText) { if (!animationReady || !dotLottie) return; currentState = stateName; const seg = segments[stateName]; if (!seg) return; dotLottie.stop(); dotLottie.setSegment(seg.start, seg.end); if (stateName === 'idle') { dotLottie.setSpeed(0.1); } else { dotLottie.setSpeed(1); } dotLottie.setLoop(stateName === 'idle'); dotLottie.play(); if (statusLabelText !== undefined) { statusText.textContent = statusLabelText; } // Обновляем класс для свечения фона wrapper.className = `animation-wrapper ${stateName}`; }; // ==================== МАППИНГ СОБЫТИЙ SATELLITE → АНИМАЦИЯ ==================== function handleSatelliteEvent(eventType, eventData) { const data = eventData || {}; switch (eventType) { // ─── yes: СЛУШАЮ, ГОВОРЮ, АКТИВЕН ─── case 'voice-started': case 'stt-start': goToState('yes', 'СЛУШАЮ'); break; case 'tts-start': case 'audio-start': goToState('yes', 'ГОВОРЮ'); break; case 'streaming-started': goToState('yes', 'АКТИВЕН'); break; // ─── thinking: ОБРАБОТКА ─── case 'voice-stopped': case 'stt-stop': goToState('thinking', 'ОБРАБОТКА'); break; // ─── idle: В ОЖИДАНИИ ─── case 'played': case 'tts-stop': case 'audio-stop': goToState('idle', 'В ОЖИДАНИИ'); break; // ─── no: ОШИБКА, СПЛЮ ─── case 'satellite-disconnected': case 'error': goToState('no', 'ОШИБКА'); break; case 'pause-satellite': goToState('no', 'СПЛЮ...'); break; // ─── jump: ПРИВЕТ!!! ─── case 'detection': goToState('jump', 'ПРИВЕТ!!!'); break; // ─── transcript / synthesize ─── case 'transcript': if (data.text) { transcriptText.textContent = `"${data.text}"`; goToState('yes'); } break; case 'synthesize': if (data.text) { transcriptText.textContent = `"${data.text}"`; goToState('yes'); } break; // ─── audio-level ─── case 'audio-level': break; default: if (eventType !== 'audio-level') { console.log('Unhandled event:', eventType, data); } break; } } // ==================== WEBSOCKET ==================== function connectWebSocket() { ws = new WebSocket(WS_URL); ws.onopen = () => { isConnected = true; statusDot.classList.add('connected'); statusLabel.textContent = 'Подключено'; console.log('✅ WebSocket connected'); }; ws.onmessage = (event) => { try { const data = JSON.parse(event.data); const eventType = data.type || data.event; const eventData = data.data || {}; if (eventType != "audio-level") { console.info('EventType:', eventType); console.info('EventData:', eventData); } handleSatelliteEvent(eventType, eventData); } catch (e) { console.error('Parse error:', e); } }; ws.onerror = (error) => console.error('WebSocket error:', error); ws.onclose = () => { isConnected = false; statusDot.classList.remove('connected'); statusLabel.textContent = 'Отключено'; handleSatelliteEvent('satellite-disconnected'); console.log('❌ WebSocket disconnected'); setTimeout(connectWebSocket, 3000); }; } // ==================== ГРОМКОСТЬ ==================== function updateVolume(newVolume) { currentVolume = Math.max(0, Math.min(1, newVolume)); volumeSlider.value = Math.round(currentVolume * 100); volumeValue.textContent = Math.round(currentVolume * 100) + '%'; if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ command: 'set_volume', data: { level: currentVolume } })); } } // ==================== ИНИЦИАЛИЗАЦИЯ ==================== async function init() { const canvas = document.getElementById('lottie-canvas'); const overlay = document.getElementById('loadingOverlay'); initParticles(); connectWebSocket(); dotLottie = new DotLottie({ canvas: canvas, src: LOTTIE_SRC, autoplay: false, loop: true, renderConfig: { preserveAspectRatio: 'xMidYMid meet' } }); dotLottie.addEventListener('load', () => { animationReady = true; overlay.classList.add('hidden'); goToState('idle', 'В ОЖИДАНИИ'); }); dotLottie.addEventListener('complete', () => { if (currentState !== 'idle') { goToState('idle', 'В ОЖИДАНИИ'); } }); // Volume controls volumeSlider.addEventListener('input', (e) => updateVolume(e.target.value / 100)); volUpBtn.addEventListener('click', () => updateVolume(currentVolume + 0.1)); volDownBtn.addEventListener('click', () => updateVolume(currentVolume - 0.1)); } // Запуск после загрузки страницы if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }