opencompletion.com/templates/chat.html
Claude 6fa1b15188
Add auto-growing textarea for chat input
- Textarea now automatically expands as user types multiline messages
- Resets to minimum height after message is sent
- CSS: Set min-height (60px) and max-height (400px) with auto overflow
- Removed fixed rows attribute to allow dynamic height
- Disabled manual resize to prevent user confusion
- Provides better UX for composing longer messages
2025-11-08 23:02:24 +00:00

1614 lines
62 KiB
HTML

{% extends "base.html" %}
{% block title %}Chatroom{% endblock %}
{% block content %}
<div id="chat-container">
<!-- Chat area where messages will be displayed -->
<div id="chat"></div>
<!-- Form for sending messages -->
<form id="message-form">
<textarea id="message" placeholder="Type your message..."></textarea>
</form>
</div>
<div class="utility-belt">
<div class="download-links">
History
<a href="/download_chat_history?room_name={{ room_name }}" download="{{ room_name }}.json">JSON</a> or
<a href="/download_chat_history_md?room_name={{ room_name }}" download="{{ room_name }}.md">Markdown</a>
</div>
<br>
<div>
<label for="username-input">Username</label>
<input type="text" id="username-input" placeholder="guest" maxlength="50">
</div>
<div>
<label for="model-select">Model</label>
<select id="model-select">
<option value="None">None</option>
</select>
</div>
<div>
<label for="voice-select">Voice</label>
<select id="voice-select">
<option value="onyx">Onyx</option>
<option value="alloy">Alloy</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="nova">Nova</option>
<option value="shimmer">Shimmer</option>
</select>
</div>
<div>
<button id="theme-toggle-btn" onclick="toggleTheme()">
🌙 Dark Mode
</button>
</div>
<div>
<button id="auto-play-tts-btn" onclick="toggleAutoPlayTTS()" style="width: 100%; margin-top: 10px; background-color: #f44336; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
Auto-Play TTS: OFF
</button>
</div>
<div id="activity-controls">
<h3>Activities</h3>
<div id="current-activity-info" style="display: none;">
<p>Current Activity: <span id="current-activity-name"></span></p>
<button id="cancel-activity-btn" onclick="cancelActivity()">Cancel Activity</button>
</div>
<div id="activity-list-section">
<div style="display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px; margin-bottom: 5px;">
<select id="activity-select">
<option value="">-- Select an Activity --</option>
</select>
<button id="refresh-activities-btn" onclick="refreshActivityList()">🔄</button>
</div>
<button id="load-activity-btn" onclick="loadSelectedActivity()" style="margin-top: 5px;">Load Activity</button>
</div>
</div>
<div id="user-lists">
<div id="active-users-list">
<h3>Active Users</h3>
<ul id="active-users">
<!-- Active users will be dynamically populated here -->
</ul>
</div>
<div id="inactive-users-list">
<h3>Inactive Users</h3>
<ul id="inactive-users">
<!-- Inactive users will be dynamically populated here -->
</ul>
</div>
</div>
</div>
<script>
// Constants
const API_KEY = "dummy-api-key";
const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
const CODE_EXEC_URL = "https://code.ai.unturf.com"; // Code execution service URL (served via Caddy)
const urlParams = new URLSearchParams(window.location.search);
let username = urlParams.get("username") || "guest"; // Default to "guest" if no username in URL
// If no username was in the URL, add it now
if (!urlParams.get("username")) {
const newUrl = new URL(window.location.href);
newUrl.searchParams.set("username", username);
window.history.replaceState({}, '', newUrl);
}
const room_name = "{{ room_name }}";
// Global constants for valid voices
const VALID_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
// Configuration for DOMPurify to specify which tags and attributes are allowed
const dompurify_config = {
ADD_TAGS: ["iframe", "img", "video"],
FORBID_TAGS: ["form"],
ALLOWED_ATTR: [
"src", "width", "height", "frameborder", "allowfullscreen",
"alt", "class", "title", "style", "controls",
]
};
// keeping track of scrolling to prevent autoscrolling.
let userHasScrolledUp = false;
let currentAudio = null; // To keep track of the currently playing audio
let currentQueuedAudio = null; // To keep track of currently playing queued TTS audio
let audioCache = {}; // Cache to store audio blobs
// Flag to prevent mutual updates on desktop/mobile
let isSyncingDropdowns = false;
// Auto-play TTS state
let autoPlayTTS = localStorage.getItem('autoPlayTTS') === 'true' || false;
let ttsQueue = [];
let isPlayingTTS = false;
// Function to sanitize the username
function sanitizeUsername(username) {
// Split the username on commas and take the first part.
// The backend denormalizes the user list in the room table via csv.
return username.split(',')[0].trim();
}
// Function to copy message content to clipboard
function copyMessageContent(content) {
navigator.clipboard.writeText(content).then(() => {
// Optional: show a temporary success message
const button = event.currentTarget;
const originalText = button.textContent;
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Error copying text: ', err);
});
}
// Function to sync all inputs and update the query string
function syncInputsAndQueryString() {
const usernameInputDesktop = document.getElementById("username-input");
const usernameInputMobile = document.getElementById("username-input-mobile");
const modelSelectDesktop = document.getElementById("model-select");
const voiceSelectDesktop = document.getElementById("voice-select");
const modelSelectMobile = document.getElementById("model-select-mobile");
const voiceSelectMobile = document.getElementById("voice-select-mobile");
// Get current values
const currentUsername = usernameInputDesktop?.value || username || "guest";
const currentModel = modelSelectDesktop.value;
const currentVoice = VALID_VOICES.includes(voiceSelectDesktop.value) ? voiceSelectDesktop.value : 'onyx';
// Update global username variable
username = currentUsername;
const sanitizedUsername = sanitizeUsername(username);
// Sync username inputs
if (usernameInputDesktop) usernameInputDesktop.value = sanitizedUsername;
if (usernameInputMobile) usernameInputMobile.value = sanitizedUsername;
// Sync dropdowns
modelSelectDesktop.value = currentModel;
voiceSelectDesktop.value = currentVoice;
if (modelSelectMobile) modelSelectMobile.value = currentModel;
if (voiceSelectMobile) voiceSelectMobile.value = currentVoice;
// Save to localStorage for persistence
localStorage.setItem('selectedModel', currentModel);
localStorage.setItem('selectedVoice', currentVoice);
// Update URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.set("username", sanitizedUsername);
newUrl.searchParams.set("model", currentModel);
newUrl.searchParams.set("voice", currentVoice);
window.history.replaceState({}, '', newUrl);
// Update room links with new parameters
if (typeof updateRoomLinksWithCurrentParams === 'function') {
updateRoomLinksWithCurrentParams();
}
}
// Backward compatibility
function syncDropdownsAndQueryString() {
syncInputsAndQueryString();
}
document.addEventListener('DOMContentLoaded', (event) => {
const chatContainer = document.getElementById("chat");
const modelSelectDesktop = document.getElementById("model-select");
const voiceSelectDesktop = document.getElementById("voice-select");
const modelSelectMobile = document.getElementById("model-select-mobile");
const voiceSelectMobile = document.getElementById("voice-select-mobile");
// Initialize auto-play TTS button state from localStorage
updateAutoPlayTTSDisplay();
// Function to populate the dropdown
function populateModelDropdown(models) {
// Clear options starting from index 1 (preserve "None" at index 0)
while (modelSelectDesktop.options.length > 1) {
modelSelectDesktop.remove(1);
}
// Append new model options
models.forEach(modelId => {
const option = document.createElement('option');
option.value = modelId;
option.textContent = modelId;
modelSelectDesktop.appendChild(option);
});
// Set initial value from URL
const urlParams = new URLSearchParams(window.location.search);
const initialModel = urlParams.get("model") || "None";
modelSelectDesktop.value = initialModel;
}
// Memoization with localStorage (1-minute cache)
const cacheKey = 'modelList';
const cacheExpirationKey = 'modelListExpiration';
const cacheDuration = 60 * 1000; // 1 minute in milliseconds
const cachedData = localStorage.getItem(cacheKey);
const cachedExpiration = localStorage.getItem(cacheExpirationKey);
if (cachedData && cachedExpiration && Date.now() < parseInt(cachedExpiration)) {
// Use cached data if it exists and hasn't expired
const models = JSON.parse(cachedData);
populateModelDropdown(models);
} else {
// Fetch from backend and update cache
fetch('/models')
.then(response => response.json())
.then(data => {
const models = data.models;
populateModelDropdown(models);
// Store in localStorage with expiration
localStorage.setItem(cacheKey, JSON.stringify(models));
localStorage.setItem(cacheExpirationKey, Date.now() + cacheDuration);
})
.catch(error => console.error("Error fetching models:", error));
}
chatContainer.addEventListener('scroll', () => {
const distanceFromBottom = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight;
userHasScrolledUp = distanceFromBottom > 5;
});
// Load model and voice from localStorage if not in URL
const storedModel = localStorage.getItem('selectedModel');
const storedVoice = localStorage.getItem('selectedVoice');
// Set initial model, voice, and username from URL, localStorage, or defaults
const initialModel = urlParams.get("model") || storedModel || "None";
const initialVoice = urlParams.get("voice") || storedVoice || "onyx";
const initialUsername = username; // Already set to URL param or "guest"
modelSelectDesktop.value = initialModel;
voiceSelectDesktop.value = initialVoice;
modelSelectMobile.value = initialModel;
voiceSelectMobile.value = initialVoice;
// Set initial username values
const usernameInputDesktop = document.getElementById("username-input");
const usernameInputMobile = document.getElementById("username-input-mobile");
if (usernameInputDesktop) usernameInputDesktop.value = initialUsername;
if (usernameInputMobile) usernameInputMobile.value = initialUsername;
// Initial sync to ensure localStorage and URL are updated with current values
syncInputsAndQueryString();
// Add event listeners for desktop dropdowns
modelSelectDesktop.addEventListener("change", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
modelSelectMobile.value = modelSelectDesktop.value;
syncDropdownsAndQueryString();
isSyncingDropdowns = false;
});
voiceSelectDesktop.addEventListener("change", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
voiceSelectMobile.value = voiceSelectDesktop.value;
syncDropdownsAndQueryString();
isSyncingDropdowns = false;
});
// Add event listeners for mobile dropdowns
modelSelectMobile.addEventListener("change", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
modelSelectDesktop.value = modelSelectMobile.value;
syncDropdownsAndQueryString();
isSyncingDropdowns = false;
});
voiceSelectMobile.addEventListener("change", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
voiceSelectDesktop.value = voiceSelectMobile.value;
syncDropdownsAndQueryString();
isSyncingDropdowns = false;
});
// Add event listeners for username inputs
if (usernameInputDesktop) {
usernameInputDesktop.addEventListener("input", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
if (usernameInputMobile) {
usernameInputMobile.value = usernameInputDesktop.value;
}
syncInputsAndQueryString();
isSyncingDropdowns = false;
});
usernameInputDesktop.addEventListener("blur", () => {
syncInputsAndQueryString();
});
}
if (usernameInputMobile) {
usernameInputMobile.addEventListener("input", () => {
if (isSyncingDropdowns) return;
isSyncingDropdowns = true;
if (usernameInputDesktop) {
usernameInputDesktop.value = usernameInputMobile.value;
}
syncInputsAndQueryString();
isSyncingDropdowns = false;
});
usernameInputMobile.addEventListener("blur", () => {
syncInputsAndQueryString();
});
}
});
// Socket event when the user connects
socket.on("connect", () => {
// Sanitize the username before joining
const sanitizedUsername = sanitizeUsername(username);
socket.emit("join", {"username": sanitizedUsername, "room_name": room_name});
// Sync inputs and update the query string
syncInputsAndQueryString();
});
// Function to update the active and inactive user lists in the DOM
function updateUserLists(activeUsers, inactiveUsers) {
const activeUserListElement = document.getElementById("active-users");
const inactiveUserListElement = document.getElementById("inactive-users");
const activeUserListElementMobile = document.getElementById("active-users-mobile");
const inactiveUserListElementMobile = document.getElementById("inactive-users-mobile");
activeUserListElement.innerHTML = ''; // Clear the current list
inactiveUserListElement.innerHTML = ''; // Clear the current list
activeUserListElementMobile.innerHTML = ''; // Clear the current list (mobile)
inactiveUserListElementMobile.innerHTML = ''; // Clear the current list (mobile)
// Populate the list with active users (desktop)
activeUsers.forEach(username => {
const userItem = document.createElement("li");
userItem.textContent = username;
activeUserListElement.appendChild(userItem);
});
// Populate the list with inactive users (desktop)
inactiveUsers.forEach(username => {
const userItem = document.createElement("li");
userItem.textContent = username;
inactiveUserListElement.appendChild(userItem);
});
// Populate the list with active users (mobile)
activeUsers.forEach(username => {
const userItemMobile = document.createElement("li");
userItemMobile.textContent = username;
activeUserListElementMobile.appendChild(userItemMobile);
});
// Populate the list with inactive users (mobile)
inactiveUsers.forEach(username => {
const userItemMobile = document.createElement("li");
userItemMobile.textContent = username;
inactiveUserListElementMobile.appendChild(userItemMobile);
});
}
// Update user lists whenever the event is received
socket.on("active_users", (data) => {
updateUserLists(data.active_users, data.inactive_users);
});
// Function to handle sending the message
function sendMessage() {
const messageTextarea = document.getElementById("message");
const message = messageTextarea.value;
const model = document.getElementById("model-select").value;
let messageToSend = message.trim();
if (messageToSend !== "") { // Ensure we're not sending empty messages
socket.emit("chat_message", {
"username": username,
"message": messageToSend,
"model": model, // Pass model as a separate attribute
"room_name": room_name
});
messageTextarea.value = "";
// Reset textarea height after sending
messageTextarea.style.height = 'auto';
}
}
// Function to handle deleting a message
function deleteMessage(messageId, room_name) {
socket.emit("delete_message", {"message_id": messageId, "room_name": room_name});
}
// Event listener for form submission to send a message
document.getElementById("message-form").addEventListener("submit", (e) => {
e.preventDefault();
sendMessage();
});
// Event listener for the Enter key press in the textarea to send a message
document.getElementById("message").addEventListener("keydown", function(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Auto-grow textarea as user types
document.getElementById("message").addEventListener("input", function() {
// Reset height to auto to get the correct scrollHeight
this.style.height = 'auto';
// Set height to scrollHeight to fit content
this.style.height = this.scrollHeight + 'px';
});
// Socket event for updating the room title
socket.on("update_room_title", (data) => {
document.title = data.title; // Update the window's title
});
// Socket event to update the room title in the sidebar.
socket.on('update_room_list', function(updatedRoom) {
// Find the room list item by its data-room-id attribute
const roomListItem = document.querySelector(`#rooms-list li[data-room-id="${updatedRoom.id}"]`);
if (roomListItem) {
// Update the room list item's content with the new title
roomListItem.innerHTML = `<b>${updatedRoom.name}</b> ${updatedRoom.title ? '<br />' + updatedRoom.title : ''}`;
}
});
// Function to read text using TTS (for manual button clicks)
async function speakText(text, playButton, messageId) {
console.log("speakText called with:", {text: text, messageId, autoPlay: autoPlayTTS});
const voice = document.getElementById("voice-select").value;
const cacheKey = `${messageId}-${voice}`; // Unique cache key for each message and voice
// Clean the text to include only alphanumeric characters, spaces, and key punctuation
const cleanText = text.replace(/[^a-zA-Z0-9\s.,!?]/g, '');
try {
// Check if the audio is already cached
if (audioCache[cacheKey]) {
const audio = audioCache[cacheKey];
toggleAudioPlayback(audio, playButton);
return;
}
// Set button to processing state
playButton.textContent = "Processing...";
playButton.disabled = true;
const response = await fetch(TTS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'tts-1',
voice: voice,
input: cleanText // Use the cleaned text
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.playbackRate = 0.9;
// Cache the audio only after it is successfully created
audioCache[cacheKey] = audio;
// Enable button and change text to "Pause"
playButton.disabled = false;
toggleAudioPlayback(audio, playButton);
} catch (error) {
console.error('Error in TTS:', error);
playButton.textContent = "Play"; // Reset button text on error
playButton.disabled = false;
}
}
// Function to read text using TTS (for queued auto-play)
async function speakTextQueued(text, playButton, messageId) {
return new Promise((resolve, reject) => {
const voice = document.getElementById("voice-select").value;
const cacheKey = `${messageId}-${voice}`;
const cleanText = text.replace(/[^a-zA-Z0-9\s.,!?]/g, '');
const playAudio = (audio) => {
currentQueuedAudio = audio; // Track the currently playing queued audio
audio.onended = () => {
console.log("TTS finished for:", messageId);
currentQueuedAudio = null; // Clear when finished
resolve();
};
audio.onerror = () => {
console.error("TTS audio error for:", messageId);
currentQueuedAudio = null; // Clear on error
reject(new Error("Audio playback failed"));
};
audio.play().catch(reject);
};
// Check if audio is cached
if (audioCache[cacheKey]) {
playAudio(audioCache[cacheKey]);
return;
}
// Fetch new audio
fetch(TTS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'tts-1',
voice: voice,
input: cleanText
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
})
.then(audioBlob => {
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.playbackRate = 0.9;
audioCache[cacheKey] = audio;
playAudio(audio);
})
.catch(reject);
});
}
// Function to add TTS to queue
function queueTTS(text, playButton, messageId) {
ttsQueue.push({ text, playButton, messageId });
console.log("Added to TTS queue:", messageId, "Queue length:", ttsQueue.length);
processNextTTS();
}
// Function to process the next TTS in queue
function processNextTTS() {
if (isPlayingTTS || ttsQueue.length === 0) {
return;
}
isPlayingTTS = true;
const { text, playButton, messageId } = ttsQueue.shift();
console.log("Processing TTS from queue:", messageId);
// Use non-blocking async processing
speakTextQueued(text, playButton, messageId)
.then(() => {
console.log("TTS completed successfully for:", messageId);
})
.catch((error) => {
console.error("TTS error:", error);
})
.finally(() => {
isPlayingTTS = false;
// Schedule next item with minimal delay to prevent blocking
setTimeout(processNextTTS, 10);
});
}
// Function to update auto-play TTS button display
function updateAutoPlayTTSDisplay() {
const autoPlayBtn = document.getElementById("auto-play-tts-btn");
const autoPlayBtnMobile = document.getElementById("auto-play-tts-btn-mobile");
if (autoPlayTTS) {
autoPlayBtn.textContent = "Auto-Play TTS: ON";
autoPlayBtn.style.backgroundColor = "#4CAF50";
autoPlayBtnMobile.textContent = "Auto-Play TTS: ON";
autoPlayBtnMobile.style.backgroundColor = "#4CAF50";
} else {
autoPlayBtn.textContent = "Auto-Play TTS: OFF";
autoPlayBtn.style.backgroundColor = "#f44336";
autoPlayBtnMobile.textContent = "Auto-Play TTS: OFF";
autoPlayBtnMobile.style.backgroundColor = "#f44336";
// Clear queue when turning off
ttsQueue = [];
isPlayingTTS = false;
}
}
// Function to toggle auto-play TTS
function toggleAutoPlayTTS() {
autoPlayTTS = !autoPlayTTS;
console.log("Auto-play TTS toggled to:", autoPlayTTS);
// Save to localStorage
localStorage.setItem('autoPlayTTS', autoPlayTTS.toString());
// If turning off, clear the queue and stop current audio
if (!autoPlayTTS) {
console.log("Clearing TTS queue, had", ttsQueue.length, "items");
ttsQueue = [];
isPlayingTTS = false;
// Stop any currently playing audio
if (currentAudio) {
currentAudio.pause();
currentAudio.currentTime = 0;
if (currentAudio.playButton) {
currentAudio.playButton.textContent = "Play";
}
currentAudio = null;
}
// Stop any currently playing queued TTS audio
if (currentQueuedAudio) {
currentQueuedAudio.pause();
currentQueuedAudio.currentTime = 0;
currentQueuedAudio = null;
}
}
updateAutoPlayTTSDisplay();
}
// Function to toggle audio playback
function toggleAudioPlayback(audio, playButton) {
if (currentAudio && currentAudio !== audio) {
currentAudio.pause();
currentAudio.currentTime = 0;
currentAudio.playButton.textContent = "Play";
}
if (audio.paused) {
audio.play();
playButton.textContent = "Pause";
} else {
audio.pause();
playButton.textContent = "Play";
}
currentAudio = audio;
currentAudio.playButton = playButton;
audio.onended = () => {
playButton.textContent = "Play";
};
}
// Socket event for receiving a new message
socket.on("chat_message", (data) => {
const messageWrapper = document.createElement("div");
messageWrapper.className = "message-wrapper";
messageWrapper.id = "message-" + data.id;
const newMessage = document.createElement("div");
newMessage.className = "message-content";
// Check if the username is present and prepend it to the message content
const messageContent = data.username ? `**${data.username}:**\n\n${data.content}` : data.content;
// Check if the message starts with a base64 image tag
if (data.content.startsWith('<img src="data:image/jpeg;base64')) {
// Directly assign the message as innerHTML if it starts with a base64 image
newMessage.innerHTML = data.content;
} else {
// Otherwise, sanitize and process the message with marked
newMessage.innerHTML = DOMPurify.sanitize(marked.marked(messageContent), dompurify_config);
}
// Check if the message has an id which means we can delete it.
if (data.id) {
newMessage.dataset.rawMarkdown = data.content;
// Create a container for the buttons
const buttonContainer = document.createElement("div");
buttonContainer.className = "button-container";
// Create the delete button
const deleteButton = document.createElement("button");
deleteButton.innerHTML = "x";
deleteButton.onclick = () => deleteMessage(data.id, room_name);
buttonContainer.appendChild(deleteButton);
// Create the edit button
const editButton = document.createElement("button");
editButton.textContent = "Edit";
editButton.className = "edit-button";
editButton.onclick = () => editMessage(data.id, newMessage, data.content);
buttonContainer.appendChild(editButton);
// Create the copy button
const copyButton = document.createElement("button");
copyButton.textContent = "Copy";
copyButton.className = "copy-button";
copyButton.onclick = () => copyMessageContent(data.content);
buttonContainer.appendChild(copyButton);
// Create the play button for TTS
const playButton = document.createElement("button");
playButton.textContent = "Play";
playButton.onclick = () => speakText(data.content, playButton, data.id);
buttonContainer.appendChild(playButton);
messageWrapper.appendChild(buttonContainer);
}
messageWrapper.appendChild(newMessage);
document.getElementById("chat").appendChild(messageWrapper);
// Apply syntax highlighting to code blocks within the message
newMessage.querySelectorAll("pre code").forEach((block) => {
addCopyButtonToCodeBlock(block);
truncateCodeBlock(block);
hljs.highlightElement(block);
addLineNumbers(block);
});
// Scroll to the bottom of the chat container to show the new message.
if (data.id) {
document.getElementById("chat").scrollTop = document.getElementById("chat").scrollHeight;
// Auto-play TTS if enabled and message has content - AFTER buttons are created
if (autoPlayTTS && data.content && data.content.trim() !== "") {
setTimeout(() => {
// Find the play button after buttons have been created
const buttons = messageWrapper.querySelectorAll("button");
const playButton = Array.from(buttons).find(btn => btn.textContent === "Play");
console.log("Auto-play TTS: enabled=", autoPlayTTS, "content=", data.content, "playButton=", playButton);
if (playButton) {
console.log("Queueing TTS for message:", data.id);
queueTTS(data.content, playButton, data.id);
}
}, 10); // Very short delay to let buttons be created
}
}
});
// Socket event for receiving previous messages
socket.on("previous_messages", (data) => {
if (document.getElementById("message-" + data.id)) {
// If it exists, skip appending it
return;
}
const messageWrapper = document.createElement("div");
messageWrapper.className = "message-wrapper";
messageWrapper.id = "message-" + data.id;
const newMessage = document.createElement("div");
newMessage.className = "message-content";
// Check if the message contains a base64 image
if (data.content.startsWith('<img src="data:image/jpeg;base64')) {
// Directly assign the message as innerHTML if it's a base64 image
newMessage.innerHTML = data.content;
} else {
// Otherwise, sanitize and process the message with marked
newMessage.innerHTML = DOMPurify.sanitize(marked.marked(`**${data.username}:**\n\n${data.content}`), dompurify_config);
}
newMessage.dataset.rawMarkdown = data.content;
// Create a container for the buttons
const buttonContainer = document.createElement("div");
buttonContainer.className = "button-container";
// Create the delete button
const deleteButton = document.createElement("button");
deleteButton.innerHTML = "x";
deleteButton.onclick = () => deleteMessage(data.id, room_name);
buttonContainer.appendChild(deleteButton);
// Create the edit button
const editButton = document.createElement("button");
editButton.textContent = "Edit";
editButton.className = "edit-button";
editButton.onclick = () => editMessage(data.id, newMessage);
buttonContainer.appendChild(editButton);
// Create the copy button
const copyButton = document.createElement("button");
copyButton.textContent = "Copy";
copyButton.className = "copy-button";
copyButton.onclick = () => copyMessageContent(data.content);
buttonContainer.appendChild(copyButton);
// Create the play button for TTS
const playButton = document.createElement("button");
playButton.textContent = "Play";
playButton.onclick = () => speakText(data.content, playButton, data.id);
buttonContainer.appendChild(playButton);
messageWrapper.appendChild(buttonContainer);
messageWrapper.appendChild(newMessage);
document.getElementById("chat").appendChild(messageWrapper);
// Apply syntax highlighting to code blocks within the message
newMessage.querySelectorAll("pre code").forEach((block) => {
addCopyButtonToCodeBlock(block);
truncateCodeBlock(block);
hljs.highlightElement(block);
addLineNumbers(block);
});
// Scroll to the bottom of the chat container
document.getElementById("chat").scrollTop = document.getElementById("chat").scrollHeight;
});
// Socket event for deleting a processing message
socket.on("delete_processing_message", (msg_id) => {
const tempMessages = document.querySelectorAll("#message-null");
tempMessages.forEach((tempMessage) => {
tempMessage.remove();
});
// Clear the message buffer and header for the corresponding message ID
delete messageBuffers[msg_id];
delete messageHeaders[msg_id];
});
// A dictionary to hold buffers for each message ID
const messageBuffers = {};
// A dictionary to track message headers (username/model) for each message ID
const messageHeaders = {};
// Socket event for receiving chunks of a message
socket.on("message_chunk", (data) => {
const wrapperId = "message-" + data.id;
let messageWrapper = document.getElementById(wrapperId);
let targetMessageElement;
// If the message wrapper doesn't exist, create it
if (!messageWrapper) {
messageWrapper = document.createElement("div");
messageWrapper.className = "message-wrapper";
messageWrapper.id = wrapperId;
document.getElementById("chat").appendChild(messageWrapper);
}
// If the message-content div doesn't exist, create it
if (!messageWrapper.querySelector(".message-content")) {
// Create a message body wrapper to contain both header and content
const messageBodyWrapper = document.createElement("div");
messageBodyWrapper.className = "message-body";
messageWrapper.appendChild(messageBodyWrapper);
// Create header element for username/model
const headerElement = document.createElement("div");
headerElement.className = "message-header";
messageBodyWrapper.appendChild(headerElement);
// Create content element for actual message content
targetMessageElement = document.createElement("div");
targetMessageElement.className = "message-content";
messageBodyWrapper.appendChild(targetMessageElement);
} else {
targetMessageElement = messageWrapper.querySelector(".message-content");
}
// If the message buffer for this ID doesn't exist, create it
if (!messageBuffers[data.id]) {
messageBuffers[data.id] = "";
}
// Store header info on first chunk and update header element
if (data.is_first_chunk && data.username && data.model_name) {
messageHeaders[data.id] = {
username: data.username,
model_name: data.model_name
};
// Update header element
const headerElement = messageWrapper.querySelector(".message-header");
if (headerElement) {
const headerContent = `**${data.username} (${data.model_name}):**`;
headerElement.innerHTML = DOMPurify.sanitize(marked.marked(headerContent), dompurify_config);
}
}
// Append the chunk to the buffer
messageBuffers[data.id] += data.content;
// Process just the content and set it in the content element
const sanitizedContent = DOMPurify.sanitize(marked.marked(messageBuffers[data.id]), dompurify_config);
targetMessageElement.innerHTML = sanitizedContent;
// Store the raw markdown in a data attribute for later use in editing (without header for clean editing)
targetMessageElement.dataset.rawMarkdown = messageBuffers[data.id];
// Apply syntax highlighting to code blocks within the content
targetMessageElement.querySelectorAll("pre code").forEach((block) => {
addCopyButtonToCodeBlock(block);
hljs.highlightElement(block);
addLineNumbers(block);
});
// Scroll to the bottom of the chat container, but skip it if the user has scrolled up.
if (!userHasScrolledUp) {
document.getElementById("chat").scrollTop = document.getElementById("chat").scrollHeight;
}
// Check if the message is complete and add buttons if they haven't been added
if (data.is_complete && !messageWrapper.querySelector(".button-container")) {
// Create a container for the buttons
const buttonContainer = document.createElement("div");
buttonContainer.className = "button-container";
// Create the delete button
const deleteButton = document.createElement("button");
deleteButton.innerHTML = "x";
deleteButton.onclick = () => deleteMessage(data.id, room_name);
buttonContainer.appendChild(deleteButton);
// Create the edit button
const editButton = document.createElement("button");
editButton.textContent = "Edit";
editButton.className = "edit-button";
editButton.onclick = () => editMessage(data.id, targetMessageElement);
buttonContainer.appendChild(editButton);
// Create the copy button
const copyButton = document.createElement("button");
copyButton.textContent = "Copy";
copyButton.className = "copy-button";
copyButton.onclick = () => copyMessageContent(messageBuffers[data.id]);
buttonContainer.appendChild(copyButton);
// Create the play button for TTS
const playButton = document.createElement("button");
playButton.textContent = "Play";
playButton.onclick = () => {
// Use content from the content element (clean text without header)
const cleanText = targetMessageElement.textContent || targetMessageElement.innerText || "";
speakText(cleanText, playButton, data.id);
};
buttonContainer.appendChild(playButton);
// Insert the button container at the beginning of the message wrapper (before header and content)
messageWrapper.insertBefore(buttonContainer, messageWrapper.firstChild);
// Auto-play TTS if enabled and message is complete (only when streaming finishes)
console.log("DEBUG: Streaming complete check:", {
autoPlayTTS: autoPlayTTS,
is_complete: data.is_complete,
hasBuffer: !!messageBuffers[data.id],
bufferContent: messageBuffers[data.id] ? messageBuffers[data.id].substring(0, 50) + "..." : "none",
messageId: data.id
});
if (autoPlayTTS && data.is_complete && messageBuffers[data.id] && messageBuffers[data.id].trim() !== "") {
const playButton = Array.from(buttonContainer.querySelectorAll("button")).find(btn => btn.textContent === "Play");
console.log("DEBUG: Found play button for streaming TTS:", playButton ? "YES" : "NO", playButton?.textContent);
if (playButton) {
setTimeout(() => {
// Use content from the content element (clean text without header)
const cleanText = targetMessageElement.textContent || targetMessageElement.innerText || "";
console.log("DEBUG: Queueing streaming TTS:", data.id, cleanText.substring(0, 50) + "...");
queueTTS(cleanText, playButton, data.id);
}, 50); // Small delay to let the message render
}
}
}
});
// Socket event for when a message is deleted
socket.on("message_deleted", (data) => {
const messageElement = document.getElementById("message-" + data.message_id);
if (messageElement) {
messageElement.remove();
}
});
// Socket event for when a message is updated
socket.on("message_updated", (data) => {
// Find the existing message wrapper by ID
const messageWrapper = document.getElementById("message-" + data.message_id);
if (messageWrapper) {
// Find the specific element that contains the message content
const messageContentContainer = messageWrapper.querySelector(".message-content");
// Update the message content
if (data.content.startsWith('<img src="data:image/jpeg;base64')) {
// If it's a base64 image, set it directly
messageContentContainer.innerHTML = data.content;
} else {
// If it's not an image, sanitize and process the message with marked
messageContentContainer.innerHTML = DOMPurify.sanitize(marked.marked(data.content), dompurify_config);
}
// Update the raw markdown stored in the data attribute
messageContentContainer.dataset.rawMarkdown = data.content;
// Apply syntax highlighting and other functionalities to code blocks within the message
messageContentContainer.querySelectorAll("pre code").forEach((block) => {
addCopyButtonToCodeBlock(block);
truncateCodeBlock(block);
hljs.highlightElement(block);
addLineNumbers(block);
});
}
});
// Function to enter edit mode
function editMessage(messageId, messageContentContainer) {
// Store the current HTML in a data attribute
messageContentContainer.dataset.originalHtml = messageContentContainer.innerHTML;
const rawMarkdown = messageContentContainer.dataset.rawMarkdown;
// Create a textarea for editing
const textarea = document.createElement("textarea");
textarea.value = rawMarkdown;
textarea.rows = 16;
textarea.className = "message-edit";
// Replace the message content with the textarea
messageContentContainer.innerHTML = '';
messageContentContainer.appendChild(textarea);
// Find the message wrapper to access the edit and save buttons
const messageWrapper = messageContentContainer.closest('.message-wrapper');
// Create a save button with the 'save-button' class
const saveButton = document.createElement("button");
saveButton.textContent = "Save";
saveButton.className = "save-button"; // Add the class here
saveButton.onclick = () => saveEditedMessage(messageId, textarea, messageContentContainer);
// Change the edit button to a cancel button
const editButton = messageWrapper.querySelector(".edit-button");
editButton.textContent = "Cancel";
editButton.onclick = () => cancelEdit(messageId, messageContentContainer);
// Append the save button next to the cancel button
editButton.after(saveButton);
}
// Function to save the edited message
function saveEditedMessage(messageId, textarea, messageContentContainer) {
// Get the updated markdown from the textarea
const updatedMarkdown = textarea.value;
// Emit the update_message event to the server
socket.emit("update_message", {
"message_id": messageId,
"content": updatedMarkdown,
"room_name": room_name
});
// Clear the cached audio for this message to recompute TTS
if (audioCache[messageId]) {
delete audioCache[messageId];
}
// Reset the edit button to its original state
const messageWrapper = messageContentContainer.closest('.message-wrapper');
const editButton = messageWrapper.querySelector(".edit-button");
editButton.textContent = 'Edit';
editButton.onclick = () => editMessage(messageId, messageContentContainer, updatedMarkdown);
// Reset the play button to its initial state
const playButton = Array.from(messageWrapper.querySelectorAll("button")).find(btn => btn.textContent === 'Pause' || btn.textContent === 'Play');
if (playButton) {
playButton.textContent = 'Play';
playButton.onclick = () => speakText(updatedMarkdown, playButton, messageId); // Ensure it uses the updated content
}
// Remove the save button using the 'save-button' class
const saveButton = messageWrapper.querySelector(".save-button");
if (saveButton) {
saveButton.remove();
}
}
// Function to cancel the edit and revert changes
function cancelEdit(messageId, messageContentContainer) {
// Restore the original HTML of the message content from the data attribute
messageContentContainer.innerHTML = messageContentContainer.dataset.originalHtml;
// Reset the edit button to its original state
const messageWrapper = messageContentContainer.closest('.message-wrapper');
const editButton = messageWrapper.querySelector(".edit-button");
editButton.textContent = 'Edit';
editButton.onclick = () => editMessage(messageId, messageContentContainer, messageContentContainer.dataset.rawMarkdown);
// Remove the save button using the 'save-button' class
const saveButton = messageWrapper.querySelector(".save-button");
if (saveButton) {
saveButton.remove();
}
}
function truncateCodeBlock(block, maxLines = 100) {
// Split the content by new lines and check if it exceeds the maxLines
const lines = block.textContent.split('\n');
if (lines.length > maxLines) {
// Store the full content in a data attribute
block.dataset.fullContent = block.textContent;
// Truncate the displayed content
const truncatedText = lines.slice(0, maxLines).join('\n') + '\n...';
block.textContent = truncatedText;
// Create a container for bottom buttons
const bottomButtonContainer = document.createElement('div');
bottomButtonContainer.classList.add('code-block-bottom-buttons');
bottomButtonContainer.style.display = 'flex';
bottomButtonContainer.style.gap = '8px';
bottomButtonContainer.style.marginTop = '8px';
// Create the expand button
const expandButton = document.createElement('button');
expandButton.textContent = 'Show More';
expandButton.classList.add('show-more-button');
// Create bottom copy button
const bottomCopyButton = document.createElement('button');
bottomCopyButton.textContent = 'Copy';
bottomCopyButton.classList.add('copy-button');
bottomCopyButton.onclick = function() {
const contentToCopy = block.dataset.fullContent || block.textContent;
navigator.clipboard.writeText(contentToCopy).then(() => {
bottomCopyButton.textContent = 'Copied!';
setTimeout(() => {
bottomCopyButton.textContent = 'Copy';
}, 2000);
}).catch(err => {
console.error('Error copying text: ', err);
});
};
// Create bottom run button
const bottomPlayButton = document.createElement('button');
bottomPlayButton.textContent = '▶ Run';
bottomPlayButton.classList.add('play-button');
bottomPlayButton.onclick = function() {
const contentToRun = block.dataset.fullContent || block.textContent;
executeCodeBlock(contentToRun, block, bottomPlayButton);
};
expandButton.onclick = function() {
// Restore the full content from the data attribute
block.textContent = block.dataset.fullContent;
// Reapply syntax highlighting
hljs.highlightElement(block);
addLineNumbers(block);
// Change the button text to "Show Less"
expandButton.textContent = 'Show Less';
// Change the onclick function to truncate the block again
expandButton.onclick = function() {
block.textContent = truncatedText;
// Reapply syntax highlighting
hljs.highlightElement(block);
addLineNumbers(block);
// Change the button text back to "Show More"
expandButton.textContent = 'Show More';
// Set the onclick function back to the original expand function
expandButton.onclick = originalExpandFunction;
};
};
// Keep a reference to the original expand function
const originalExpandFunction = expandButton.onclick;
// Add all buttons to container
bottomButtonContainer.appendChild(expandButton);
bottomButtonContainer.appendChild(bottomCopyButton);
bottomButtonContainer.appendChild(bottomPlayButton);
// Insert the button container after the code block
block.parentNode.insertBefore(bottomButtonContainer, block.nextSibling);
}
}
// Modify the addCopyButtonToCodeBlock function to use the full content
function addCopyButtonToCodeBlock(block) {
// Check if the full content is stored in a data attribute, otherwise use textContent
const contentToCopy = block.dataset.fullContent || block.textContent;
// Create a container for the buttons
const buttonContainer = document.createElement('div');
buttonContainer.classList.add('code-block-button-container');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '8px';
buttonContainer.style.marginBottom = '8px';
// Create a button to copy the code block's content
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
copyButton.classList.add('copy-button'); // Add a class for styling if needed
copyButton.onclick = function() {
// Copy the content to the clipboard
navigator.clipboard.writeText(contentToCopy).then(() => {
// Optionally, indicate that the text was copied
copyButton.textContent = 'Copied!';
setTimeout(() => {
copyButton.textContent = 'Copy';
}, 2000); // Reset button text after 2 seconds
}).catch(err => {
console.error('Error copying text: ', err);
});
};
// Create a button to execute the code block's content
const playButton = document.createElement('button');
playButton.textContent = '▶ Run';
playButton.classList.add('play-button');
playButton.onclick = function() {
executeCodeBlock(contentToCopy, block, playButton);
};
// Add buttons to container
buttonContainer.appendChild(copyButton);
buttonContainer.appendChild(playButton);
// Insert the button container before the code block
block.parentNode.insertBefore(buttonContainer, block);
}
// Function to execute code block content
async function executeCodeBlock(code, blockElement, playButton) {
// Update button state
playButton.textContent = 'Running...';
playButton.disabled = true;
// Check if we already have a results container
let resultsContainer = blockElement.parentNode.querySelector('.code-execution-results');
if (!resultsContainer) {
// Create results container
resultsContainer = document.createElement('div');
resultsContainer.classList.add('code-execution-results');
resultsContainer.style.marginTop = '10px';
resultsContainer.style.padding = '10px';
resultsContainer.style.backgroundColor = 'var(--bg-code)';
resultsContainer.style.borderRadius = '5px';
resultsContainer.style.fontFamily = 'monospace';
resultsContainer.style.fontSize = '14px';
resultsContainer.style.whiteSpace = 'pre-wrap';
resultsContainer.style.wordWrap = 'break-word';
// Insert after the code block
blockElement.parentNode.insertBefore(resultsContainer, blockElement.nextSibling);
}
// Clear previous results
resultsContainer.innerHTML = '<div style="color: var(--text-muted);">Executing code...</div>';
try {
// Try to detect language from the code block's class
let language = null;
const classes = blockElement.className.split(' ');
for (const cls of classes) {
if (cls.startsWith('language-')) {
language = cls.replace('language-', '');
break;
}
}
// If no language specified (no class on code block), default to Python
if (!language) {
language = 'python';
}
// Use /execute/async endpoint with polling
const asyncResponse = await fetch(`${CODE_EXEC_URL}/execute/async`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
language: language,
code: code
})
});
if (!asyncResponse.ok) {
throw new Error(`HTTP error! status: ${asyncResponse.status}`);
}
const { job_id } = await asyncResponse.json();
// Poll for results: 300ms, 750ms, 1450ms, 2350ms, 3000ms, 4600ms, 6600ms+
const delays = [300, 450, 700, 900, 650, 1600, 2000];
let pollCount = 0;
let cancelButtonShown = false;
// Create cancel button (hidden initially)
let cancelButton = resultsContainer.querySelector('.cancel-execution-btn');
if (!cancelButton) {
cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.classList.add('cancel-execution-btn');
cancelButton.style.display = 'none';
cancelButton.style.marginTop = '8px';
cancelButton.style.padding = '4px 8px';
cancelButton.style.backgroundColor = 'var(--button-danger)';
cancelButton.style.color = 'white';
cancelButton.style.border = 'none';
cancelButton.style.borderRadius = '3px';
cancelButton.style.cursor = 'pointer';
cancelButton.onclick = async () => {
try {
await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`, { method: 'DELETE' });
cancelButton.disabled = true;
cancelButton.textContent = 'Cancelling...';
} catch (error) {
console.error('Error cancelling job:', error);
}
};
resultsContainer.appendChild(cancelButton);
}
while (true) {
await sleep(delays[Math.min(pollCount, delays.length - 1)]);
pollCount++;
const jobResponse = await fetch(`${CODE_EXEC_URL}/jobs/${job_id}`);
if (!jobResponse.ok) {
throw new Error(`Failed to fetch job status: ${jobResponse.status}`);
}
const job = await jobResponse.json();
if (job.status !== 'pending' && job.status !== 'running') {
// Job finished - hide cancel button
if (cancelButton) {
cancelButton.style.display = 'none';
}
if (job.status === 'completed') {
const result = job.result;
displayExecutionResults(result, resultsContainer, language);
break;
}
// timeout or cancelled
const errorMsg = job.result?.error || 'Execution failed';
const partialOutput = job.result?.partial_output;
let outputHtml = `<div style="color: var(--text-error); font-weight: bold;">${escapeHtml(errorMsg)}</div>`;
if (partialOutput) {
outputHtml += '<div style="color: var(--text-muted); margin-top: 8px;">Partial output before timeout:</div>';
outputHtml += `<div style="color: var(--text-primary); margin-left: 10px;">${escapeHtml(partialOutput)}</div>`;
}
resultsContainer.innerHTML = outputHtml;
break;
}
// Show cancel button after poll #5 (3000ms) if still running
if (!cancelButtonShown && pollCount === 5) {
cancelButtonShown = true;
cancelButton.style.display = 'inline-block';
}
}
} catch (error) {
console.error('Error executing code:', error);
resultsContainer.innerHTML = `<div style="color: var(--text-error);">Error: ${escapeHtml(error.message)}</div>`;
} finally {
// Reset button state
playButton.textContent = '▶ Run';
playButton.disabled = false;
}
}
// Helper function to sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Helper function to display execution results
function displayExecutionResults(result, resultsContainer, language) {
// Format and display results
let outputHtml = '';
// Handle nested response structure - check if stdout is an object with nested data
let actualStdout = result.stdout;
let actualStderr = result.stderr;
// If stdout is an object (nested response), extract the actual stdout/stderr
if (typeof result.stdout === 'object' && result.stdout !== null) {
actualStdout = result.stdout.stdout || '';
actualStderr = result.stdout.stderr || '';
}
// Show language
if (language) {
outputHtml += `<div style="color: var(--text-info); margin-bottom: 8px;">Language: ${language}</div>`;
}
// Show stdout
if (actualStdout) {
outputHtml += '<div style="color: var(--text-success); font-weight: bold;">Output:</div>';
outputHtml += `<div style="color: var(--text-primary); margin-left: 10px;">${escapeHtml(actualStdout)}</div>`;
}
// Show stderr if present
if (actualStderr) {
outputHtml += '<div style="color: var(--text-error); font-weight: bold; margin-top: 8px;">Errors/Warnings:</div>';
outputHtml += `<div style="color: var(--text-error); margin-left: 10px;">${escapeHtml(actualStderr)}</div>`;
}
// If no output at all
if (!actualStdout && !actualStderr) {
outputHtml += '<div style="color: var(--text-muted);">(No output produced)</div>';
}
resultsContainer.innerHTML = outputHtml;
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function addLineNumbers(block) {
const lines = block.textContent.split('\n').length - 1;
const lineNumbersWrapper = document.createElement('div');
lineNumbersWrapper.className = 'line-numbers-rows';
for (let i = 0; i < lines; i++) {
lineNumbersWrapper.appendChild(document.createElement('span'));
}
block.appendChild(lineNumbersWrapper);
}
// Socket event for setting the chat background
socket.on("set_background", (data) => {
// Use setTimeout to ensure background updates don't get blocked by TTS
setTimeout(() => {
const chat = document.getElementById("chat");
chat.style.backgroundImage = `url('data:image/png;base64,${data.image_data}')`;
chat.style.backgroundRepeat = "no-repeat";
chat.style.backgroundPosition = "right center";
chat.style.backgroundSize = "auto"; // Ensures the image is not stretched
console.log("Background image updated");
}, 0);
});
// Activity management functions
function refreshActivityList() {
fetch('/api/activities')
.then(response => response.json())
.then(data => {
const activitySelect = document.getElementById('activity-select');
const activitySelectMobile = document.getElementById('activity-select-mobile');
// Clear existing options except the first one for desktop
while (activitySelect.options.length > 1) {
activitySelect.remove(1);
}
// Clear existing options except the first one for mobile
while (activitySelectMobile.options.length > 1) {
activitySelectMobile.remove(1);
}
// Add activities to both dropdowns
data.activities.forEach(activity => {
const option = document.createElement('option');
option.value = activity;
option.textContent = activity;
activitySelect.appendChild(option);
const optionMobile = document.createElement('option');
optionMobile.value = activity;
optionMobile.textContent = activity;
activitySelectMobile.appendChild(optionMobile);
});
})
.catch(error => {
console.error('Error fetching activities:', error);
alert('Failed to fetch activities');
});
}
function loadSelectedActivity() {
const activitySelect = document.getElementById('activity-select');
const selectedActivity = activitySelect.value;
if (!selectedActivity) {
alert('Please select an activity');
return;
}
// Send command to load activity
socket.emit("chat_message", {
"username": username,
"message": `/activity ${selectedActivity}`,
"model": document.getElementById("model-select").value,
"room_name": room_name
});
}
function loadSelectedActivityMobile() {
const activitySelectMobile = document.getElementById('activity-select-mobile');
const selectedActivity = activitySelectMobile.value;
if (!selectedActivity) {
alert('Please select an activity');
return;
}
// Send command to load activity
socket.emit("chat_message", {
"username": username,
"message": `/activity ${selectedActivity}`,
"model": document.getElementById("model-select").value,
"room_name": room_name
});
}
function cancelActivity() {
if (confirm('Are you sure you want to cancel the current activity?')) {
socket.emit("chat_message", {
"username": username,
"message": "/activity cancel",
"model": document.getElementById("model-select").value,
"room_name": room_name
});
}
}
// Socket event for activity status updates
socket.on("activity_status", (data) => {
const currentActivityInfo = document.getElementById('current-activity-info');
const activityListSection = document.getElementById('activity-list-section');
const currentActivityName = document.getElementById('current-activity-name');
const currentActivityInfoMobile = document.getElementById('current-activity-info-mobile');
const activityListSectionMobile = document.getElementById('activity-list-section-mobile');
const currentActivityNameMobile = document.getElementById('current-activity-name-mobile');
if (data.active) {
currentActivityInfo.style.display = 'block';
activityListSection.style.display = 'none';
currentActivityName.textContent = data.activity_name || 'Unknown';
currentActivityInfoMobile.style.display = 'block';
activityListSectionMobile.style.display = 'none';
currentActivityNameMobile.textContent = data.activity_name || 'Unknown';
} else {
currentActivityInfo.style.display = 'none';
activityListSection.style.display = 'block';
currentActivityInfoMobile.style.display = 'none';
activityListSectionMobile.style.display = 'block';
}
});
// Load activities on page load
document.addEventListener('DOMContentLoaded', () => {
refreshActivityList();
// Request current activity status
socket.emit("get_activity_status", {"room_name": room_name});
});
</script>
{% endblock %}