All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
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.
116 lines
3.7 KiB
JavaScript
116 lines
3.7 KiB
JavaScript
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();
|