druvx13 commited on
Commit
bfad274
·
verified ·
1 Parent(s): 398c864

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +540 -19
index.html CHANGED
@@ -1,19 +1,540 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Qwen2.5 Coder - AI Assistant</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ :root {
11
+ --primary: #2563eb;
12
+ --primary-dark: #1d4ed8;
13
+ --dark: #1e293b;
14
+ --light: #f8fafc;
15
+ --gray: #94a3b8;
16
+ --success: #10b981;
17
+ }
18
+
19
+ .chat-container {
20
+ height: calc(100vh - 120px);
21
+ }
22
+
23
+ .message-user {
24
+ background-color: white;
25
+ border-left: 4px solid var(--primary);
26
+ }
27
+
28
+ .message-assistant {
29
+ background-color: white;
30
+ border-left: 4px solid var(--success);
31
+ }
32
+
33
+ .typing-indicator span {
34
+ display: inline-block;
35
+ width: 8px;
36
+ height: 8px;
37
+ background-color: var(--gray);
38
+ border-radius: 50%;
39
+ margin-right: 4px;
40
+ animation: bounce 1.4s infinite ease-in-out;
41
+ }
42
+
43
+ .typing-indicator span:nth-child(2) {
44
+ animation-delay: 0.2s;
45
+ }
46
+
47
+ .typing-indicator span:nth-child(3) {
48
+ animation-delay: 0.4s;
49
+ }
50
+
51
+ @keyframes bounce {
52
+ 0%, 60%, 100% { transform: translateY(0); }
53
+ 30% { transform: translateY(-5px); }
54
+ }
55
+
56
+ .code-block {
57
+ background-color: #0f172a;
58
+ color: #e2e8f0;
59
+ font-family: 'Fira Code', monospace;
60
+ }
61
+
62
+ .sidebar {
63
+ transition: transform 0.3s ease;
64
+ }
65
+
66
+ @media (max-width: 768px) {
67
+ .sidebar {
68
+ transform: translateX(-100%);
69
+ position: fixed;
70
+ z-index: 50;
71
+ height: 100vh;
72
+ }
73
+
74
+ .sidebar-open {
75
+ transform: translateX(0);
76
+ }
77
+ }
78
+ </style>
79
+ </head>
80
+ <body>
81
+ <div class="flex h-screen overflow-hidden">
82
+ <!-- Sidebar -->
83
+ <div class="sidebar bg-white w-64 border-r border-gray-200 flex flex-col md:relative absolute">
84
+ <div class="p-4 border-b border-gray-200 flex items-center">
85
+ <div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">Q</div>
86
+ <div class="ml-3">
87
+ <h2 class="font-semibold">Qwen2.5 Coder</h2>
88
+ <p class="text-xs text-gray-500">32B Instruct</p>
89
+ </div>
90
+ <button id="close-sidebar" class="ml-auto md:hidden text-gray-500">
91
+ <i class="fas fa-times"></i>
92
+ </button>
93
+ </div>
94
+
95
+ <div class="p-4 border-b border-gray-200">
96
+ <button id="new-chat" class="w-full bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded-md flex items-center justify-center">
97
+ <i class="fas fa-plus mr-2"></i> New Chat
98
+ </button>
99
+ </div>
100
+
101
+ <div class="flex-1 overflow-y-auto p-2">
102
+ <div class="p-2 text-sm font-medium text-gray-500">Recent Chats</div>
103
+ <div id="chat-history" class="space-y-1">
104
+ <!-- Chat history will be populated here -->
105
+ </div>
106
+ </div>
107
+
108
+ <div class="p-4 border-t border-gray-200">
109
+ <div class="flex items-center">
110
+ <div class="w-8 h-8 rounded-full bg-gray-300 flex items-center justify-center">
111
+ <i class="fas fa-user text-gray-600"></i>
112
+ </div>
113
+ <div class="ml-2">
114
+ <p class="text-sm font-medium">User Account</p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Main Content -->
121
+ <div class="flex-1 flex flex-col overflow-hidden">
122
+ <!-- Header -->
123
+ <header class="bg-white border-b border-gray-200 p-4 flex items-center">
124
+ <button id="menu-button" class="mr-4 text-gray-500 md:hidden">
125
+ <i class="fas fa-bars"></i>
126
+ </button>
127
+ <h1 class="text-xl font-semibold">Chat with Qwen2.5 Coder</h1>
128
+ <div class="ml-auto flex space-x-2">
129
+ <button class="p-2 rounded-full hover:bg-gray-100">
130
+ <i class="fas fa-cog text-gray-500"></i>
131
+ </button>
132
+ <button class="p-2 rounded-full hover:bg-gray-100">
133
+ <i class="fas fa-question-circle text-gray-500"></i>
134
+ </button>
135
+ </div>
136
+ </header>
137
+
138
+ <!-- Chat Area -->
139
+ <div id="chat-area" class="chat-container flex-1 overflow-y-auto p-4 space-y-4">
140
+ <div class="message-assistant p-4 rounded-lg shadow-sm">
141
+ <div class="flex items-start">
142
+ <div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center text-white mr-3">
143
+ <i class="fas fa-robot"></i>
144
+ </div>
145
+ <div class="flex-1">
146
+ <p class="font-medium text-gray-700">Qwen2.5 Coder</p>
147
+ <p class="mt-1 text-gray-800">Hello! I'm Qwen2.5 Coder, a 32B parameter AI assistant specialized in coding and technical questions. How can I help you today?</p>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <!-- Input Area -->
154
+ <div class="border-t border-gray-200 p-4 bg-white">
155
+ <div class="flex items-center mb-2">
156
+ <button class="p-2 rounded-full hover:bg-gray-100 text-gray-500 mr-1">
157
+ <i class="fas fa-paperclip"></i>
158
+ </button>
159
+ <button class="p-2 rounded-full hover:bg-gray-100 text-gray-500 mr-1">
160
+ <i class="fas fa-code"></i>
161
+ </button>
162
+ <button id="settings-button" class="p-2 rounded-full hover:bg-gray-100 text-gray-500">
163
+ <i class="fas fa-sliders-h"></i>
164
+ </button>
165
+ </div>
166
+
167
+ <!-- Settings Panel -->
168
+ <div id="settings-panel" class="hidden bg-gray-50 p-4 rounded-lg mb-4">
169
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
170
+ <div>
171
+ <label class="block text-sm font-medium text-gray-700 mb-1">System Message</label>
172
+ <textarea id="system-message" class="w-full p-2 border border-gray-300 rounded-md text-sm" rows="2">You are a helpful AI assistant that specializes in coding and technical questions.</textarea>
173
+ </div>
174
+ <div>
175
+ <label class="block text-sm font-medium text-gray-700 mb-1">Max Tokens</label>
176
+ <input id="max-tokens" type="range" min="1" max="200000" value="512" class="w-full">
177
+ <div class="flex justify-between text-xs text-gray-500">
178
+ <span>1</span>
179
+ <span>512</span>
180
+ <span>200k</span>
181
+ </div>
182
+ </div>
183
+ <div>
184
+ <label class="block text-sm font-medium text-gray-700 mb-1">Temperature</label>
185
+ <input id="temperature" type="range" min="0.1" max="4.0" step="0.1" value="0.7" class="w-full">
186
+ <div class="flex justify-between text-xs text-gray-500">
187
+ <span>0.1</span>
188
+ <span>0.7</span>
189
+ <span>4.0</span>
190
+ </div>
191
+ </div>
192
+ <div>
193
+ <label class="block text-sm font-medium text-gray-700 mb-1">Top-p</label>
194
+ <input id="top-p" type="range" min="0.1" max="1.0" step="0.05" value="0.95" class="w-full">
195
+ <div class="flex justify-between text-xs text-gray-500">
196
+ <span>0.1</span>
197
+ <span>0.95</span>
198
+ <span>1.0</span>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <div class="flex items-center">
205
+ <textarea id="message-input" class="flex-1 p-3 border border-gray-300 rounded-l-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="Type your message here..." rows="1"></textarea>
206
+ <button id="send-button" class="bg-blue-500 hover:bg-blue-600 text-white p-3 rounded-r-md">
207
+ <i class="fas fa-paper-plane"></i>
208
+ </button>
209
+ </div>
210
+ <p class="text-xs text-gray-500 mt-2">Qwen2.5 Coder may produce inaccurate information about people, places, or facts.</p>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <script>
216
+ // State management
217
+ const state = {
218
+ currentChatId: generateId(),
219
+ chatHistory: {},
220
+ settings: {
221
+ systemMessage: "You are a helpful AI assistant that specializes in coding and technical questions.",
222
+ maxTokens: 512,
223
+ temperature: 0.7,
224
+ topP: 0.95
225
+ }
226
+ };
227
+
228
+ // Initialize the app
229
+ document.addEventListener('DOMContentLoaded', function() {
230
+ // Load chat history from localStorage if available
231
+ const savedHistory = localStorage.getItem('chatHistory');
232
+ if (savedHistory) {
233
+ state.chatHistory = JSON.parse(savedHistory);
234
+ renderChatHistory();
235
+ }
236
+
237
+ // Create initial chat
238
+ if (!state.chatHistory[state.currentChatId]) {
239
+ state.chatHistory[state.currentChatId] = {
240
+ id: state.currentChatId,
241
+ title: "New Chat",
242
+ messages: [
243
+ {
244
+ role: "assistant",
245
+ content: "Hello! I'm Qwen2.5 Coder, a 32B parameter AI assistant specialized in coding and technical questions. How can I help you today?"
246
+ }
247
+ ],
248
+ createdAt: new Date().toISOString()
249
+ };
250
+ saveChatHistory();
251
+ }
252
+
253
+ // UI event listeners
254
+ setupEventListeners();
255
+ });
256
+
257
+ // Helper functions
258
+ function generateId() {
259
+ return Date.now().toString(36) + Math.random().toString(36).substring(2);
260
+ }
261
+
262
+ function saveChatHistory() {
263
+ localStorage.setItem('chatHistory', JSON.stringify(state.chatHistory));
264
+ }
265
+
266
+ function renderChatHistory() {
267
+ const chatHistoryContainer = document.getElementById('chat-history');
268
+ chatHistoryContainer.innerHTML = '';
269
+
270
+ // Sort chats by creation date (newest first)
271
+ const sortedChats = Object.values(state.chatHistory).sort((a, b) =>
272
+ new Date(b.createdAt) - new Date(a.createdAt)
273
+ );
274
+
275
+ sortedChats.forEach(chat => {
276
+ const chatElement = document.createElement('div');
277
+ chatElement.className = `p-2 hover:bg-gray-100 rounded-md cursor-pointer flex items-center ${chat.id === state.currentChatId ? 'bg-gray-100' : ''}`;
278
+ chatElement.innerHTML = `
279
+ <i class="fas fa-comment mr-2 text-gray-500"></i>
280
+ <span class="truncate">${chat.title}</span>
281
+ `;
282
+ chatElement.addEventListener('click', () => loadChat(chat.id));
283
+ chatHistoryContainer.appendChild(chatElement);
284
+ });
285
+ }
286
+
287
+ function loadChat(chatId) {
288
+ state.currentChatId = chatId;
289
+ renderChatHistory();
290
+ renderChatMessages();
291
+ }
292
+
293
+ function renderChatMessages() {
294
+ const chatArea = document.getElementById('chat-area');
295
+ chatArea.innerHTML = '';
296
+
297
+ const currentChat = state.chatHistory[state.currentChatId];
298
+ if (!currentChat) return;
299
+
300
+ currentChat.messages.forEach(message => {
301
+ const messageDiv = document.createElement('div');
302
+ messageDiv.className = message.role === 'user' ?
303
+ 'message-user p-4 rounded-lg shadow-sm' :
304
+ 'message-assistant p-4 rounded-lg shadow-sm';
305
+
306
+ messageDiv.innerHTML = `
307
+ <div class="flex items-start">
308
+ <div class="w-8 h-8 rounded-full ${message.role === 'user' ? 'bg-blue-500' : 'bg-green-500'} flex items-center justify-center text-white mr-3">
309
+ <i class="fas ${message.role === 'user' ? 'fa-user' : 'fa-robot'}"></i>
310
+ </div>
311
+ <div class="flex-1">
312
+ <p class="font-medium text-gray-700">${message.role === 'user' ? 'You' : 'Qwen2.5 Coder'}</p>
313
+ <div class="mt-1 text-gray-800">${formatMessageContent(message.content)}</div>
314
+ </div>
315
+ </div>
316
+ `;
317
+
318
+ chatArea.appendChild(messageDiv);
319
+ });
320
+
321
+ chatArea.scrollTop = chatArea.scrollHeight;
322
+ }
323
+
324
+ function formatMessageContent(content) {
325
+ // Simple formatting for code blocks (replace with proper markdown parsing if needed)
326
+ return content.replace(/```([\s\S]*?)```/g, '<div class="code-block mt-2 p-3 rounded-md text-sm overflow-x-auto"><pre><code>$1</code></pre></div>');
327
+ }
328
+
329
+ function setupEventListeners() {
330
+ // Toggle sidebar on mobile
331
+ document.getElementById('menu-button').addEventListener('click', function() {
332
+ document.querySelector('.sidebar').classList.add('sidebar-open');
333
+ });
334
+
335
+ document.getElementById('close-sidebar').addEventListener('click', function() {
336
+ document.querySelector('.sidebar').classList.remove('sidebar-open');
337
+ });
338
+
339
+ // Toggle settings panel
340
+ document.getElementById('settings-button').addEventListener('click', function() {
341
+ document.getElementById('settings-panel').classList.toggle('hidden');
342
+ });
343
+
344
+ // Auto-resize textarea
345
+ const textarea = document.getElementById('message-input');
346
+ textarea.addEventListener('input', function() {
347
+ this.style.height = 'auto';
348
+ this.style.height = (this.scrollHeight) + 'px';
349
+ });
350
+
351
+ // New chat button
352
+ document.getElementById('new-chat').addEventListener('click', function() {
353
+ const newChatId = generateId();
354
+ state.currentChatId = newChatId;
355
+ state.chatHistory[newChatId] = {
356
+ id: newChatId,
357
+ title: "New Chat",
358
+ messages: [
359
+ {
360
+ role: "assistant",
361
+ content: "Hello! I'm Qwen2.5 Coder, a 32B parameter AI assistant specialized in coding and technical questions. How can I help you today?"
362
+ }
363
+ ],
364
+ createdAt: new Date().toISOString()
365
+ };
366
+ saveChatHistory();
367
+ renderChatHistory();
368
+ renderChatMessages();
369
+ document.querySelector('.sidebar').classList.remove('sidebar-open');
370
+ });
371
+
372
+ // Send message
373
+ document.getElementById('send-button').addEventListener('click', sendMessage);
374
+
375
+ // Allow pressing Enter to send message (Shift+Enter for new line)
376
+ textarea.addEventListener('keydown', function(e) {
377
+ if (e.key === 'Enter' && !e.shiftKey) {
378
+ e.preventDefault();
379
+ sendMessage();
380
+ }
381
+ });
382
+
383
+ // Settings change listeners
384
+ document.getElementById('system-message').addEventListener('change', function() {
385
+ state.settings.systemMessage = this.value;
386
+ });
387
+
388
+ document.getElementById('max-tokens').addEventListener('input', function() {
389
+ state.settings.maxTokens = parseInt(this.value);
390
+ // Update the displayed value
391
+ this.parentNode.querySelector('span:nth-child(2)').textContent = this.value;
392
+ });
393
+
394
+ document.getElementById('temperature').addEventListener('input', function() {
395
+ state.settings.temperature = parseFloat(this.value);
396
+ this.parentNode.querySelector('span:nth-child(2)').textContent = this.value;
397
+ });
398
+
399
+ document.getElementById('top-p').addEventListener('input', function() {
400
+ state.settings.topP = parseFloat(this.value);
401
+ this.parentNode.querySelector('span:nth-child(2)').textContent = this.value;
402
+ });
403
+ }
404
+
405
+ async function sendMessage() {
406
+ const textarea = document.getElementById('message-input');
407
+ const message = textarea.value.trim();
408
+
409
+ if (message) {
410
+ // Clear input and adjust height
411
+ textarea.value = '';
412
+ textarea.style.height = 'auto';
413
+
414
+ // Add user message to chat
415
+ addMessageToChat('user', message);
416
+
417
+ // Show typing indicator
418
+ showTypingIndicator();
419
+
420
+ try {
421
+ // Prepare the request data
422
+ const requestData = {
423
+ message: message,
424
+ history: getMessageHistory(),
425
+ system_message: state.settings.systemMessage,
426
+ max_tokens: state.settings.maxTokens,
427
+ temperature: state.settings.temperature,
428
+ top_p: state.settings.topP
429
+ };
430
+
431
+ // Call the API
432
+ const response = await fetch('http://localhost:7860/api/chat', {
433
+ method: 'POST',
434
+ headers: {
435
+ 'Content-Type': 'application/json',
436
+ },
437
+ body: JSON.stringify(requestData)
438
+ });
439
+
440
+ // Handle streaming response
441
+ const reader = response.body.getReader();
442
+ const decoder = new TextDecoder();
443
+ let assistantResponse = '';
444
+ let responseId = generateId();
445
+
446
+ while(true) {
447
+ const { done, value } = await reader.read();
448
+ if(done) break;
449
+
450
+ const chunk = decoder.decode(value);
451
+ assistantResponse += chunk;
452
+
453
+ // Update the assistant message in real-time
454
+ updateAssistantMessage(responseId, assistantResponse);
455
+ }
456
+
457
+ // Finalize the message
458
+ finalizeMessage(responseId, assistantResponse);
459
+ updateChatTitle(message);
460
+
461
+ } catch (error) {
462
+ console.error('Error:', error);
463
+ addMessageToChat('assistant', "Sorry, I encountered an error. Please try again.");
464
+ } finally {
465
+ hideTypingIndicator();
466
+ }
467
+ }
468
+ }
469
+
470
+ function addMessageToChat(role, content, id) {
471
+ const messageId = id || generateId();
472
+ const currentChat = state.chatHistory[state.currentChatId];
473
+
474
+ currentChat.messages.push({
475
+ id: messageId,
476
+ role: role,
477
+ content: content
478
+ });
479
+
480
+ saveChatHistory();
481
+ renderChatMessages();
482
+ return messageId;
483
+ }
484
+
485
+ function updateAssistantMessage(id, content) {
486
+ const messageElement = document.querySelector(`[data-message-id="${id}"]`);
487
+ if(messageElement) {
488
+ messageElement.querySelector('.message-content').innerHTML = formatMessageContent(content);
489
+ messageElement.scrollIntoView({ behavior: 'smooth' });
490
+ }
491
+ }
492
+
493
+ function finalizeMessage(id, content) {
494
+ const currentChat = state.chatHistory[state.currentChatId];
495
+ const message = currentChat.messages.find(m => m.id === id);
496
+ if(message) {
497
+ message.content = content;
498
+ saveChatHistory();
499
+ }
500
+ }
501
+
502
+ function showTypingIndicator() {
503
+ const typingIndicator = document.createElement('div');
504
+ typingIndicator.className = 'message-assistant p-4 rounded-lg shadow-sm';
505
+ typingIndicator.innerHTML = `
506
+ <div class="flex items-start">
507
+ <div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center text-white mr-3">
508
+ <i class="fas fa-robot"></i>
509
+ </div>
510
+ <div class="flex-1">
511
+ <p class="font-medium text-gray-700">Qwen2.5 Coder</p>
512
+ <div class="typing-indicator mt-1">
513
+ <span></span>
514
+ <span></span>
515
+ <span></span>
516
+ </div>
517
+ </div>
518
+ </div>
519
+ `;
520
+ document.getElementById('chat-area').appendChild(typingIndicator);
521
+ }
522
+
523
+ function hideTypingIndicator() {
524
+ const typingIndicators = document.getElementsByClassName('typing-indicator');
525
+ while(typingIndicators.length > 0) {
526
+ typingIndicators[0].parentNode.parentNode.parentNode.remove();
527
+ }
528
+ }
529
+
530
+ function getMessageHistory() {
531
+ const currentChat = state.chatHistory[state.currentChatId];
532
+ return currentChat.messages
533
+ .filter(msg => msg.role !== 'system')
534
+ .map(msg => [msg.role === 'user' ? msg.content : '', msg.role === 'assistant' ? msg.content : '']);
535
+ }
536
+
537
+ // ... (keep the rest of your existing helper functions) ...
538
+ </script>
539
+ </body>
540
+ </html>