opencompletion.com/templates/base.html
Claude 314651e910
Add dark/light mode theme switcher with localStorage persistence
- Added CSS variables for light and dark themes
- Implemented theme toggle buttons in both desktop sidebar and mobile menu
- Added JavaScript logic to switch themes and persist choice in localStorage
- Applied dark theme styling to all UI elements including code blocks
- Theme is applied immediately on page load to prevent flash
2025-11-08 15:39:23 +00:00

884 lines
31 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 a highlight.js theme (replace 'default' with your preferred theme) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/styles/default.min.css">
<!-- 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') }}">
<style>
/* CSS Variables for Light and Dark Themes */
:root {
--bg-primary: #f7f7f7;
--bg-secondary: #ffffff;
--bg-tertiary: #f8f9fa;
--bg-code: #f0f0f0;
--text-primary: #000000;
--text-secondary: #495057;
--text-muted: #666;
--border-color: #e1e1e1;
--border-color-dark: #ced4da;
--border-code: #ccc;
--button-primary: #007bff;
--button-primary-hover: #0056b3;
--button-success: #28a745;
--button-success-hover: #218838;
--button-activity: #4CAF50;
--button-danger: #f44336;
--shadow: rgba(0, 0, 0, 0.1);
--shadow-dark: rgba(0, 0, 0, 0.2);
--highlight-bg: #f8f9fa;
--code-line-numbers: #999;
--modal-overlay: rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--bg-tertiary: #252525;
--bg-code: #1e1e1e;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #888;
--border-color: #404040;
--border-color-dark: #4a4a4a;
--border-code: #555;
--button-primary: #0d6efd;
--button-primary-hover: #0b5ed7;
--button-success: #198754;
--button-success-hover: #157347;
--button-activity: #4CAF50;
--button-danger: #dc3545;
--shadow: rgba(0, 0, 0, 0.3);
--shadow-dark: rgba(0, 0, 0, 0.5);
--highlight-bg: #2a2a2a;
--code-line-numbers: #666;
--modal-overlay: rgba(0, 0, 0, 0.7);
}
/* Dark theme overrides for code blocks */
[data-theme="dark"] .hljs {
color: #e0e0e0;
}
[data-theme="dark"] pre {
background-color: var(--bg-code);
color: var(--text-primary);
}
[data-theme="dark"] code {
background-color: var(--bg-code);
color: var(--text-primary);
}
/* Basic styling for the chat application */
html, body {
height: 100%;
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
background-color: var(--bg-primary);
color: var(--text-primary);
display: grid;
place-items: center;
overflow: hidden; /* Prevent scrolling of the main viewport */
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Styling for the chat container */
#chat-container {
display: grid;
grid-template-rows: 1fr auto;
width: 100%;
height: 90vh;
background-color: var(--bg-secondary);
border-radius: 5px;
padding: 15px;
box-shadow: 0 2px 6px var(--shadow);
box-sizing: border-box;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
/* Styling for the chat area */
#chat {
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 5px;
padding-left: 10px;
margin-bottom: 10px;
width: 100%; /* Allow chat window to fill available space */
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Styling for the message input area */
#message, .message-edit {
width: 100%;
border: 1px solid var(--border-color);
border-radius: 5px;
padding: 5px;
display: block;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Styling for the message body wrapper that contains header and content */
.message-body {
width: 100%;
display: flex;
flex-direction: column;
}
/* Styling for the message header (username/model) */
.message-header {
width: 100%;
margin-bottom: 0;
}
/* Styling for the message div holding html/markdown content */
.message-content {
width: 100%;
}
/* Styling for individual message wrappers */
.message-wrapper {
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 4px;
margin-bottom: 32px;
}
/* Styling for the delete and edit buttons next to messages */
.message-wrapper button {
margin-right: 4px;
margin-bottom: 4px;
}
/* Styling for the button container within each message */
.button-container {
display: grid;
grid-auto-rows: min-content; /* Ensure each button takes up only as much space as it needs */
gap: 4px; /* Vertical space between buttons */
}
/* Styling for paragraphs, used for messages */
p {
margin: 0;
margin-bottom: 12px;
}
/* Styling for the main container that holds the rooms list and chat */
.main-container {
display: grid;
grid-template-columns: 15% 70% 15%;
width: 100%;
height: 90vh;
}
/* Styling for the rooms list */
#rooms-list {
border-right: 1px solid var(--border-color);
overflow-y: auto;
padding: 10px;
background-color: var(--bg-secondary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Styling for site header */
#site-header {
margin-bottom: 20px;
text-align: center;
}
#opencompletion-btn {
width: 100%;
padding: 10px;
background-color: var(--button-primary);
color: white;
border: none;
border-radius: 5px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s;
}
#opencompletion-btn:hover {
background-color: var(--button-primary-hover);
}
/* Styling for new room creation section */
#new-room-section {
margin-bottom: 20px;
padding: 10px;
border: 1px solid var(--border-color);
border-radius: 5px;
background-color: var(--bg-tertiary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
#new-room-section h4 {
margin: 0 0 10px 0;
font-size: 14px;
color: var(--text-secondary);
transition: color 0.3s ease;
}
#new-room-name {
width: 100%;
padding: 8px;
border: 1px solid var(--border-color-dark);
border-radius: 3px;
font-size: 12px;
resize: vertical;
margin-bottom: 10px;
box-sizing: border-box;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
#create-room-btn {
width: 100%;
padding: 8px;
background-color: var(--button-success);
color: white;
border: none;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
}
#create-room-btn:hover {
background-color: var(--button-success-hover);
}
/* Styling for public rooms header */
#public-rooms-header {
margin-bottom: 10px;
}
#public-rooms-header h4 {
margin: 0;
font-size: 14px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
padding-bottom: 5px;
transition: color 0.3s ease, border-color 0.3s ease;
}
/* Styling for the unordered list in the rooms list */
#rooms-list ul, #rooms-list-modal-content ul {
list-style: none; /* Removes default list styling */
padding: 0; /* Resets default padding */
margin: 0; /* Resets default margin */
}
/* Styling for list items in the rooms list */
#rooms-list li {
margin-bottom: 10px; /* Adds space between items */
padding: 5px; /* Adds padding inside each item */
border: 1px solid var(--border-color); /* Adds a border around each item */
border-radius: 5px; /* Optional: Rounds the corners of the border */
background-color: var(--bg-secondary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Styling for links in the rooms list */
#rooms-list a {
text-decoration: none; /* Optional: Removes underline from links */
color: var(--text-primary); /* Ensures link color matches the text color */
transition: color 0.3s ease;
}
.hljs {
position: relative;
padding-left: 46px !important;
counter-reset: line;
background-color: var(--bg-code) !important;
transition: background-color 0.3s ease;
}
.hljs .line-numbers-rows {
position: absolute;
top: 0;
left: 0;
width: 3em; /* Adjust the width as needed */
letter-spacing: -1px;
border-right: 1px solid var(--border-code); /* Optional: adds a line to separate numbers */
text-align: right;
margin-top: 14px; /* Align with the code block */
color: var(--code-line-numbers);
pointer-events: none;
transition: border-color 0.3s ease, color 0.3s ease;
}
.hljs .line-numbers-rows span {
display: block;
counter-increment: line;
}
.hljs .line-numbers-rows span::before {
content: counter(line);
display: block;
padding-right: 0.8em; /* Adjust the padding as needed */
}
.download-links {
text-align: center;
}
/* Hamburger button styling */
#hamburger-button {
display: none; /* Hidden by default */
position: fixed;
top: 20px;
left: 20px;
background-color: #333;
color: white;
border: none;
border-radius: 5px;
padding: 10px;
cursor: pointer;
z-index: 1001;
}
/* Modal styling */
#room-list-modal {
display: none; /* Hidden by default */
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--modal-overlay);
z-index: 1002;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
}
#room-list-modal-content {
display: none; /* Hidden by default */
background-color: var(--bg-secondary);
padding: 20px;
border-radius: 5px;
width: 80%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto; /* Make the room list scrollable */
position: relative;
transition: background-color 0.3s ease;
}
/* Close button styling */
#close-modal-button {
display: none; /* Hidden by default */
position: fixed;
top: 10px;
right: 10px;
background-color: #333;
color: white;
border: none;
border-radius: 5px;
font-size: 20px;
cursor: pointer;
z-index: 1003; /* Ensure it is above the modal content */
padding: 5px 10px; /* Add padding for a button-like appearance */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); /* Add a shadow for depth */
transition: background-color 0.3s; /* Smooth transition for hover effect */
}
#close-modal-button:hover {
background-color: #555; /* Darken on hover */
}
.utility-belt {
padding: 10px;
background-color: var(--bg-secondary);
transition: background-color 0.3s ease;
}
/* Activity controls styling */
#activity-controls {
margin-top: 20px;
padding: 10px;
border-top: 1px solid var(--border-color);
transition: border-color 0.3s ease;
}
#activity-controls h3 {
margin-top: 0;
margin-bottom: 10px;
color: var(--text-primary);
transition: color 0.3s ease;
}
#current-activity-info {
background-color: var(--bg-code);
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
transition: background-color 0.3s ease;
}
#current-activity-info p {
margin: 0 0 10px 0;
color: var(--text-primary);
transition: color 0.3s ease;
}
#activity-controls button {
background-color: var(--button-activity);
color: white;
border: none;
padding: 8px 16px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px;
margin: 4px 2px;
cursor: pointer;
border-radius: 4px;
transition: opacity 0.3s ease;
}
#cancel-activity-btn {
background-color: var(--button-danger);
}
#activity-controls button:hover {
opacity: 0.8;
}
#activity-select {
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Model and voice select dropdowns styling */
#model-select, #voice-select, #model-select-mobile, #voice-select-mobile {
width: 100%;
max-width: 100%;
box-sizing: border-box;
padding: 5px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: var(--bg-secondary);
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease;
}
/* Media query for mobile devices */
@media (max-width: 768px) {
.main-container {
grid-template-columns: 1fr; /* Single column layout */
}
#rooms-list {
display: none; /* Hide the room list on mobile */
}
#hamburger-button {
display: block; /* Show the hamburger button on mobile */
}
}
</style>
</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..." style="width: 100%; text-align: center; background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); padding: 8px; border-radius: 5px;" 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">
<div id="username-chooser-mobile">
<label for="username-input-mobile" style="color: var(--text-primary);">Username:</label>
<input type="text" id="username-input-mobile" placeholder="guest" maxlength="50" style="width: 100%; padding: 4px; margin-top: 2px; border: 1px solid var(--border-color); border-radius: 3px; background-color: var(--bg-secondary); color: var(--text-primary);">
</div>
<div id="model-chooser-mobile">
<label for="model-select-mobile" style="color: var(--text-primary);">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" style="color: var(--text-primary);">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()" style="width: 100%; margin-top: 10px; background-color: #6c757d; color: white; border: none; padding: 8px; border-radius: 4px; cursor: pointer;">
🌙 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 style="color: var(--text-primary);">Activities</h3>
<div id="current-activity-info-mobile" style="display: none;">
<p style="color: var(--text-primary);">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 style="color: var(--text-primary);">Active Users</h3>
<ul id="active-users-mobile">
<!-- Active users will be dynamically populated here -->
</ul>
</div>
<div id="inactive-users-list">
<h3 style="color: var(--text-primary);">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);
}
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);
}
// 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>