Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AI Chat Interface</title> | |
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
<style> | |
:root { | |
--primary-color: #2563eb; | |
--secondary-color: #3b82f6; | |
--background-dark: #111827; | |
--sidebar-dark: #1f2937; | |
--chat-area-dark: #1a1f2b; | |
} | |
/* Custom scrollbar */ | |
::-webkit-scrollbar { | |
width: 6px; | |
} | |
::-webkit-scrollbar-track { | |
background: var(--background-dark); | |
} | |
::-webkit-scrollbar-thumb { | |
background-color: #4b5563; | |
border-radius: 3px; | |
} | |
::-webkit-scrollbar-thumb:hover { | |
background-color: #6b7280; | |
} | |
/* Chat styles */ | |
.chat-message { | |
max-width: 85%; | |
word-wrap: break-word; | |
animation: fadeIn 0.3s ease-in-out; | |
} | |
.user-message { | |
background-color: var(--primary-color); | |
color: white; | |
margin-left: auto; | |
} | |
.assistant-message { | |
background-color: var(--chat-area-dark); | |
color: #e5e7eb; | |
margin-right: auto; | |
border: 1px solid #374151; | |
} | |
/* Code blocks */ | |
.markdown-content pre { | |
background-color: #1a1f2b; | |
padding: 1rem; | |
border-radius: 0.5rem; | |
overflow-x: auto; | |
margin: 1rem 0; | |
border: 1px solid #374151; | |
} | |
.markdown-content code { | |
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | |
font-size: 0.9em; | |
padding: 0.2rem 0.4rem; | |
border-radius: 0.25rem; | |
background-color: rgba(45, 55, 72, 0.5); | |
} | |
/* Context Menu Styles */ | |
.context-menu { | |
position: fixed; | |
background: var(--sidebar-dark); | |
border: 1px solid #374151; | |
border-radius: 0.5rem; | |
padding: 0.5rem 0; | |
min-width: 160px; | |
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); | |
z-index: 1000; | |
display: none; | |
} | |
.context-menu-item { | |
padding: 0.5rem 1rem; | |
cursor: pointer; | |
display: flex; | |
align-items: center; | |
transition: background-color 0.2s; | |
} | |
.context-menu-item:hover { | |
background-color: var(--primary-color); | |
} | |
.context-menu-item i { | |
margin-right: 0.5rem; | |
width: 20px; | |
} | |
/* Edit Modal Styles */ | |
.modal { | |
display: none; | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background-color: rgba(0, 0, 0, 0.5); | |
z-index: 1001; | |
} | |
.modal-content { | |
position: relative; | |
background-color: var(--sidebar-dark); | |
margin: 0; | |
padding: 1.5rem; | |
border-radius: 0; | |
width: 100%; | |
height: 100%; | |
display: flex; | |
flex-direction: column; | |
} | |
.close-modal { | |
position: absolute; | |
right: 1rem; | |
top: 1rem; | |
cursor: pointer; | |
font-size: 1.5rem; | |
color: #9ca3af; | |
transition: color 0.2s; | |
} | |
.close-modal:hover { | |
color: #f3f4f6; | |
} | |
.modal-textarea { | |
flex-grow: 1; | |
margin-bottom: 1rem; | |
} | |
/* Animations */ | |
@keyframes fadeIn { | |
from { opacity: 0; transform: translateY(10px); } | |
to { opacity: 1; transform: translateY(0); } | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.1); } | |
100% { transform: scale(1); } | |
} | |
.sidebar-collapsed { | |
width: 4rem ; | |
} | |
.sidebar-expanded { | |
width: 18rem; | |
} | |
.transition-width { | |
transition: width 0.3s ease-in-out; | |
} | |
/* Glass effect */ | |
.glass-effect { | |
background: rgba(31, 41, 55, 0.7); | |
backdrop-filter: blur(10px); | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
/* Button effects */ | |
.hover-shadow { | |
transition: all 0.3s ease; | |
} | |
.hover-shadow:hover { | |
transform: translateY(-1px); | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); | |
} | |
/* Input focus effects */ | |
.input-focus { | |
transition: all 0.3s ease; | |
border: 1px solid rgba(255, 255, 255, 0.1); | |
} | |
.input-focus:focus { | |
border-color: var(--primary-color); | |
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2); | |
outline: none; | |
} | |
/* Responsive design */ | |
@media (max-width: 768px) { | |
.chat-message { | |
max-width: 95%; | |
} | |
} | |
/* Hide sidebar content when collapsed */ | |
.sidebar-collapsed .sidebar-content { | |
display: none; | |
} | |
.sidebar-collapsed .sidebar-icon { | |
display: block; | |
} | |
/* Disabled UI styles */ | |
.disabled { | |
pointer-events: none; | |
opacity: 0.6; | |
} | |
/* Stop button styles */ | |
.stop-button { | |
display: none; | |
animation: pulse 1s infinite; | |
} | |
</style> | |
</head> | |
<body class="bg-gradient-to-br from-gray-900 to-gray-800 text-gray-100 min-h-screen"> | |
<!-- Context Menu --> | |
<div id="contextMenu" class="context-menu"> | |
<div class="context-menu-item" id="editMenuItem"> | |
<i class="fas fa-edit"></i> | |
<span>Edit</span> | |
</div> | |
</div> | |
<!-- Edit Modal --> | |
<div id="editModal" class="modal"> | |
<div class="modal-content"> | |
<span class="close-modal">×</span> | |
<h2 class="text-xl font-bold mb-4">Edit Message</h2> | |
<textarea id="editMessageInput" class="modal-textarea w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm"></textarea> | |
<div class="flex justify-end space-x-2"> | |
<button id="cancelEditBtn" class="bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg transition">Cancel</button> | |
<button id="saveEditBtn" class="bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg transition">Save</button> | |
</div> | |
</div> | |
</div> | |
<div class="flex h-screen"> | |
<!-- Collapsible Sidebar --> | |
<div id="sidebar" class="transition-width sidebar-expanded bg-gray-800 glass-effect flex flex-col h-full"> | |
<div class="p-4 flex items-center justify-between sidebar-icon"> | |
<button id="toggleSidebar" class="text-gray-400 hover:text-white"> | |
<i class="fas fa-bars text-xl"></i> | |
</button> | |
<span id="sidebarTitle" class="font-semibold text-lg ml-2 sidebar-content">AI Chat</span> | |
</div> | |
<button id="newChatBtn" class="flex items-center m-4 bg-gradient-to-r from-blue-600 to-blue-500 text-white px-4 py-2 rounded-lg hover-shadow sidebar-content"> | |
<i class="fas fa-plus mr-2"></i> | |
<span>New Chat</span> | |
</button> | |
<div class="p-4 sidebar-content"> | |
<div class="space-y-3"> | |
<input type="text" id="baseHost" placeholder="Ollama Base Host" value="http://localhost:11434" | |
class="w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm"> | |
<select id="ollamaModel" class="w-full bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm"> | |
<option value="">Select Model</option> | |
</select> | |
<button id="listModelsBtn" class="w-full bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg text-sm transition"> | |
<i class="fas fa-sync-alt mr-2"></i>List Models | |
</button> | |
</div> | |
</div> | |
<div class="flex-grow overflow-y-auto px-4 sidebar-content"> | |
<h3 class="text-sm font-semibold text-gray-400 mb-2">Saved Chats</h3> | |
<div id="chatsList" class="space-y-2"> | |
{% for chat in chat_files %} | |
<div class="chat-file hover:bg-gray-700 p-2 rounded-lg cursor-pointer transition" data-file="{{ chat }}"> | |
<i class="fas fa-comment-alt mr-2"></i>{{ chat }} | |
</div> | |
{% endfor %} | |
</div> | |
</div> | |
</div> | |
<!-- Main Content --> | |
<div class="flex-grow flex flex-col bg-gray-900"> | |
<!-- Chat Area --> | |
<div id="chatArea" class="flex-grow overflow-y-auto p-4 space-y-4"> | |
<!-- Messages will be inserted here --> | |
</div> | |
<!-- Input Area --> | |
<div class="border-t border-gray-700 p-4 space-y-4"> | |
<div class="flex space-x-2"> | |
<textarea id="messageInput" rows="3" placeholder="Type your message here..." | |
class="flex-grow bg-gray-800 input-focus rounded-lg px-4 py-2 resize-none"></textarea> | |
<div class="flex flex-col space-y-2"> | |
<button id="sendBtn" class="bg-gradient-to-r from-blue-600 to-blue-500 px-6 py-2 rounded-lg hover-shadow"> | |
<i class="fas fa-paper-plane mr-2"></i>Send | |
</button> | |
<button id="stopBtn" class="stop-button bg-red-600 hover:bg-red-700 px-6 py-2 rounded-lg transition"> | |
<i class="fas fa-stop mr-2"></i>Stop | |
</button> | |
</div> | |
</div> | |
<!-- TTS Controls --> | |
<div class="flex items-center space-x-4 bg-gray-800 p-4 rounded-lg glass-effect"> | |
<select id="ttsModel" class="bg-gray-700 input-focus rounded-lg px-3 py-2 text-sm"> | |
<option value="">Select TTS Model</option> | |
{% for model in tts_models %} | |
<option value="{{ model }}">{{ model }}</option> | |
{% endfor %} | |
</select> | |
<button id="playBtn" class="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg transition"> | |
<i class="fas fa-play mr-2"></i>Play | |
</button> | |
<button id="pauseBtn" class="bg-yellow-600 hover:bg-yellow-700 px-4 py-2 rounded-lg transition"> | |
<i class="fas fa-pause mr-2"></i>Pause | |
</button> | |
<div class="flex items-center space-x-2"> | |
<i class="fas fa-volume-up text-gray-400"></i> | |
<input type="range" id="volumeSlider" min="0" max="100" value="100" | |
class="w-24 accent-blue-500"> | |
</div> | |
<label class="flex items-center space-x-2 text-sm"> | |
<input type="checkbox" id="removeMarkdown" class="form-checkbox text-blue-500"> | |
<span>Remove Markdown</span> | |
</label> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let currentChatFile = null; | |
let conversationHistory = []; | |
let currentAudio = null; | |
let currentEventSource = null; | |
let lastAssistantMessage = null; | |
let abortController = null; | |
let selectedOllamaModel = ''; | |
let selectedTtsModel = ''; | |
let editingMessageIndex = null; | |
function escapeHtml(html) { | |
const div = document.createElement('div'); | |
div.textContent = html; | |
return div.innerHTML; | |
} | |
function renderMarkdown(text) { | |
let html = text | |
.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code class="language-$1">$2</code></pre>') | |
.replace(/`([^`]+)`/g, '<code>$1</code>') | |
.replace(/^### (.*$)/gm, '<h3>$1</h3>') | |
.replace(/^## (.*$)/gm, '<h2>$1</h2>') | |
.replace(/^# (.*$)/gm, '<h1>$1</h1>') | |
.replace(/^\* (.*$)/gm, '<li>$1</li>') | |
.replace(/^\d\. (.*$)/gm, '<li>$1</li>') | |
.replace(/\n\n/g, '</p><p>') | |
.replace(/\n/g, '<br>'); | |
return `<p>${html}</p>`; | |
} | |
function addMessage(text, isUser = true, messageIndex = null) { | |
const messageDiv = document.createElement('div'); | |
messageDiv.className = `chat-message ${isUser ? 'user-message' : 'assistant-message'} p-4 rounded-lg`; | |
messageDiv.dataset.index = messageIndex !== null ? messageIndex : conversationHistory.length; | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'markdown-content'; | |
contentDiv.innerHTML = renderMarkdown(text); | |
messageDiv.appendChild(contentDiv); | |
document.getElementById('chatArea').appendChild(messageDiv); | |
scrollToBottom(); | |
// Add context menu event listener | |
messageDiv.addEventListener('contextmenu', showContextMenu); | |
return messageDiv; | |
} | |
function scrollToBottom() { | |
const chatArea = document.getElementById('chatArea'); | |
chatArea.scrollTop = chatArea.scrollHeight; | |
} | |
function loadChat(chatFile) { | |
fetch(`/api/load_chat/${chatFile}`) | |
.then(response => response.json()) | |
.then(data => { | |
currentChatFile = chatFile; | |
conversationHistory = data.messages; | |
document.getElementById('chatArea').innerHTML = ''; | |
conversationHistory.forEach((message, index) => { | |
addMessage(message.content, message.role === 'user', index); | |
}); | |
}); | |
} | |
function showContextMenu(e) { | |
e.preventDefault(); | |
const contextMenu = document.getElementById('contextMenu'); | |
const messageDiv = e.currentTarget; | |
// Store the message index for editing | |
editingMessageIndex = parseInt(messageDiv.dataset.index); | |
// Position the context menu | |
contextMenu.style.left = `${e.pageX}px`; | |
contextMenu.style.top = `${e.pageY}px`; | |
contextMenu.style.display = 'block'; | |
} | |
function hideContextMenu() { | |
document.getElementById('contextMenu').style.display = 'none'; | |
} | |
function showEditModal(content) { | |
const modal = document.getElementById('editModal'); | |
const input = document.getElementById('editMessageInput'); | |
input.value = content; | |
modal.style.display = 'block'; | |
} | |
function hideEditModal() { | |
document.getElementById('editModal').style.display = 'none'; | |
editingMessageIndex = null; | |
} | |
async function saveEditedMessage() { | |
const newContent = document.getElementById('editMessageInput').value; | |
const isUserMessage = conversationHistory[editingMessageIndex].role === 'user'; | |
try { | |
const response = await fetch('/api/update_message', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
chat_file: currentChatFile, | |
message_index: editingMessageIndex, | |
content: newContent, | |
is_user: isUserMessage | |
}) | |
}); | |
const data = await response.json(); | |
if (data.success) { | |
// Update conversation history and UI | |
conversationHistory = data.messages; | |
document.getElementById('chatArea').innerHTML = ''; | |
conversationHistory.forEach((message, index) => { | |
addMessage(message.content, message.role === 'user', index); | |
}); | |
// If it was a user message, regenerate the response | |
if (isUserMessage) { | |
await sendMessage(false); | |
} | |
} | |
} catch (error) { | |
console.error('Error saving edited message:', error); | |
alert('Failed to save the edited message'); | |
} | |
hideEditModal(); | |
} | |
function disableUI() { | |
document.getElementById('sendBtn').disabled = true; | |
document.getElementById('newChatBtn').disabled = true; | |
document.getElementById('listModelsBtn').disabled = true; | |
document.getElementById('messageInput').disabled = true; | |
document.getElementById('sidebar').classList.add('disabled'); | |
document.getElementById('chatsList').classList.add('disabled'); | |
document.getElementById('stopBtn').style.display = 'block'; | |
} | |
function enableUI() { | |
document.getElementById('sendBtn').disabled = false; | |
document.getElementById('newChatBtn').disabled = false; | |
document.getElementById('listModelsBtn').disabled = false; | |
document.getElementById('messageInput').disabled = false; | |
document.getElementById('sidebar').classList.remove('disabled'); | |
document.getElementById('chatsList').classList.remove('disabled'); | |
document.getElementById('stopBtn').style.display = 'none'; | |
} | |
async function sendMessage(addUserMessage = true) { | |
const messageInput = document.getElementById('messageInput'); | |
const text = messageInput.value.trim(); | |
if (!text && addUserMessage) return; | |
const model = document.getElementById('ollamaModel').value; | |
if (!model) { | |
alert('Please select a model first'); | |
return; | |
} | |
if (addUserMessage) { | |
// Add user message | |
addMessage(text, true); | |
messageInput.value = ''; | |
// Add to conversation history | |
conversationHistory.push({ role: 'user', content: text }); | |
} | |
// Stop any existing EventSource | |
if (currentEventSource) { | |
currentEventSource.close(); | |
} | |
const baseHost = document.getElementById('baseHost').value; | |
// Create a new AbortController | |
abortController = new AbortController(); | |
// Disable UI | |
disableUI(); | |
try { | |
const response = await fetch('/api/chat', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ | |
base_host: baseHost, | |
model: model, | |
messages: conversationHistory, | |
chat_file: currentChatFile || '' | |
}), | |
signal: abortController.signal | |
}); | |
if (!response.ok) { | |
throw new Error(`HTTP error! status: ${response.status}`); | |
} | |
const reader = response.body.getReader(); | |
const decoder = new TextDecoder(); | |
let assistantMessage = addMessage('', false); | |
let completeResponse = ''; | |
while (true) { | |
const { value, done } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value); | |
const lines = chunk.split('\n'); | |
for (const line of lines) { | |
if (line.startsWith('data: ')) { | |
try { | |
const data = JSON.parse(line.slice(6)); | |
if (data.error) { | |
alert(data.error); | |
break; | |
} else if (data.chunk) { | |
completeResponse = data.chunk; | |
assistantMessage.querySelector('.markdown-content').innerHTML = renderMarkdown(completeResponse); | |
scrollToBottom(); | |
} else if (data.done) { | |
lastAssistantMessage = completeResponse; | |
conversationHistory.push({ role: 'assistant', content: completeResponse }); | |
// Convert to speech | |
const ttsModel = document.getElementById('ttsModel').value; | |
if (ttsModel) { | |
convertToSpeech(completeResponse, ttsModel); | |
} | |
} | |
} catch (e) { | |
console.error('Error parsing SSE data:', e); | |
} | |
} | |
} | |
} | |
} catch (error) { | |
if (error.name === 'AbortError') { | |
console.log('Fetch aborted'); | |
} else { | |
console.error('Error:', error); | |
alert('Error sending message: ' + error.message); | |
} | |
} finally { | |
// Enable UI | |
enableUI(); | |
} | |
} | |
function convertToSpeech(text, model) { | |
const removeMarkdown = document.getElementById('removeMarkdown').checked; | |
fetch('/api/tts', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/json' }, | |
body: JSON.stringify({ text, model, remove_markdown: removeMarkdown }) | |
}) | |
.then(response => response.json()) | |
.then(data => { | |
if (data.error) { | |
alert(data.error); | |
} else { | |
if (currentAudio) { | |
currentAudio.pause(); | |
} | |
currentAudio = new Audio(`/audio/${data.audio_file}`); | |
currentAudio.volume = document.getElementById('volumeSlider').value / 100; | |
currentAudio.play(); | |
} | |
}); | |
} | |
// Event Listeners | |
document.getElementById('sendBtn').addEventListener('click', () => sendMessage(true)); | |
document.getElementById('messageInput').addEventListener('keypress', (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(true); | |
} | |
}); | |
document.getElementById('stopBtn').addEventListener('click', () => { | |
if (abortController) { | |
abortController.abort(); | |
} | |
}); | |
document.getElementById('newChatBtn').addEventListener('click', () => { | |
currentChatFile = null; | |
conversationHistory = []; | |
document.getElementById('chatArea').innerHTML = ''; | |
// Restore selected models | |
document.getElementById('ollamaModel').value = selectedOllamaModel; | |
document.getElementById('ttsModel').value = selectedTtsModel; | |
}); | |
document.getElementById('listModelsBtn').addEventListener('click', () => { | |
const baseHost = document.getElementById('baseHost').value; | |
fetch(`/api/list_ollama_models?base_host=${encodeURIComponent(baseHost)}`) | |
.then(response => response.json()) | |
.then(data => { | |
const select = document.getElementById('ollamaModel'); | |
select.innerHTML = '<option value="">Select Ollama Model</option>'; | |
data.models.forEach(model => { | |
const option = document.createElement('option'); | |
option.value = model; | |
option.textContent = model; | |
select.appendChild(option); | |
}); | |
}); | |
}); | |
document.getElementById('playBtn').addEventListener('click', () => { | |
if (currentAudio) { | |
currentAudio.play(); | |
} else if (lastAssistantMessage) { | |
const ttsModel = document.getElementById('ttsModel').value; | |
if (ttsModel) { | |
convertToSpeech(lastAssistantMessage, ttsModel); | |
} | |
} | |
}); | |
document.getElementById('pauseBtn').addEventListener('click', () => { | |
if (currentAudio) { | |
currentAudio.pause(); | |
} | |
}); | |
document.getElementById('volumeSlider').addEventListener('input', (e) => { | |
if (currentAudio) { | |
currentAudio.volume = e.target.value / 100; | |
} | |
}); | |
document.getElementById('chatsList').addEventListener('click', (e) => { | |
const chatFile = e.target.dataset.file; | |
if (chatFile) { | |
loadChat(chatFile); | |
} | |
}); | |
// Store selected models | |
document.getElementById('ollamaModel').addEventListener('change', (e) => { | |
selectedOllamaModel = e.target.value; | |
}); | |
document.getElementById('ttsModel').addEventListener('change', (e) => { | |
selectedTtsModel = e.target.value; | |
}); | |
// Context Menu Event Listeners | |
document.addEventListener('click', hideContextMenu); | |
document.getElementById('editMenuItem').addEventListener('click', () => { | |
if (editingMessageIndex !== null) { | |
const content = conversationHistory[editingMessageIndex].content; | |
showEditModal(content); | |
} | |
hideContextMenu(); | |
}); | |
// Edit Modal Event Listeners | |
document.querySelector('.close-modal').addEventListener('click', hideEditModal); | |
document.getElementById('cancelEditBtn').addEventListener('click', hideEditModal); | |
document.getElementById('saveEditBtn').addEventListener('click', saveEditedMessage); | |
// Initial load of Ollama models | |
document.getElementById('listModelsBtn').click(); | |
// Add sidebar toggle functionality | |
document.getElementById('toggleSidebar').addEventListener('click', () => { | |
const sidebar = document.getElementById('sidebar'); | |
const sidebarTitle = document.getElementById('sidebarTitle'); | |
if (sidebar.classList.contains('sidebar-expanded')) { | |
sidebar.classList.remove('sidebar-expanded'); | |
sidebar.classList.add('sidebar-collapsed'); | |
sidebarTitle.style.display = 'none'; | |
document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'none'); | |
} else { | |
sidebar.classList.remove('sidebar-collapsed'); | |
sidebar.classList.add('sidebar-expanded'); | |
sidebarTitle.style.display = 'block'; | |
document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'inline'); | |
} | |
}); | |
// Add responsive sidebar behavior | |
function handleResize() { | |
const sidebar = document.getElementById('sidebar'); | |
if (window.innerWidth < 768 && !sidebar.classList.contains('sidebar-collapsed')) { | |
sidebar.classList.remove('sidebar-expanded'); | |
sidebar.classList.add('sidebar-collapsed'); | |
document.getElementById('sidebarTitle').style.display = 'none'; | |
document.querySelectorAll('.chat-file span').forEach(span => span.style.display = 'none'); | |
} | |
} | |
window.addEventListener('resize', handleResize); | |
handleResize(); // Initial check | |
</script> | |
</body> | |
</html> | |