send2streamer/public/send.js
vrubelroman 8aeca230bc
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
Fix TTS overlap deterministically, preserve message order, fix flaky Google login button
Overlay display duration is now computed server-side from estimated
reading time (text length / speaking rate), not from the audio
element's 'ended' event — edge-tts streams MP3 without a duration
header, and OBS's embedded browser fires 'ended' unreliably for that,
which is why the earlier event-based fix didn't hold up.

Per-streamer broadcasts are now serialized through a queue, so a
message waiting on an AI reply can no longer be overtaken by a later
message from a different viewer that doesn't need one.

Also fix the Google Sign-In button intermittently not rendering: the
GSI script tag is async/defer, and both index.html and send.html/js
called google.accounts.id.initialize() without waiting for it to
actually finish loading — a race that failed silently until the
script was cached from a previous load.
2026-07-08 18:21:57 +00:00

116 lines
3.7 KiB
JavaScript
Raw 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.

const loginContainer = document.getElementById('login-container');
const mainSection = document.getElementById('hidden-until-login');
const userPic = document.getElementById('user-pic');
const userName = document.getElementById('user-name');
const logoutBtn = document.getElementById('logout');
const displayNameInput = document.getElementById('display-name');
const messageInput = document.getElementById('message');
const aiReplyCheckbox = document.getElementById('ai-reply');
const aiReplyLabel = document.getElementById('ai-reply-label');
const sendBtn = document.getElementById('send');
const statusEl = document.getElementById('status');
const token = location.pathname.split('/').filter(Boolean).pop();
const socket = io({ query: { role: 'sender', token } });
function showLoggedIn(user) {
loginContainer.style.display = 'none';
mainSection.style.display = 'block';
userPic.src = user.picture || '';
userName.textContent = user.name || user.email;
}
function showLoggedOut() {
loginContainer.style.display = 'block';
mainSection.style.display = 'none';
}
async function loadDisplayName() {
const res = await fetch(`/api/viewer-name?token=${encodeURIComponent(token)}`);
if (!res.ok) return;
const { name } = await res.json();
displayNameInput.value = name || '';
}
async function handleCredentialResponse(response) {
const res = await fetch('/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: response.credential }),
});
if (res.ok) {
const { user } = await res.json();
showLoggedIn(user);
loadDisplayName();
// The socket connected before login with no session cookie; reconnect so
// its handshake picks up the freshly-issued, now-authenticated cookie.
socket.disconnect();
socket.connect();
} else {
statusEl.textContent = 'Не удалось войти, попробуйте ещё раз';
}
}
async function loadAiName() {
const res = await fetch(`/api/streamer-info?token=${encodeURIComponent(token)}`);
if (!res.ok) return;
const { aiName } = await res.json();
if (aiName) aiReplyLabel.textContent = `Задать вопрос: ${aiName} 🤖`;
}
async function init() {
loadAiName();
const configRes = await fetch('/config');
const { googleClientId } = await configRes.json();
// The GSI script tag is async/defer — without waiting for it to actually
// finish loading, this can run before `google` exists (a race that fails
// silently), which is why the button sometimes only appeared after a
// reload (once the script was already cached).
await window.googleGsiLoaded;
google.accounts.id.initialize({
client_id: googleClientId,
callback: handleCredentialResponse,
});
google.accounts.id.renderButton(loginContainer, { theme: 'outline', size: 'large' });
const meRes = await fetch('/me');
const { user } = await meRes.json();
if (user) {
showLoggedIn(user);
loadDisplayName();
} else {
showLoggedOut();
}
}
sendBtn.addEventListener('click', () => {
const text = messageInput.value.trim();
if (!text) return;
const name = displayNameInput.value.trim();
const aiReply = aiReplyCheckbox.checked;
socket.emit('send_message', { token, text, name, aiReply });
messageInput.value = '';
statusEl.textContent = 'Отправлено';
setTimeout(() => { statusEl.textContent = ''; }, 1500);
});
messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
sendBtn.click();
}
});
socket.on('send_error', (msg) => {
statusEl.textContent = 'Ошибка: ' + msg;
});
logoutBtn.addEventListener('click', async () => {
await fetch('/auth/logout', { method: 'POST' });
showLoggedOut();
});
init();