opencompletion.com/templates/base.html
Claude 31152c295b
Refactor CSS from inline to external stylesheet
Move all CSS from base.html to static/css/style.css for better:
- Separation of concerns
- Browser caching
- Maintainability
- Code organization

Changes:
- Created static/css/style.css with all application styles
- Updated base.html to link to external stylesheet
- Reduced base.html from ~980 to ~407 lines
2025-11-08 18:41:56 +00:00

407 lines
17 KiB
HTML
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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Chatroom{% endblock %}</title>
<!-- Include highlight.js library for syntax highlighting -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<!-- Include highlight.js themes for light and dark modes -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github.min.css" id="highlight-theme-light">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/github-dark.min.css" id="highlight-theme-dark" disabled>
<!-- Include socket.io for real-time bidirectional event-based communication -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
<!-- Include marked.js for markdown parsing -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/marked/9.1.2/marked.min.js"
integrity="sha512-rfX4p3RNnxdwLT3wWP1K0NR3ztTobn+sISlT9WhxDDK00zNYbQ6MCHA5OHm0hqKAzEMXYCgFrp8iY/ER5MkXqA=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<!-- Include DOMPurify to sanitize HTML and prevent XSS attacks -->
<script src="https://cdn.jsdelivr.net/npm/dompurify@2/dist/purify.min.js"></script>
<!-- Include utility functions -->
<script src="{{ url_for('static', filename='js/utils.js') }}"></script>
<script>
// Connect to the server using socket.io
const socket = io.connect(window.location.protocol + "//" + document.domain + ":" + location.port);
</script>
<!-- Link to the favicon -->
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<!-- Link to external stylesheet -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body>
<!-- Search form -->
<div id="search-form" style="width: 90%;">
<form action="/search" method="get">
<input type="text" id="search-keywords" name="keywords" placeholder="Search for keywords..." value="{{ keywords }}">
<input type="hidden" id="username" name="username" value="{{ username }}">
</form>
</div>
<!-- Hamburger button for mobile -->
<button id="hamburger-button"></button>
<!-- Modal for room list and utility belt -->
<div id="room-list-modal">
<div id="room-list-modal-content">
<button id="close-modal-button" onclick="closeModal()">×</button>
<div id="utility-belt-mobile" class="utility-belt">
<div id="username-chooser-mobile">
<label for="username-input-mobile">Username:</label>
<input type="text" id="username-input-mobile" placeholder="guest" maxlength="50">
</div>
<div id="model-chooser-mobile">
<label for="model-select-mobile">Choose Model:</label>
<select id="model-select-mobile">
<option value="None">None</option>
</select>
</div>
<div id="voice-chooser-mobile">
<label for="voice-select-mobile">Choose Voice:</label>
<select id="voice-select-mobile">
<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 id="theme-toggle-mobile">
<button id="theme-toggle-btn-mobile" onclick="toggleTheme()">
🌙 Dark Mode
</button>
</div>
<div id="auto-play-tts-mobile">
<button id="auto-play-tts-btn-mobile" 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-mobile">
<h3>Activities</h3>
<div id="current-activity-info-mobile" style="display: none;">
<p>Current Activity: <span id="current-activity-name-mobile"></span></p>
<button id="cancel-activity-btn-mobile" onclick="cancelActivity()">Cancel Activity</button>
</div>
<div id="activity-list-section-mobile">
<div style="display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 5px; margin-bottom: 5px;">
<select id="activity-select-mobile" style="width: 100%; max-width: 100%; box-sizing: border-box;">
<option value="">-- Select an Activity --</option>
</select>
<button id="refresh-activities-btn-mobile" onclick="refreshActivityList()">🔄</button>
</div>
<button id="load-activity-btn-mobile" onclick="loadSelectedActivityMobile()" style="margin-top: 5px;">Load Activity</button>
</div>
</div>
<div id="user-lists-mobile">
<div id="active-users-list">
<h3>Active Users</h3>
<ul id="active-users-mobile">
<!-- Active users will be dynamically populated here -->
</ul>
</div>
<div id="inactive-users-list">
<h3>Inactive Users</h3>
<ul id="inactive-users-mobile">
<!-- Inactive users will be dynamically populated here -->
</ul>
</div>
</div>
</div>
<div id="rooms-list-modal-content">
<!-- Room list will be cloned here for mobile view -->
</div>
</div>
</div>
<!-- Chatroom list -->
<div class="main-container">
<div id="rooms-list">
<!-- opencompletion.com button -->
<div id="site-header">
<button id="opencompletion-btn" onclick="window.open('https://opencompletion.com', '_blank')">
opencompletion.com
</button>
</div>
<!-- New room creation section -->
<div id="new-room-section">
<textarea id="new-room-name" placeholder="Enter room name..." rows="2" maxlength="100"></textarea>
<button id="create-room-btn" onclick="createNewRoom()">Create Room</button>
</div>
<!-- Public rooms header -->
<div id="public-rooms-header">
<h4>Public Rooms</h4>
</div>
<ul id="rooms-list-ul">
<!-- Loop through rooms and create list items for each room -->
{% for room in rooms %}
<a href="{{ url_for('chat', room_name=room.name) }}?{{ request.query_string.decode('utf-8')|safe }}">
<li data-room-id="{{ room.id }}">
<!-- Display the room title if available, otherwise the room name -->
<b>{{ room.name }}</b>
{% if room.title %}
<br />{{ room.title }}
{% endif %}
{% if room.get_active_users()|length %}
<br /> {{ room.get_active_users()|length }} users
{% endif %}
</li>
</a>
{% endfor %}
</ul>
</div>
{% block content %}{% endblock %}
</div>
<script>
// Function to perform the search
function performSearch() {
const keywords = document.getElementById("search-keywords").value;
const username = document.getElementById("username").value;
if (!keywords) {
alert("Please enter keywords to search.");
return;
}
// Navigate to the search results page with the keywords as a query parameter
window.location.href = `/search?keywords=${encodeURIComponent(keywords)}&username=${username}`;
}
// Add event listener for keyword search the "Enter" key
document.getElementById("search-keywords").addEventListener("keydown", function(event) {
if (event.key === "Enter") {
event.preventDefault();
performSearch();
}
});
// Function to open the modal
function openModal() {
const modal = document.getElementById("room-list-modal");
const modalContent = document.getElementById("room-list-modal-content");
const closeButton = document.getElementById("close-modal-button");
// Clone the site header, new room section, public rooms header, and rooms list
const siteHeader = document.getElementById("site-header").cloneNode(true);
const newRoomSection = document.getElementById("new-room-section").cloneNode(true);
const publicRoomsHeader = document.getElementById("public-rooms-header").cloneNode(true);
const roomsList = document.getElementById("rooms-list-ul").cloneNode(true);
// Clear previous content and add all sections
document.getElementById("rooms-list-modal-content").innerHTML = '';
document.getElementById("rooms-list-modal-content").appendChild(siteHeader);
document.getElementById("rooms-list-modal-content").appendChild(newRoomSection);
document.getElementById("rooms-list-modal-content").appendChild(publicRoomsHeader);
document.getElementById("rooms-list-modal-content").appendChild(roomsList);
modal.style.display = "flex";
modalContent.style.display = "block";
closeButton.style.display = "block";
}
// Function to close the modal
function closeModal() {
const modal = document.getElementById("room-list-modal");
const modalContent = document.getElementById("room-list-modal-content");
const closeButton = document.getElementById("close-modal-button");
modal.style.display = "none";
modalContent.style.display = "none";
closeButton.style.display = "none";
}
// Function to update all room links with current URL parameters
function updateRoomLinksWithCurrentParams() {
const currentParams = new URLSearchParams(window.location.search).toString();
// Update desktop room links
const roomLinks = document.querySelectorAll('#rooms-list-ul a[href*="/chat/"]');
roomLinks.forEach(link => {
const url = new URL(link.href, window.location.origin);
const roomPath = url.pathname; // e.g., "/chat/room-name"
link.href = roomPath + (currentParams ? '?' + currentParams : '');
});
// Update mobile room links (if they exist)
const mobileRoomLinks = document.querySelectorAll('#rooms-list-modal-content a[href*="/chat/"]');
mobileRoomLinks.forEach(link => {
const url = new URL(link.href, window.location.origin);
const roomPath = url.pathname; // e.g., "/chat/room-name"
link.href = roomPath + (currentParams ? '?' + currentParams : '');
});
}
// Function to create a new room
function createNewRoom() {
const roomName = document.getElementById("new-room-name").value.trim();
if (!roomName) {
alert("Please enter a room name.");
return;
}
// Slugify the room name to make it URL-friendly
const slugifiedRoomName = slugify(roomName);
if (!slugifiedRoomName) {
alert("Please enter a valid room name with at least some letters or numbers.");
return;
}
// Get current URL parameters to maintain username, model, voice, etc.
const urlParams = new URLSearchParams(window.location.search);
// Also check localStorage for model and voice if not in URL
if (!urlParams.has('model')) {
const storedModel = localStorage.getItem('selectedModel');
if (storedModel && storedModel !== 'None') {
urlParams.set('model', storedModel);
}
}
if (!urlParams.has('voice')) {
const storedVoice = localStorage.getItem('selectedVoice');
if (storedVoice) {
urlParams.set('voice', storedVoice);
}
}
const currentParams = urlParams.toString();
// Navigate to the new room using the slugified name
window.location.href = `/chat/${slugifiedRoomName}${currentParams ? '?' + currentParams : ''}`;
}
// Add event listener for Enter key in the new room textarea
document.addEventListener('DOMContentLoaded', function() {
const newRoomTextarea = document.getElementById("new-room-name");
if (newRoomTextarea) {
newRoomTextarea.addEventListener("keydown", function(event) {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
createNewRoom();
}
});
}
});
// Add event listener to the hamburger button
document.getElementById("hamburger-button").addEventListener("click", openModal);
// Theme switching functionality
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
// Update the data-theme attribute
html.setAttribute('data-theme', newTheme);
// Save to localStorage
localStorage.setItem('theme', newTheme);
// Update button text for both desktop and mobile
updateThemeButtonText(newTheme);
// Switch highlight.js theme
updateHighlightTheme(newTheme);
}
function updateHighlightTheme(theme) {
const lightTheme = document.getElementById('highlight-theme-light');
const darkTheme = document.getElementById('highlight-theme-dark');
if (theme === 'dark') {
lightTheme.disabled = true;
darkTheme.disabled = false;
} else {
lightTheme.disabled = false;
darkTheme.disabled = true;
}
}
function updateThemeButtonText(theme) {
const themeBtn = document.getElementById('theme-toggle-btn');
const themeBtnMobile = document.getElementById('theme-toggle-btn-mobile');
if (theme === 'dark') {
if (themeBtn) themeBtn.textContent = '☀️ Light Mode';
if (themeBtnMobile) themeBtnMobile.textContent = '☀️ Light Mode';
} else {
if (themeBtn) themeBtn.textContent = '🌙 Dark Mode';
if (themeBtnMobile) themeBtnMobile.textContent = '🌙 Dark Mode';
}
}
// Apply saved theme on page load
function applySavedTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeButtonText(savedTheme);
updateHighlightTheme(savedTheme);
}
// Apply theme immediately (before DOMContentLoaded to prevent flash)
applySavedTheme();
// Populate the mobile model dropdown dynamically
document.addEventListener('DOMContentLoaded', (event) => {
const modelSelectMobile = document.getElementById("model-select-mobile");
// Ensure theme button text is correct on page load
const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
updateThemeButtonText(currentTheme);
// Function to populate the dropdown
function populateModelDropdown(models) {
while (modelSelectMobile.options.length > 1) {
modelSelectMobile.remove(1);
}
models.forEach(modelId => {
const option = document.createElement('option');
option.value = modelId;
option.textContent = modelId;
modelSelectMobile.appendChild(option);
});
}
// 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)) {
const models = JSON.parse(cachedData);
populateModelDropdown(models);
} else {
fetch('/models')
.then(response => response.json())
.then(data => {
const models = data.models;
populateModelDropdown(models);
localStorage.setItem(cacheKey, JSON.stringify(models));
localStorage.setItem(cacheExpirationKey, Date.now() + cacheDuration);
})
.catch(error => console.error("Error fetching models:", error));
}
});
</script>
</body>
</html>