231 lines
7.8 KiB
PHP
231 lines
7.8 KiB
PHP
|
|
<!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>
|