Update to support sync captions

This commit is contained in:
2025-12-26 16:15:52 -08:00
parent 2870d45bdc
commit c28679acb6
12 changed files with 4513 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html>
<head>
<title>Multi-User Transcription Display (Polling)</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
margin: 0;
padding: 20px;
background: transparent;
font-family: Arial, sans-serif;
color: white;
}
#transcriptions {
max-height: 100vh;
overflow-y: auto;
}
.transcription {
margin: 10px 0;
padding: 10px;
background: rgba(0, 0, 0, 0.7);
border-radius: 5px;
animation: slideIn 0.3s ease-out;
transition: opacity 1s ease-out;
}
.transcription.fading {
opacity: 0;
}
.timestamp {
color: #888;
font-size: 0.9em;
margin-right: 10px;
}
.user {
font-weight: bold;
margin-right: 10px;
/* Color set dynamically via inline style */
}
.text {
color: white;
}
#status {
position: fixed;
top: 10px;
right: 10px;
padding: 10px;
background: rgba(0, 0, 0, 0.8);
border-radius: 5px;
font-size: 0.9em;
}
#status.connected { color: #4CAF50; }
#status.disconnected { color: #f44336; }
#status.polling { color: #FFC107; }
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div id="status" class="polling">🟡 Polling...</div>
<div id="transcriptions"></div>
<script>
// Get URL parameters
const urlParams = new URLSearchParams(window.location.search);
const room = urlParams.get('room') || 'default';
const fadeAfter = parseInt(urlParams.get('fade') || '10');
const showTimestamps = urlParams.get('timestamps') !== 'false';
const pollInterval = parseInt(urlParams.get('poll') || '1000'); // Poll every 1 second
const container = document.getElementById('transcriptions');
const statusEl = document.getElementById('status');
const userColors = new Map(); // Map user names to HSL colors
let colorIndex = 0;
let lastCount = 0; // Track how many transcriptions we've seen
let consecutiveErrors = 0;
let isPolling = false;
// Generate distinct color for each user using golden ratio
function getUserColor(userName) {
if (!userColors.has(userName)) {
// Use golden ratio for evenly distributed hues
const goldenRatio = 0.618033988749895;
const hue = (colorIndex * goldenRatio * 360) % 360;
// High saturation and medium lightness for vibrant, readable colors
const color = `hsl(${hue}, 85%, 65%)`;
userColors.set(userName, color);
colorIndex++;
}
return userColors.get(userName);
}
function addTranscription(data) {
const div = document.createElement('div');
div.className = 'transcription';
// Get user color (generates new color if first time)
const userColor = getUserColor(data.user_name);
let html = '';
if (showTimestamps && data.timestamp) {
html += `<span class="timestamp">[${data.timestamp}]</span>`;
}
if (data.user_name) {
html += `<span class="user" style="color: ${userColor}">${data.user_name}:</span>`;
}
html += `<span class="text">${data.text}</span>`;
div.innerHTML = html;
container.appendChild(div);
// Auto-scroll to bottom
container.scrollTop = container.scrollHeight;
// Set up fade-out if enabled
if (fadeAfter > 0) {
setTimeout(() => {
div.classList.add('fading');
setTimeout(() => {
if (div.parentNode === container) {
container.removeChild(div);
}
}, 1000);
}, fadeAfter * 1000);
}
// Limit to 100 transcriptions
while (container.children.length > 100) {
container.removeChild(container.firstChild);
}
}
// Poll for new transcriptions
async function poll() {
if (isPolling) return; // Prevent concurrent polls
isPolling = true;
try {
const url = `server.php?action=list&room=${encodeURIComponent(room)}&t=${Date.now()}`;
const response = await fetch(url, {
cache: 'no-cache',
headers: {
'Cache-Control': 'no-cache'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.transcriptions) {
const currentCount = data.transcriptions.length;
// Only show new transcriptions
if (currentCount > lastCount) {
const newTranscriptions = data.transcriptions.slice(lastCount);
newTranscriptions.forEach(addTranscription);
lastCount = currentCount;
}
// Update status
statusEl.textContent = `🟢 Connected (${currentCount})`;
statusEl.className = 'connected';
consecutiveErrors = 0;
} else {
statusEl.textContent = '🟡 Waiting for data...';
statusEl.className = 'polling';
}
} catch (error) {
console.error('Polling error:', error);
consecutiveErrors++;
if (consecutiveErrors < 5) {
statusEl.textContent = `🟡 Retrying... (${consecutiveErrors})`;
statusEl.className = 'polling';
} else {
statusEl.textContent = '🔴 Connection failed';
statusEl.className = 'disconnected';
}
} finally {
isPolling = false;
}
}
// Load initial transcriptions
async function loadInitial() {
try {
const url = `server.php?action=list&room=${encodeURIComponent(room)}&t=${Date.now()}`;
const response = await fetch(url, { cache: 'no-cache' });
const data = await response.json();
if (data.transcriptions && data.transcriptions.length > 0) {
// Show last 20 transcriptions
const recent = data.transcriptions.slice(-20);
recent.forEach(addTranscription);
lastCount = data.transcriptions.length;
}
} catch (error) {
console.error('Error loading initial transcriptions:', error);
}
}
// Start polling
async function start() {
statusEl.textContent = '🟡 Loading...';
statusEl.className = 'polling';
await loadInitial();
// Start regular polling
setInterval(poll, pollInterval);
poll(); // First poll immediately
}
// Start when page loads
start();
</script>
</body>
</html>