Files
satellite/web-client/html/droidAssist.js
sirmrak 3cc8bb8aef refactor: update web-client files - replace old HTML/JS/CSS with new droidAssist implementation
- 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
2026-05-28 13:19:00 +03:00

244 lines
7.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}