- Remove old: assist.html, index.html, script.js, styles.css, AI robot.lottie - Add new: droidAssist.html, droidAssist.css, droidAssist.js, animation.lottie - Restructure web-client files into html/ directory
244 lines
7.0 KiB
JavaScript
244 lines
7.0 KiB
JavaScript
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();
|
||
} |