riteshraut commited on
Commit
3666d6e
·
1 Parent(s): bd93e48
Files changed (1) hide show
  1. templates/index.html +610 -880
templates/index.html CHANGED
@@ -1,885 +1,615 @@
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>CogniChat - Chat with your Documents</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Google+Sans:wght@400;500;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
11
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
- <style>
13
- :root {
14
- --background: #f0f4f9;
15
- --foreground: #1f1f1f;
16
- --primary: #1a73e8;
17
- --primary-hover: #1867cf;
18
- --card: #ffffff;
19
- --card-border: #dadce0;
20
- --input-bg: #e8f0fe;
21
- --user-bubble: #d9e7ff;
22
- --bot-bubble: #f1f3f4;
23
- --select-bg: #ffffff;
24
- --select-border: #dadce0;
25
- --select-text: #1f1f1f;
26
- }
27
-
28
- .dark {
29
- --background: #111827; /* Darker background */
30
- --foreground: #e5e7eb;
31
- --primary: #3b82f6; /* Adjusted primary blue */
32
- --primary-hover: #60a5fa; /* Lighter hover blue */
33
- --card: #1f2937; /* Dark card background */
34
- --card-border: #4b5563; /* Greyer border */
35
- --input-bg: #374151; /* Darker input background */
36
- --user-bubble: #374151; /* Darker user bubble */
37
- --bot-bubble: #374151; /* Darker bot bubble */
38
- --select-bg: #374151;
39
- --select-border: #6b7280;
40
- --select-text: #f3f4f6;
41
- --code-bg: #2d2d2d; /* Specific background for code blocks */
42
- --code-text: #d4d4d4; /* Light grey text for code */
43
- --copy-btn-bg: #4a4a4a;
44
- --copy-btn-hover-bg: #5a5a5a;
45
- --copy-btn-text: #e0e0e0;
46
- }
47
-
48
- body {
49
- font-family: 'Inter', 'Google Sans', 'Roboto', sans-serif;
50
- background-color: var(--background);
51
- color: var(--foreground);
52
- overflow: hidden; /* Prevent body scroll */
53
- }
54
-
55
- #chat-window {
56
- scroll-behavior: smooth; /* Ensure smooth programatic scroll */
57
- }
58
- #chat-window::-webkit-scrollbar { width: 8px; }
59
- #chat-window::-webkit-scrollbar-track { background: transparent; }
60
- #chat-window::-webkit-scrollbar-thumb { background-color: #4b5563; border-radius: 20px; }
61
- .dark #chat-window::-webkit-scrollbar-thumb { background-color: #5f6368; }
62
-
63
- .drop-zone--over {
64
- border-color: var(--primary);
65
- box-shadow: 0 0 20px rgba(59, 130, 246, 0.4);
66
- }
67
-
68
- .loader {
69
- width: 48px;
70
- height: 48px;
71
- border: 3px solid var(--card-border);
72
- border-radius: 50%;
73
- display: inline-block;
74
- position: relative;
75
- box-sizing: border-box;
76
- animation: rotation 1s linear infinite;
77
- }
78
- .loader::after {
79
- content: '';
80
- box-sizing: border-box;
81
- position: absolute;
82
- left: 50%;
83
- top: 50%;
84
- transform: translate(-50%, -50%);
85
- width: 56px;
86
- height: 56px;
87
- border-radius: 50%;
88
- border: 3px solid;
89
- border-color: var(--primary) transparent;
90
- }
91
-
92
- @keyframes rotation {
93
- 0% { transform: rotate(0deg); }
94
- 100% { transform: rotate(360deg); }
95
- }
96
-
97
- /* --- Updated Typing Indicator --- */
98
- .typing-indicator {
99
- display: inline-flex; /* Changed to inline-flex */
100
- align-items: center;
101
- padding: 8px 0; /* Add some vertical padding */
102
- }
103
- .typing-indicator span {
104
- height: 8px; /* Slightly smaller dots */
105
- width: 8px;
106
- margin: 0 2px;
107
- background-color: #9E9E9E;
108
- border-radius: 50%;
109
- opacity: 0; /* Start invisible */
110
- animation: typing-pulse 1.4s infinite ease-in-out;
111
- }
112
- .typing-indicator span:nth-child(1) { animation-delay: 0s; }
113
- .typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
114
- .typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
115
-
116
- @keyframes typing-pulse {
117
- 0%, 100% { opacity: 0; transform: scale(0.7); }
118
- 50% { opacity: 1; transform: scale(1); }
119
- }
120
- /* --- End Typing Indicator --- */
121
-
122
- /* --- Updated Markdown Styling --- */
123
- .markdown-content { /* Base styles for the content area */
124
- line-height: 1.75;
125
- }
126
- .markdown-content p { margin-bottom: 1rem; }
127
- .markdown-content h1, .markdown-content h2, .markdown-content h3,
128
- .markdown-content h4, .markdown-content h5, .markdown-content h6 {
129
- font-weight: 600;
130
- margin-top: 1.5rem;
131
- margin-bottom: 0.75rem;
132
- line-height: 1.3;
133
- }
134
- .markdown-content h1 { font-size: 1.5em; border-bottom: 1px solid var(--card-border); padding-bottom: 0.3rem;}
135
- .markdown-content h2 { font-size: 1.25em; }
136
- .markdown-content h3 { font-size: 1.1em; }
137
- .markdown-content ul, .markdown-content ol { padding-left: 1.75rem; margin-bottom: 1rem; }
138
- .markdown-content li { margin-bottom: 0.5rem; }
139
- .markdown-content a { color: var(--primary); text-decoration: none; font-weight: 500; }
140
- .markdown-content a:hover { text-decoration: underline; }
141
- .markdown-content strong, .markdown-content b { font-weight: 600; } /* Ensure bold works */
142
- .markdown-content blockquote {
143
- border-left: 4px solid var(--card-border);
144
- padding-left: 1rem;
145
- margin-left: 0;
146
- margin-bottom: 1rem;
147
- color: #a0aec0; /* Lighter text for quotes */
148
- }
149
- /* --- Code Block Styling --- */
150
- .markdown-content pre {
151
- position: relative;
152
- background-color: var(--code-bg);
153
- border: 1px solid var(--card-border);
154
- border-radius: 0.5rem;
155
- margin-bottom: 1rem;
156
- font-size: 0.9em;
157
- color: var(--code-text);
158
- overflow: hidden; /* Hide horizontal overflow until hovered/focused */
159
- }
160
- .markdown-content pre code {
161
- display: block;
162
- padding: 1rem;
163
- overflow-x: auto; /* Enable horizontal scroll on the code itself */
164
- background: none !important; /* Override potential highlight.js background */
165
- font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
166
- white-space: pre; /* Ensure whitespace is preserved */
167
- }
168
- /* --- Copy Button Styling --- */
169
- .markdown-content pre .copy-code-btn {
170
  position: absolute;
171
- top: 0.5rem;
172
- right: 0.5rem;
173
- background-color: var(--copy-btn-bg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  border: 1px solid var(--card-border);
175
- color: var(--copy-btn-text);
176
- padding: 0.3rem 0.6rem;
177
- border-radius: 0.25rem;
178
- cursor: pointer;
179
- opacity: 0; /* Initially hidden */
180
- transition: opacity 0.2s, background-color 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  font-size: 0.8em;
182
- display: flex; /* For icon alignment */
183
- align-items: center;
184
- gap: 0.25rem;
185
- }
186
- .markdown-content pre .copy-code-btn:hover {
187
- background-color: var(--copy-btn-hover-bg);
188
- }
189
- .markdown-content pre:hover .copy-code-btn {
190
- opacity: 1; /* Show on hover */
191
- }
192
- /* --- Inline Code Styling --- */
193
- .markdown-content code:not(pre code) {
194
- background-color: rgba(110, 118, 129, 0.4);
195
- padding: 0.2em 0.4em;
196
- margin: 0 0.1em; /* Add slight horizontal margin */
197
- font-size: 85%;
198
- border-radius: 6px;
199
- font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
200
- }
201
- /* --- End Markdown Styling --- */
202
-
203
- .tts-button-loader {
204
- width: 16px;
205
- height: 16px;
206
- border: 2px solid currentColor;
207
- border-radius: 50%;
208
- display: inline-block;
209
- box-sizing: border-box;
210
- animation: rotation 0.8s linear infinite;
211
- border-bottom-color: transparent;
212
- }
213
-
214
- /* --- Style for TTS controls container --- */
215
- .tts-controls {
216
- display: flex;
217
- align-items: center;
218
- gap: 0.5rem; /* Space between play and speed buttons */
219
- margin-top: 0.5rem;
220
- }
221
-
222
- /* --- Style for Speed Cycle Button --- */
223
- .speed-cycle-btn {
224
- padding: 0.25rem 0.6rem; /* Smaller padding */
225
- font-size: 0.75rem; /* Smaller text */
226
- background-color: #4b5563; /* Grey background */
227
- color: #e5e7eb; /* Light text */
228
- border-radius: 9999px; /* Pill shape */
229
- border: none;
230
- cursor: pointer;
231
- transition: background-color 0.2s;
232
- white-space: nowrap; /* Prevent text wrapping */
233
- margin-top: 0.5rem;
234
- }
235
- .speed-cycle-btn:hover {
236
- background-color: #1f0bb8e6; /* Lighter grey on hover */
237
- }
238
- .speed-cycle-btn:disabled {
239
- opacity: 0.5;
240
- cursor: not-allowed;
241
- }
242
-
243
- /* --- Select dropdown styles (kept for consistency if needed elsewhere) --- */
244
- .select-wrapper {
245
- position: relative;
246
- }
247
- .select-wrapper select {
248
- background-color: var(--select-bg);
249
- border: 1px solid var(--select-border);
250
- color: var(--select-text);
251
- padding: 0.75rem 2.5rem 0.75rem 1rem;
252
- border-radius: 0.75rem;
253
- font-size: 0.875rem;
254
- width: 100%;
255
- appearance: none;
256
- -webkit-appearance: none;
257
- transition: all 0.2s ease-in-out;
258
- cursor: pointer;
259
- background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
260
- background-position: right 0.75rem center;
261
- background-repeat: no-repeat;
262
- background-size: 1.25em 1.25em;
263
- }
264
- </style>
265
- </head>
266
- <body class="w-screen h-screen dark">
267
- <main id="main-content" class="h-full flex flex-col transition-opacity duration-500">
268
- <div id="chat-container" class="hidden flex-1 flex flex-col w-full mx-auto overflow-hidden">
269
-
270
- <header class="p-4 border-b border-[var(--card-border)] flex-shrink-0 flex justify-between items-center w-full">
271
- <div class="w-1/4"></div> <div class="w-1/2 text-center">
272
- <h1 class="text-xl font-medium tracking-wide">CogniChat ✨</h1>
273
- <p id="chat-filename" class="text-xs text-gray-400 mt-1 truncate"></p>
274
- </div>
275
- <div id="chat-session-info" class="w-1/4 text-right text-xs space-y-1 pr-4">
276
- </div>
277
- </header>
278
-
279
- <div id="chat-window" class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-10">
280
- <div id="chat-content" class="max-w-4xl mx-auto space-y-8"></div>
281
- </div>
282
- <div class="p-4 flex-shrink-0 bg-opacity-50 backdrop-blur-md border-t border-[var(--card-border)]">
283
- <form id="chat-form" class="max-w-4xl mx-auto bg-[var(--card)] rounded-full p-2 flex items-center shadow-lg border border-[var(--card-border)] focus-within:ring-2 focus-within:ring-[var(--primary)] transition-all">
284
- <input type="text" id="chat-input" placeholder="Ask a question about your documents..." class="flex-grow bg-transparent focus:outline-none px-4 text-sm" autocomplete="off">
285
- <button type="submit" id="chat-submit-btn" class="bg-[var(--primary)] hover:bg-[var(--primary-hover)] text-white p-2.5 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed" title="Send">
286
- <svg class="w-5 h-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clip-rule="evenodd"></path></svg>
287
- </button>
288
- </form>
289
- </div>
290
- </div>
291
-
292
- <div id="upload-container" class="flex-1 flex flex-col items-center justify-center p-8 transition-opacity duration-300">
293
- <div class="text-center max-w-xl w-full">
294
- <h1 class="text-5xl font-bold mb-3 tracking-tight">CogniChat ✨</h1>
295
- <p class="text-lg text-gray-400 mb-8">Upload your documents to start a conversation.</p>
296
- <div class="mb-8 p-5 bg-[var(--card)] rounded-2xl border border-[var(--card-border)] shadow-lg">
297
- <div class="flex flex-col sm:flex-row items-center gap-6">
298
- <div class="w-full sm:w-1/2">
299
- <div class="flex items-center gap-2 mb-2">
300
- <svg class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z" /></svg>
301
- <label for="model-select" class="block text-sm font-medium text-gray-300">Model</label>
302
- </div>
303
- <div class="select-wrapper">
304
- <select id="model-select" name="model_name">
305
- <option value="moonshotai/kimi-k2-instruct" selected>Kimi Instruct</option>
306
- <option value="openai/gpt-oss-20b">GPT OSS 20b</option>
307
- <option value="llama-3.3-70b-versatile">Llama 3.3 70b</option>
308
- <option value="llama-3.1-8b-instant">Llama 3.1 8b Instant</option>
309
- </select>
310
- </div>
311
- </div>
312
- <div class="w-full sm:w-1/2">
313
- <div class="flex items-center gap-2 mb-2">
314
- <svg class="w-5 h-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M5.5 16a3.5 3.5 0 100-7 3.5 3.5 0 000 7zM12 5.5a3.5 3.5 0 11-7 0 3.5 3.5 0 017 0zM14.5 16a3.5 3.5 0 100-7 3.5 3.5 0 000 7z" clip-rule="evenodd" /></svg>
315
- <label for="temperature-select" class="block text-sm font-medium text-gray-300">Mode</label>
316
- </div>
317
- <div class="select-wrapper">
318
- <select id="temperature-select" name="temperature">
319
- <option value="0.2" selected>0.2 - Precise</option>
320
- <option value="0.4">0.4 - Confident</option>
321
- <option value="0.6">0.6 - Balanced</option>
322
- <option value="0.8">0.8 - Flexible</option>
323
- <option value="1.0">1.0 - Creative</option>
324
- </select>
325
- </div>
326
- </div>
327
- </div>
328
- <p class="text-xs text-gray-500 mt-4 text-center">Higher creativity modes may reduce factual accuracy.</p>
329
- </div>
330
- <div id="drop-zone" class="w-full text-center border-2 border-dashed border-[var(--card-border)] rounded-2xl p-10 transition-all duration-300 cursor-pointer hover:bg-[var(--card)] hover:border-[var(--primary)]">
331
- <div class="flex flex-col items-center justify-center pointer-events-none">
332
- <svg class="mx-auto h-12 w-12 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16.5V9.75m0 0l3-3m-3 3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"></path></svg>
333
- <p class="mt-4 text-sm font-medium text-gray-400">Drag & drop files or <span class="text-[var(--primary)] font-semibold">click to upload</span></p>
334
- <p class="text-xs text-gray-400 mt-1">Supports PDF, DOCX, TXT</p>
335
- <p id="file-name" class="mt-2 text-xs text-gray-500"></p>
336
- </div>
337
- <input id="file-upload" type="file" class="hidden" accept=".pdf,.txt,.docx" multiple>
338
- </div>
339
- </div>
340
- </div>
341
-
342
- <div id="loading-overlay" class="hidden fixed inset-0 bg-[var(--background)] bg-opacity-80 backdrop-blur-sm flex flex-col items-center justify-center z-50">
343
- <div class="loader"></div>
344
- <p id="loading-text" class="mt-6 text-sm font-medium"></p>
345
- <p id="loading-subtext" class="mt-2 text-xs text-gray-400"></p>
346
- </div>
347
- </main>
348
-
349
- <script>
350
- document.addEventListener('DOMContentLoaded', () => {
351
- const uploadContainer = document.getElementById('upload-container');
352
- const chatContainer = document.getElementById('chat-container');
353
- const dropZone = document.getElementById('drop-zone');
354
- const fileUploadInput = document.getElementById('file-upload');
355
- const fileNameSpan = document.getElementById('file-name');
356
- const loadingOverlay = document.getElementById('loading-overlay');
357
- const loadingText = document.getElementById('loading-text');
358
- const loadingSubtext = document.getElementById('loading-subtext');
359
- const chatForm = document.getElementById('chat-form');
360
- const chatInput = document.getElementById('chat-input');
361
- const chatSubmitBtn = document.getElementById('chat-submit-btn');
362
- const chatWindow = document.getElementById('chat-window');
363
- const chatContent = document.getElementById('chat-content');
364
- const modelSelect = document.getElementById('model-select');
365
- const temperatureSelect = document.getElementById('temperature-select');
366
- const chatFilename = document.getElementById('chat-filename');
367
- const chatSessionInfo = document.getElementById('chat-session-info');
368
-
369
- let sessionId = sessionStorage.getItem('cognichat_session_id');
370
- let currentModelInfo = JSON.parse(sessionStorage.getItem('cognichat_model_info'));
371
-
372
- marked.setOptions({
373
- breaks: true,
374
- gfm: true,
375
- });
376
-
377
- if (sessionId && currentModelInfo) {
378
- console.log("Restoring session:", sessionId);
379
- uploadContainer.classList.add('hidden');
380
- chatContainer.classList.remove('hidden');
381
- chatFilename.innerHTML = `Chatting with: <strong class="font-semibold">${sessionStorage.getItem('cognichat_filename') || 'documents'}</strong>`;
382
- chatFilename.title = sessionStorage.getItem('cognichat_filename') || 'documents';
383
- chatSessionInfo.innerHTML = `
384
- <p>Model: ${currentModelInfo.simpleModelName}</p>
385
- <p>Mode: ${currentModelInfo.mode}</p>
386
- <button class="mt-1 text-xs text-blue-400 hover:text-blue-300 focus:outline-none" onclick="sessionStorage.clear(); location.reload();">New Chat</button>`;
387
- }
388
-
389
-
390
- // --- File Upload Logic ---
391
- dropZone.addEventListener('click', () => fileUploadInput.click());
392
-
393
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
394
- dropZone.addEventListener(eventName, e => {e.preventDefault(); e.stopPropagation();}, false);
395
- document.body.addEventListener(eventName, e => {e.preventDefault(); e.stopPropagation();}, false);
396
- });
397
- ['dragenter', 'dragover'].forEach(eventName => dropZone.addEventListener(eventName, () => dropZone.classList.add('drop-zone--over')));
398
- ['dragleave', 'drop'].forEach(eventName => dropZone.addEventListener(eventName, () => dropZone.classList.remove('drop-zone--over')));
399
-
400
- dropZone.addEventListener('drop', (e) => {
401
- if (e.dataTransfer.files.length > 0) handleFiles(e.dataTransfer.files);
402
- });
403
- fileUploadInput.addEventListener('change', (e) => {
404
- if (e.target.files.length > 0) handleFiles(e.target.files);
405
- });
406
-
407
- async function handleFiles(files) {
408
- const formData = new FormData();
409
- let fileNames = Array.from(files).map(f => f.name);
410
- for (const file of files) { formData.append('file', file); }
411
-
412
- formData.append('model_name', modelSelect.value);
413
- formData.append('temperature', temperatureSelect.value);
414
-
415
- fileNameSpan.textContent = `Selected: ${fileNames.join(', ')}`;
416
- await uploadAndProcessFiles(formData);
417
- }
418
-
419
- async function uploadAndProcessFiles(formData) {
420
- loadingOverlay.classList.remove('hidden');
421
- loadingText.textContent = `Processing document(s)...`;
422
- loadingSubtext.textContent = "Creating a knowledge base... this might take a minute 🧠";
423
- chatContent.innerHTML = ''; // Clear previous chat content on new upload
424
-
425
- try {
426
- const response = await fetch('/upload', { method: 'POST', body: formData });
427
- const result = await response.json();
428
- if (!response.ok) throw new Error(result.message || 'Unknown error occurred during upload.');
429
-
430
- sessionId = result.session_id;
431
- sessionStorage.setItem('cognichat_session_id', sessionId);
432
-
433
- const modelOption = modelSelect.querySelector(`option[value="${result.model_name}"]`);
434
- const simpleModelName = modelOption ? modelOption.textContent : result.model_name; // Adjust if needed
435
-
436
- currentModelInfo = {
437
- model: result.model_name,
438
- mode: result.mode,
439
- simpleModelName: simpleModelName // Use the derived simpler name
440
- };
441
- sessionStorage.setItem('cognichat_model_info', JSON.stringify(currentModelInfo)); // Store model info
442
- sessionStorage.setItem('cognichat_filename', result.filename); // Store filename
443
-
444
- chatFilename.innerHTML = `Chatting with: <strong class="font-semibold">${result.filename}</strong>`;
445
- chatFilename.title = result.filename;
446
-
447
- chatSessionInfo.innerHTML = `
448
- <p>Model: ${currentModelInfo.simpleModelName}</p>
449
- <p>Mode: ${currentModelInfo.mode}</p>
450
- <button class="mt-1 text-xs text-blue-400 hover:text-blue-300 focus:outline-none" onclick="sessionStorage.clear(); location.reload();">New Chat</button>`;
451
-
452
- uploadContainer.classList.add('hidden');
453
- chatContainer.classList.remove('hidden');
454
- appendMessage("Hello! 👋 I've analyzed your documents. What would you like to know?", "bot", currentModelInfo);
455
-
456
- } catch (error) {
457
- console.error('Upload error:', error);
458
- alert(`Error processing files: ${error.message}`);
459
- sessionStorage.clear(); // Clear session if upload fails
460
- } finally {
461
- loadingOverlay.classList.add('hidden');
462
- fileNameSpan.textContent = '';
463
- fileUploadInput.value = '';
464
- }
465
- }
466
-
467
- // --- Chat Logic (Using Server-Sent Events - UPDATED FOR STREAMING & INDICATOR) ---
468
- chatForm.addEventListener('submit', async (e) => {
469
- e.preventDefault();
470
- const question = chatInput.value.trim();
471
- if (!question || !sessionId) {
472
- console.warn("Submit ignored: No question or session ID.");
473
- return;
474
- }
475
-
476
- appendMessage(question, 'user');
477
- chatInput.value = '';
478
- chatInput.disabled = true;
479
- chatSubmitBtn.disabled = true;
480
-
481
- let botMessageContainer;
482
- let contentDiv;
483
- let fullResponse = '';
484
- let eventSource = null;
485
- let inactivityTimeout = null;
486
- let streamClosedCleanly = false; // Flag to check if stream ended normally vs error
487
- let typingIndicatorElement = null; // Store indicator element
488
-
489
- // Function to finalize chat (called on error, timeout, or successful completion)
490
- function finalizeChat(isError = false) {
491
- console.log(`Finalizing chat. Was error: ${isError}, Stream ended cleanly: ${streamClosedCleanly}`);
492
- if (eventSource) {
493
- eventSource.close();
494
- eventSource = null;
495
- console.log("SSE connection explicitly closed in finalizeChat.");
496
- }
497
- if (inactivityTimeout) {
498
- clearTimeout(inactivityTimeout);
499
- inactivityTimeout = null;
500
- }
501
- // Remove indicator if it's still there
502
- if (typingIndicatorElement && typingIndicatorElement.parentNode) {
503
- typingIndicatorElement.parentNode.removeChild(typingIndicatorElement);
504
- typingIndicatorElement = null;
505
- }
506
-
507
-
508
- if (botMessageContainer && contentDiv) {
509
- const hasErrorMsg = contentDiv.innerHTML.includes('⚠️');
510
- // Ensure final render, apply copy buttons and TTS ONLY if response wasn't an error
511
- if (!hasErrorMsg && fullResponse) {
512
- // Re-parse the complete response to ensure correct final Markdown
513
- contentDiv.innerHTML = marked.parse(fullResponse);
514
- // Apply final touches like copy buttons and TTS
515
- contentDiv.querySelectorAll('pre').forEach(addCopyButton);
516
- addTextToSpeechControls(botMessageContainer, fullResponse);
517
- // Optional: Final highlighting if using highlight.js
518
- // contentDiv.querySelectorAll('pre code').forEach(block => hljs.highlightElement(block));
519
- }
520
- scrollToBottom(true); // Ensure scrolled to the end
521
- }
522
-
523
- // Always re-enable input fields
524
- chatInput.disabled = false;
525
- chatSubmitBtn.disabled = false;
526
- chatInput.focus();
527
- }
528
-
529
- try {
530
- // Create the bot message container *before* starting the stream
531
- botMessageContainer = appendMessage('', 'bot', currentModelInfo); // Append empty bot message
532
- contentDiv = botMessageContainer.querySelector('.markdown-content');
533
-
534
- // Show typing indicator *inside* the contentDiv
535
- typingIndicatorElement = showTypingIndicator();
536
- if (contentDiv) {
537
- contentDiv.appendChild(typingIndicatorElement);
538
- scrollToBottom(true); // Scroll to show indicator
539
- } else {
540
- console.error("Could not find contentDiv to append typing indicator.");
541
- }
542
-
543
-
544
- // Establish SSE connection via GET request
545
- const chatUrl = `/chat?question=${encodeURIComponent(question)}&session_id=${encodeURIComponent(sessionId)}`;
546
- console.log("Connecting to SSE:", chatUrl);
547
- eventSource = new EventSource(chatUrl);
548
-
549
- eventSource.onopen = () => {
550
- console.log("SSE Connection opened.");
551
- // Remove indicator when connection opens and stream is about to start
552
- if (typingIndicatorElement && typingIndicatorElement.parentNode) {
553
- typingIndicatorElement.parentNode.removeChild(typingIndicatorElement);
554
- typingIndicatorElement = null;
555
- }
556
- streamClosedCleanly = false; // Reset flag on new connection
557
- };
558
-
559
- eventSource.onmessage = (event) => {
560
- // Remove indicator on first message just in case onopen didn't fire reliably
561
- if (typingIndicatorElement && typingIndicatorElement.parentNode) {
562
- typingIndicatorElement.parentNode.removeChild(typingIndicatorElement);
563
- typingIndicatorElement = null;
564
- }
565
-
566
- // Reset inactivity timeout on each message
567
- if (inactivityTimeout) clearTimeout(inactivityTimeout);
568
- inactivityTimeout = setTimeout(() => {
569
- console.log("Inactivity timeout triggered after message.");
570
- streamClosedCleanly = true; // Assume normal end
571
- finalizeChat(false);
572
- }, 5000); // 5 seconds of inactivity
573
-
574
- let data;
575
- try {
576
- data = JSON.parse(event.data);
577
- } catch (parseError){
578
- console.error("Failed to parse SSE data:", event.data, parseError);
579
- contentDiv.innerHTML += `<p class="text-red-400 text-sm">Error receiving data chunk.</p>`;
580
- return;
581
- }
582
-
583
- if (data.error) {
584
- console.error('SSE Error from server:', data.error);
585
- contentDiv.innerHTML = `<p class="text-red-500 font-semibold">⚠️ Server Error: ${data.error}</p>`;
586
- streamClosedCleanly = false;
587
- finalizeChat(true); // Pass true for error
588
- return;
589
- }
590
-
591
- if (data.token !== undefined && data.token !== null) {
592
- fullResponse += data.token;
593
- // Update content by parsing the accumulated response
594
- contentDiv.innerHTML = marked.parse(fullResponse);
595
- scrollToBottom(); // Scroll smoothly as content arrives
596
- }
597
- };
598
-
599
- eventSource.onerror = (error) => {
600
- console.error('SSE connection error event:', error);
601
- // Remove indicator on error
602
- if (typingIndicatorElement && typingIndicatorElement.parentNode) {
603
- typingIndicatorElement.parentNode.removeChild(typingIndicatorElement);
604
- typingIndicatorElement = null;
605
- }
606
- // Don't show generic error if we received data and the stream likely just closed normally
607
- if (!fullResponse && !streamClosedCleanly) { // Only show error if nothing received AND not already cleanly closed
608
- const errorMsg = "⚠️ Connection error. Please try again.";
609
- if (contentDiv) {
610
- contentDiv.innerHTML = `<p class="text-red-500 font-semibold">${errorMsg}</p>`;
611
- } else {
612
- // Fallback if container wasn't created somehow
613
- appendMessage(errorMsg, 'bot', currentModelInfo); // Pass model info here too
614
- }
615
- streamClosedCleanly = false;
616
- } else if (!streamClosedCleanly) {
617
- // If we received data, assume it's a normal closure misinterpreted as error
618
- console.log("SSE connection closed (likely normal end detected by onerror).");
619
- streamClosedCleanly = true; // Mark as clean closure NOW
620
- } else {
621
- console.log("SSE onerror event after stream already marked cleanly closed.")
622
- }
623
- finalizeChat(!streamClosedCleanly); // Finalize, indicate error if not clean
624
- };
625
-
626
- } catch (error) {
627
- // For setup errors before SSE starts
628
- console.error('Chat setup error:', error);
629
- // Remove indicator on setup error
630
- if (typingIndicatorElement && typingIndicatorElement.parentNode) {
631
- typingIndicatorElement.parentNode.removeChild(typingIndicatorElement);
632
- typingIndicatorElement = null;
633
- }
634
- if (botMessageContainer && contentDiv) {
635
- contentDiv.innerHTML = `<p class="text-red-500 font-semibold">⚠️ Error starting chat: ${error.message}</p>`;
636
- } else {
637
- appendMessage(`Error starting chat: ${error.message}`, 'bot', currentModelInfo); // Pass model info
638
- }
639
- finalizeChat(true);
640
- }
641
- });
642
-
643
-
644
- // --- UI Helper Functions ---
645
-
646
- function appendMessage(text, sender, modelInfo = null) {
647
- const messageWrapper = document.createElement('div');
648
- const iconSVG = sender === 'user'
649
- ? `<div class="bg-blue-200 dark:bg-gray-700 p-2.5 rounded-full flex-shrink-0 mt-1 self-start"><svg class="w-5 h-5 text-blue-700 dark:text-blue-300" viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"></path></svg></div>`
650
- : `<div class="bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0 mt-1 self-start text-xl flex items-center justify-center w-10 h-10">✨</div>`;
651
-
652
- let senderHTML;
653
- if (sender === 'user') {
654
- senderHTML = '<p class="font-medium text-sm mb-1">You</p>';
655
- } else {
656
- let modelInfoHTML = '';
657
- const displayInfo = modelInfo || currentModelInfo;
658
- if (displayInfo && displayInfo.simpleModelName) {
659
- modelInfoHTML = `
660
- <span class="ml-2 text-xs font-normal text-gray-400">
661
- (Model: ${displayInfo.simpleModelName} | Mode: ${displayInfo.mode})
662
- </span>
663
- `;
664
- }
665
- senderHTML = `<div class="font-medium text-sm mb-1 flex items-center">CogniChat ${modelInfoHTML}</div>`;
666
- }
667
-
668
- messageWrapper.className = `flex items-start gap-3`;
669
- // Ensure markdown-content div exists even if text is empty for the indicator
670
- messageWrapper.innerHTML = `
671
- ${iconSVG}
672
- <div class="flex-1 pt-1 min-w-0"> ${senderHTML}
673
- <div class="text-base markdown-content prose dark:prose-invert max-w-none">${text ? marked.parse(text) : ''}</div>
674
- <div class="tts-controls mt-2"></div>
675
- </div>
676
- `;
677
- chatContent.appendChild(messageWrapper);
678
- // Force scroll only when adding user message or initial bot message with content
679
- if (sender === 'user' || text) {
680
- scrollToBottom(true);
681
- }
682
- // Return the container that holds the sender name and content div
683
- return messageWrapper.querySelector('.flex-1');
684
- }
685
-
686
- // --- UPDATED showTypingIndicator ---
687
- function showTypingIndicator() {
688
- const indicator = document.createElement('div');
689
- indicator.className = 'typing-indicator'; // Use the main class
690
- indicator.innerHTML = '<span></span><span></span><span></span>';
691
- // Don't append here, just return the element
692
- return indicator;
693
- }
694
- // --- End UPDATED showTypingIndicator ---
695
-
696
-
697
- function scrollToBottom(force = false) {
698
- const isNearBottom = chatWindow.scrollHeight - chatWindow.clientHeight <= chatWindow.scrollTop + 150; // Threshold
699
-
700
- if (force || isNearBottom) {
701
- requestAnimationFrame(() => { // Use rAF for smoother render loop
702
- chatWindow.scrollTo({
703
- top: chatWindow.scrollHeight,
704
- behavior: 'smooth'
705
- });
706
- });
707
- }
708
- }
709
-
710
- function addCopyButton(pre) {
711
- if (pre.querySelector('.copy-code-btn')) return;
712
-
713
- const button = document.createElement('button');
714
- // Updated classes for better styling
715
- button.className = 'copy-code-btn absolute top-2 right-2 p-1 rounded bg-[var(--copy-btn-bg)] text-[var(--copy-btn-text)] hover:bg-[var(--copy-btn-hover-bg)] transition-opacity duration-200 flex items-center gap-1 text-xs';
716
- button.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg> Copy`;
717
- pre.style.position = 'relative'; // Ensure parent is relative for absolute positioning
718
- pre.appendChild(button);
719
-
720
- button.addEventListener('click', () => {
721
- const code = pre.querySelector('code')?.innerText || '';
722
- navigator.clipboard.writeText(code)
723
- .then(() => {
724
- button.textContent = 'Copied!';
725
- setTimeout(() => button.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg> Copy`, 1500);
726
- })
727
- .catch(err => {
728
- console.error('Failed to copy code: ', err);
729
- button.textContent = 'Error';
730
- setTimeout(() => button.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg> Copy`, 1500);
731
- });
732
- });
733
- }
734
-
735
- // --- TTS Functions (UPDATED FOR SPEED CYCLE) ---
736
- let currentAudio = null;
737
- let currentPlayingButton = null; // Stores the currently active *play/pause* button
738
- const playIconSVG = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z"></path></svg>`;
739
- const pauseIconSVG = `<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path d="M5.75 4.75a.75.75 0 00-.75.75v9.5c0 .414.336.75.75.75h1.5a.75.75 0 00.75-.75v-9.5a.75.75 0 00-.75-.75h-1.5zm6.5 0a.75.75 0 00-.75.75v9.5c0 .414.336.75.75.75h1.5a.75.75 0 00.75-.75v-9.5a.75.75 0 00-.75-.75h-1.5z"></path></svg>`;
740
- const availableSpeeds = [1.0, 1.5, 0.75]; // Normal, Fast, Slow
741
-
742
- // --- UPDATED: addTextToSpeechControls ---
743
- function addTextToSpeechControls(messageBubble, text) {
744
- if (!text || !text.trim()) return;
745
- const ttsControls = messageBubble.querySelector('.tts-controls');
746
- if (!ttsControls || ttsControls.querySelector('.speak-btn')) return; // Avoid adding duplicates
747
-
748
- // Play/Pause Button
749
- const speakButton = document.createElement('button');
750
- speakButton.className = 'speak-btn mt-2 px-3 py-1.5 bg-blue-700 text-white rounded-full text-xs font-medium hover:bg-blue-800 transition-colors flex items-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed';
751
- speakButton.title = 'Listen to this message';
752
- speakButton.innerHTML = `${playIconSVG} <span>Listen</span>`;
753
- speakButton.setAttribute('data-current-speed', '1.0'); // Store current speed
754
- ttsControls.appendChild(speakButton);
755
- speakButton.addEventListener('click', () => handleTTS(text, speakButton));
756
-
757
- // Speed Cycle Button
758
- const speedButton = document.createElement('button');
759
- speedButton.className = 'speed-cycle-btn'; // Use new class for styling
760
- speedButton.title = 'Cycle playback speed';
761
- speedButton.textContent = 'Speed: 1x';
762
- speedButton.setAttribute('data-speeds', JSON.stringify(availableSpeeds)); // Store speeds
763
- ttsControls.appendChild(speedButton);
764
- speedButton.addEventListener('click', () => cycleSpeed(speedButton, speakButton));
765
- }
766
-
767
- // --- NEW: cycleSpeed ---
768
- function cycleSpeed(speedBtn, speakBtn) {
769
- const speeds = JSON.parse(speedBtn.getAttribute('data-speeds'));
770
- let currentSpeed = parseFloat(speakBtn.getAttribute('data-current-speed'));
771
- let currentIndex = speeds.indexOf(currentSpeed);
772
-
773
- // Find next speed index, looping back to 0
774
- let nextIndex = (currentIndex + 1) % speeds.length;
775
- let nextSpeed = speeds[nextIndex];
776
-
777
- // Update speak button's data attribute and speed button's text
778
- speakBtn.setAttribute('data-current-speed', nextSpeed.toString());
779
- speedBtn.textContent = `Speed: ${nextSpeed}x`;
780
-
781
- // If audio is currently playing and this is the active button, update playback rate
782
- if (currentAudio && !currentAudio.paused && speakBtn === currentPlayingButton) {
783
- currentAudio.playbackRate = nextSpeed;
784
- }
785
- }
786
-
787
- // --- UPDATED: handleTTS ---
788
- async function handleTTS(text, button) {
789
- if (!text || !text.trim()) return;
790
-
791
- // *** Get speed from the button's data attribute ***
792
- const selectedSpeed = parseFloat(button.getAttribute('data-current-speed')) || 1.0;
793
-
794
- if (button === currentPlayingButton) { // If clicking the currently active play/pause button
795
- if (currentAudio && !currentAudio.paused) { // If playing, pause it
796
- currentAudio.pause();
797
- button.innerHTML = `${playIconSVG} <span>Listen</span>`;
798
- } else if (currentAudio && currentAudio.paused) { // If paused, resume it
799
- currentAudio.playbackRate = selectedSpeed; // Ensure speed is set on resume
800
- currentAudio.play().catch(e => {console.error("Audio resume error:", e); resetAllSpeakButtons();});
801
- button.innerHTML = `${pauseIconSVG} <span>Pause</span>`;
802
- }
803
- return;
804
- }
805
-
806
- // If clicking a new play button (or the first time)
807
- resetAllSpeakButtons(); // Stop any other audio
808
- currentPlayingButton = button; // Mark this button as active
809
- button.innerHTML = `<div class="tts-button-loader mr-1"></div> <span>Loading...</span>`;
810
- button.disabled = true;
811
- // Disable the corresponding speed button while loading
812
- const speedBtn = button.parentElement.querySelector('.speed-cycle-btn');
813
- if(speedBtn) speedBtn.disabled = true;
814
-
815
-
816
- try {
817
- const response = await fetch('/tts', {
818
- method: 'POST',
819
- headers: { 'Content-Type': 'application/json' },
820
- body: JSON.stringify({ text: text })
821
- });
822
- if (!response.ok) throw new Error(`TTS generation failed (${response.status})`);
823
- const blob = await response.blob();
824
- if (!blob || blob.size === 0) throw new Error("Received empty audio blob.");
825
-
826
- const audioUrl = URL.createObjectURL(blob);
827
- currentAudio = new Audio(audioUrl);
828
-
829
- // *** Set the playback speed HERE ***
830
- currentAudio.playbackRate = selectedSpeed;
831
-
832
- await currentAudio.play();
833
- button.innerHTML = `${pauseIconSVG} <span>Pause</span>`;
834
- button.disabled = false;
835
- // Re-enable the speed button
836
- if(speedBtn) speedBtn.disabled = false;
837
-
838
-
839
- currentAudio.onended = () => {
840
- // Only reset if this button was the one playing
841
- if (button === currentPlayingButton) resetAllSpeakButtons();
842
- };
843
- currentAudio.onerror = (e) => {
844
- console.error('Audio object error:', e);
845
- alert('Error playing audio.');
846
- resetAllSpeakButtons();
847
- };
848
-
849
- } catch (error) {
850
- console.error('TTS Handling Error:', error);
851
- alert(`Failed to play audio: ${error.message}`);
852
- resetAllSpeakButtons(); // Reset on error
853
- }
854
- }
855
-
856
- // --- UPDATED: resetAllSpeakButtons ---
857
- function resetAllSpeakButtons() {
858
- document.querySelectorAll('.speak-btn').forEach(btn => {
859
- btn.innerHTML = `${playIconSVG} <span>Listen</span>`;
860
- btn.disabled = false;
861
- btn.setAttribute('data-current-speed', '1.0'); // Reset speed attribute
862
- });
863
- document.querySelectorAll('.speed-cycle-btn').forEach(btn => {
864
- btn.textContent = 'Speed: 1x'; // Reset speed button text
865
  btn.disabled = false;
866
- });
867
-
868
- if (currentAudio) {
869
- currentAudio.pause();
870
- currentAudio.onended = null; // Clean up listeners
871
- currentAudio.onerror = null;
872
- currentAudio = null;
873
- }
874
- currentPlayingButton = null;
875
- }
876
-
877
- // Remove the resetSpecificButton function if it exists, it's integrated now.
878
- // --- End of TTS Functions ---
879
-
880
- // ... (keep the optional highlight.js part if you have it)
881
-
882
- });
883
- </script>
884
- </body>
885
- </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>CogniChat - Chat with your Documents</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&family=Roboto:wght@400;500&display=swap" rel="stylesheet">
11
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
+ <style>
13
+ :root {
14
+ --background: #f0f4f9;
15
+ --foreground: #1f1f1f;
16
+ --primary: #1a73e8;
17
+ --primary-hover: #1867cf;
18
+ --card: #ffffff;
19
+ --card-border: #dadce0;
20
+ --input-bg: #e8f0fe;
21
+ --user-bubble: #d9e7ff;
22
+ --bot-bubble: #f1f3f4;
23
+ }
24
+
25
+ /* Dark mode styles */
26
+ .dark {
27
+ --background: #202124;
28
+ --foreground: #e8eaed;
29
+ --primary: #8ab4f8;
30
+ --primary-hover: #99bdfa;
31
+ --card: #303134;
32
+ --card-border: #5f6368;
33
+ --input-bg: #303134;
34
+ --user-bubble: #3c4043;
35
+ --bot-bubble: #3c4043;
36
+ }
37
+
38
+ body {
39
+ font-family: 'Google Sans', 'Roboto', sans-serif;
40
+ background-color: var(--background);
41
+ color: var(--foreground);
42
+ overflow: hidden;
43
+ }
44
+
45
+ #chat-window::-webkit-scrollbar { width: 8px; }
46
+ #chat-window::-webkit-scrollbar-track { background: transparent; }
47
+ #chat-window::-webkit-scrollbar-thumb { background-color: #bdc1c6; border-radius: 20px; }
48
+ .dark #chat-window::-webkit-scrollbar-thumb { background-color: #5f6368; }
49
+
50
+ .drop-zone--over {
51
+ border-color: var(--primary);
52
+ box-shadow: 0 0 15px rgba(26, 115, 232, 0.3);
53
+ }
54
+
55
+ /* Loading Spinner */
56
+ .loader {
57
+ width: 48px;
58
+ height: 48px;
59
+ border: 3px solid var(--card-border);
60
+ border-radius: 50%;
61
+ display: inline-block;
62
+ position: relative;
63
+ box-sizing: border-box;
64
+ animation: rotation 1s linear infinite;
65
+ }
66
+ .loader::after {
67
+ content: '';
68
+ box-sizing: border-box;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  position: absolute;
70
+ left: 50%;
71
+ top: 50%;
72
+ transform: translate(-50%, -50%);
73
+ width: 56px;
74
+ height: 56px;
75
+ border-radius: 50%;
76
+ border: 3px solid;
77
+ border-color: var(--primary) transparent;
78
+ }
79
+
80
+ @keyframes rotation {
81
+ 0% { transform: rotate(0deg); }
82
+ 100% { transform: rotate(360deg); }
83
+ }
84
+
85
+ /* Typing Indicator Animation */
86
+ .typing-indicator span {
87
+ height: 10px;
88
+ width: 10px;
89
+ background-color: #9E9E9E;
90
+ border-radius: 50%;
91
+ display: inline-block;
92
+ animation: bounce 1.4s infinite ease-in-out both;
93
+ }
94
+ .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
95
+ .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
96
+ @keyframes bounce {
97
+ 0%, 80%, 100% { transform: scale(0); }
98
+ 40% { transform: scale(1.0); }
99
+ }
100
+
101
+ /* Enhanced Markdown Styling for better readability and aesthetics */
102
+ .markdown-content p {
103
+ margin-bottom: 1rem;
104
+ line-height: 1.75;
105
+ }
106
+ .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4 {
107
+ font-family: 'Google Sans', sans-serif;
108
+ font-weight: 700;
109
+ margin-top: 1.75rem;
110
+ margin-bottom: 1rem;
111
+ line-height: 1.3;
112
+ }
113
+ .markdown-content h1 { font-size: 1.75em; border-bottom: 1px solid var(--card-border); padding-bottom: 0.5rem; }
114
+ .markdown-content h2 { font-size: 1.5em; }
115
+ .markdown-content h3 { font-size: 1.25em; }
116
+ .markdown-content h4 { font-size: 1.1em; }
117
+ .markdown-content ul, .markdown-content ol {
118
+ padding-left: 1.75rem;
119
+ margin-bottom: 1rem;
120
+ }
121
+ .markdown-content li {
122
+ margin-bottom: 0.5rem;
123
+ }
124
+ .dark .markdown-content ul > li::marker { color: var(--primary); }
125
+ .markdown-content ul > li::marker { color: var(--primary); }
126
+ .markdown-content a {
127
+ color: var(--primary);
128
+ text-decoration: none;
129
+ font-weight: 500;
130
+ border-bottom: 1px solid transparent;
131
+ transition: all 0.2s ease-in-out;
132
+ }
133
+ .markdown-content a:hover {
134
+ border-bottom-color: var(--primary-hover);
135
+ }
136
+ .markdown-content blockquote {
137
+ margin: 1.5rem 0;
138
+ padding-left: 1.5rem;
139
+ border-left: 4px solid var(--card-border);
140
+ color: #6c757d;
141
+ font-style: italic;
142
+ }
143
+ .dark .markdown-content blockquote {
144
+ color: #adb5bd;
145
+ }
146
+ .markdown-content hr {
147
+ border: none;
148
+ border-top: 1px solid var(--card-border);
149
+ margin: 2rem 0;
150
+ }
151
+ .markdown-content table {
152
+ width: 100%;
153
+ border-collapse: collapse;
154
+ margin: 1.5rem 0;
155
+ font-size: 0.9em;
156
+ box-shadow: 0 1px 3px rgba(0,0,0,0.05);
157
+ border-radius: 8px;
158
+ overflow: hidden;
159
+ }
160
+ .markdown-content th, .markdown-content td {
161
  border: 1px solid var(--card-border);
162
+ padding: 0.75rem 1rem;
163
+ text-align: left;
164
+ }
165
+ .markdown-content th {
166
+ background-color: var(--bot-bubble);
167
+ font-weight: 500;
168
+ }
169
+ .markdown-content code {
170
+ background-color: rgba(0,0,0,0.05);
171
+ padding: 0.2rem 0.4rem;
172
+ border-radius: 0.25rem;
173
+ font-family: 'Roboto Mono', monospace;
174
+ font-size: 0.9em;
175
+ }
176
+ .dark .markdown-content code {
177
+ background-color: rgba(255,255,255,0.1);
178
+ }
179
+ .markdown-content pre {
180
+ position: relative;
181
+ background-color: #f8f9fa;
182
+ border: 1px solid var(--card-border);
183
+ border-radius: 0.5rem;
184
+ margin-bottom: 1rem;
185
+ }
186
+ .dark .markdown-content pre {
187
+ background-color: #2e2f32;
188
+ }
189
+ .markdown-content pre code {
190
+ background: none;
191
+ padding: 1rem;
192
+ display: block;
193
+ overflow-x: auto;
194
+ }
195
+ .markdown-content pre .copy-code-btn {
196
+ position: absolute;
197
+ top: 0.5rem;
198
+ right: 0.5rem;
199
+ background-color: #e8eaed;
200
+ border: 1px solid #dadce0;
201
+ color: #5f6368;
202
+ padding: 0.3rem 0.6rem;
203
+ border-radius: 0.25rem;
204
+ cursor: pointer;
205
+ opacity: 0;
206
+ transition: opacity 0.2s;
207
  font-size: 0.8em;
208
+ }
209
+ .dark .markdown-content pre .copy-code-btn {
210
+ background-color: #3c4043;
211
+ border-color: #5f6368;
212
+ color: #e8eaed;
213
+ }
214
+ .markdown-content pre:hover .copy-code-btn {
215
+ opacity: 1;
216
+ }
217
+
218
+ /* Spinner for the TTS button */
219
+ .tts-button-loader {
220
+ width: 16px;
221
+ height: 16px;
222
+ border: 2px solid currentColor; /* Use button's text color */
223
+ border-radius: 50%;
224
+ display: inline-block;
225
+ box-sizing: border-box;
226
+ animation: rotation 0.8s linear infinite;
227
+ border-bottom-color: transparent; /* Makes it a half circle spinner */
228
+ }
229
+ </style>
230
+ </head>
231
+ <body class="w-screen h-screen dark">
232
+ <main id="main-content" class="h-full flex flex-col transition-opacity duration-500">
233
+ <div id="chat-container" class="hidden flex-1 flex flex-col w-full mx-auto overflow-hidden">
234
+ <header class="text-center p-4 border-b border-[var(--card-border)] flex-shrink-0">
235
+ <h1 class="text-xl font-medium">Chat with your Docs</h1>
236
+ <p id="chat-filename" class="text-xs text-gray-500 dark:text-gray-400 mt-1"></p>
237
+ </header>
238
+ <div id="chat-window" class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-10">
239
+ <div id="chat-content" class="max-w-4xl mx-auto space-y-8">
240
+ </div>
241
+ </div>
242
+ <div class="p-4 flex-shrink-0 bg-[var(--background)] border-t border-[var(--card-border)]">
243
+ <form id="chat-form" class="max-w-4xl mx-auto bg-[var(--card)] rounded-full p-2 flex items-center shadow-sm border border-transparent focus-within:border-[var(--primary)] transition-colors">
244
+ <input type="text" id="chat-input" placeholder="Ask a question about your documents..." class="flex-grow bg-transparent focus:outline-none px-4 text-sm" autocomplete="off">
245
+ <button type="submit" id="chat-submit-btn" class="bg-[var(--primary)] hover:bg-[var(--primary-hover)] text-white p-2 rounded-full transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-500" title="Send">
246
+ <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.49941 11.5556L11.555 3.5L12.4438 4.38889L6.27721 10.5556H21.9994V11.5556H6.27721L12.4438 17.7222L11.555 18.6111L3.49941 10.5556V11.5556Z" transform="rotate(180, 12.7497, 11.0556)" fill="currentColor"></path></svg>
247
+ </button>
248
+ </form>
249
+ </div>
250
+ </div>
251
+
252
+ <div id="upload-container" class="flex-1 flex flex-col items-center justify-center p-8 transition-opacity duration-300">
253
+ <div class="text-center">
254
+ <h1 class="text-5xl font-medium mb-4">Upload docs to chat</h1>
255
+ <div id="drop-zone" class="w-full max-w-lg text-center border-2 border-dashed border-[var(--card-border)] rounded-2xl p-10 transition-all duration-300 cursor-pointer bg-[var(--card)] hover:border-[var(--primary)]">
256
+ <input id="file-upload" type="file" class="hidden" accept=".pdf,.txt,.docx,.jpg,.jpeg,.png" multiple title="input">
257
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" ><path stroke-linecap="round" stroke-linejoin="round" d="M12 16.5V9.75m0 0l3-3m-3 3l-3 3M6.75 19.5a4.5 4.5 0 01-1.41-8.775 5.25 5.25 0 0110.233-2.33 3 3 0 013.758 3.848A3.752 3.752 0 0118 19.5H6.75z"></path></svg>
258
+ <p class="mt-4 text-sm font-medium">Drag & drop files or click to upload</p>
259
+ <p id="file-name" class="mt-2 text-xs text-gray-500"></p>
260
+ </div>
261
+ </div>
262
+ </div>
263
+
264
+ <div id="loading-overlay" class="hidden fixed inset-0 bg-[var(--background)] bg-opacity-80 backdrop-blur-sm flex flex-col items-center justify-center z-50 text-center p-4">
265
+ <div class="loader"></div>
266
+ <p id="loading-text" class="mt-6 text-sm font-medium"></p>
267
+ <p id="loading-subtext" class="mt-2 text-xs text-gray-500 dark:text-gray-400"></p>
268
+ </div>
269
+ </main>
270
+
271
+ <script>
272
+ document.addEventListener('DOMContentLoaded', () => {
273
+ const uploadContainer = document.getElementById('upload-container');
274
+ const chatContainer = document.getElementById('chat-container');
275
+ const dropZone = document.getElementById('drop-zone');
276
+ const fileUploadInput = document.getElementById('file-upload');
277
+ const fileNameSpan = document.getElementById('file-name');
278
+ const loadingOverlay = document.getElementById('loading-overlay');
279
+ const loadingText = document.getElementById('loading-text');
280
+ const loadingSubtext = document.getElementById('loading-subtext');
281
+
282
+ const chatForm = document.getElementById('chat-form');
283
+ const chatInput = document.getElementById('chat-input');
284
+ const chatSubmitBtn = document.getElementById('chat-submit-btn');
285
+ const chatWindow = document.getElementById('chat-window');
286
+ const chatContent = document.getElementById('chat-content');
287
+ const chatFilename = document.getElementById('chat-filename');
288
+
289
+ let sessionId = null;
290
+ const storedSessionId = sessionStorage.getItem('cognichat_session_id');
291
+ if (storedSessionId) {
292
+ sessionId = storedSessionId;
293
+ console.debug('Restored session ID from storage:', sessionId);
294
+ }
295
+
296
+ // --- File Upload Logic ---
297
+ dropZone.addEventListener('click', () => fileUploadInput.click());
298
+
299
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
300
+ dropZone.addEventListener(eventName, preventDefaults, false);
301
+ document.body.addEventListener(eventName, preventDefaults, false);
302
+ });
303
+
304
+ ['dragenter', 'dragover'].forEach(eventName => {
305
+ dropZone.addEventListener(eventName, () => dropZone.classList.add('drop-zone--over'));
306
+ });
307
+ ['dragleave', 'drop'].forEach(eventName => {
308
+ dropZone.addEventListener(eventName, () => dropZone.classList.remove('drop-zone--over'));
309
+ });
310
+
311
+ dropZone.addEventListener('drop', (e) => {
312
+ const files = e.dataTransfer.files;
313
+ if (files.length > 0) handleFiles(files);
314
+ });
315
+
316
+ fileUploadInput.addEventListener('change', (e) => {
317
+ if (e.target.files.length > 0) handleFiles(e.target.files);
318
+ });
319
+
320
+ function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
321
+
322
+ async function handleFiles(files) {
323
+ const formData = new FormData();
324
+ let fileNames = [];
325
+ for (const file of files) {
326
+ formData.append('file', file);
327
+ fileNames.push(file.name);
328
+ }
329
+
330
+ fileNameSpan.textContent = `Selected: ${fileNames.join(', ')}`;
331
+ await uploadAndProcessFiles(formData, fileNames);
332
+ }
333
+
334
+ async function uploadAndProcessFiles(formData, fileNames) {
335
+ loadingOverlay.classList.remove('hidden');
336
+ loadingText.textContent = `Processing ${fileNames.length} document(s)...`;
337
+ loadingSubtext.textContent = "🤓Creating a knowledge base may take a minute or two. So please hold on tight";
338
+
339
+ try {
340
+ const response = await fetch('/upload', { method: 'POST', body: formData });
341
+ const result = await response.json();
342
+
343
+ if (!response.ok) throw new Error(result.message || 'Unknown error occurred.');
344
+ if (result.session_id) {
345
+ sessionId = result.session_id;
346
+ sessionStorage.setItem('cognichat_session_id', sessionId);
347
+ console.debug('Stored session ID from upload:', sessionId);
348
+ } else {
349
+ console.warn('Upload response missing session_id field.');
350
+ }
351
+
352
+ chatFilename.textContent = `Chatting with: ${result.filename}`;
353
+ uploadContainer.classList.add('hidden');
354
+ chatContainer.classList.remove('hidden');
355
+ appendMessage("I've analyzed your documents. What would you like to know?", "bot");
356
+
357
+ } catch (error) {
358
+ console.error('Upload error:', error);
359
+ alert(`Error: ${error.message}`);
360
+ } finally {
361
+ loadingOverlay.classList.add('hidden');
362
+ loadingSubtext.textContent = '';
363
+ fileNameSpan.textContent = '';
364
+ fileUploadInput.value = '';
365
+ }
366
+ }
367
+
368
+ // --- Chat Logic ---
369
+ chatForm.addEventListener('submit', async (e) => {
370
+ e.preventDefault();
371
+ const question = chatInput.value.trim();
372
+ if (!question) return;
373
+
374
+ appendMessage(question, 'user');
375
+ chatInput.value = '';
376
+ chatInput.disabled = true;
377
+ chatSubmitBtn.disabled = true;
378
+
379
+ const typingIndicator = showTypingIndicator();
380
+ let botMessageContainer = null;
381
+ let contentDiv = null;
382
+
383
+ try {
384
+ const requestBody = { question: question };
385
+ if (sessionId) {
386
+ requestBody.session_id = sessionId;
387
+ }
388
+
389
+ const response = await fetch('/chat', {
390
+ method: 'POST',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify(requestBody),
393
+ });
394
+
395
+ if (!response.ok) throw new Error(`Server error: ${response.statusText}`);
396
+
397
+ // ============================ MODIFICATION START ==============================
398
+ // Parse the JSON response instead of reading a stream
399
+ const result = await response.json();
400
+ const answer = result.answer; // Extract the 'answer' field
401
+
402
+ if (!answer) {
403
+ throw new Error("Received an empty or invalid response from the server.");
404
+ }
405
+
406
+ typingIndicator.remove();
407
+ botMessageContainer = appendMessage('', 'bot');
408
+ contentDiv = botMessageContainer.querySelector('.markdown-content');
409
+
410
+ // Use the extracted answer for rendering
411
+ contentDiv.innerHTML = marked.parse(answer);
412
+ contentDiv.querySelectorAll('pre').forEach(addCopyButton);
413
+ scrollToBottom(); // Scroll after content is added
414
+
415
+ // Use the extracted answer for TTS
416
+ addTextToSpeechControls(botMessageContainer, answer);
417
+ // ============================ MODIFICATION END ==============================
418
+
419
+ } catch (error) {
420
+ console.error('Chat error:', error);
421
+ if (typingIndicator) typingIndicator.remove();
422
+ if (contentDiv) {
423
+ contentDiv.innerHTML = `<p class="text-red-500">Error: ${error.message}</p>`;
424
+ } else {
425
+ appendMessage(`Error: ${error.message}`, 'bot');
426
+ }
427
+ } finally {
428
+ chatInput.disabled = false;
429
+ chatSubmitBtn.disabled = false;
430
+ chatInput.focus();
431
+ }
432
+ });
433
+
434
+ // --- UI Helper Functions ---
435
+
436
+ function appendMessage(text, sender) {
437
+ const messageWrapper = document.createElement('div');
438
+ messageWrapper.className = `flex items-start gap-4`;
439
+
440
+ const iconSVG = sender === 'user'
441
+ ? `<div class="bg-blue-100 dark:bg-gray-700 p-2.5 rounded-full flex-shrink-0 mt-1"><svg class="w-5 h-5 text-blue-600 dark:text-blue-300" viewBox="0 0 24 24"><path fill="currentColor" d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"></path></svg></div>`
442
+ : `<div class="bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0 mt-1 text-xl flex items-center justify-center w-10 h-10">✨</div>`;
443
+
444
+ const messageBubble = document.createElement('div');
445
+ messageBubble.className = `flex-1 pt-1`;
446
+
447
+ const senderName = document.createElement('p');
448
+ senderName.className = 'font-medium text-sm mb-1';
449
+ senderName.textContent = sender === 'user' ? 'You' : 'CogniChat';
450
+
451
+ const contentDiv = document.createElement('div');
452
+ contentDiv.className = 'text-base markdown-content';
453
+ // Only parse if text is not empty
454
+ if (text) {
455
+ contentDiv.innerHTML = marked.parse(text);
456
+ }
457
+
458
+ const controlsContainer = document.createElement('div');
459
+ controlsContainer.className = 'tts-controls mt-2';
460
+
461
+ messageBubble.appendChild(senderName);
462
+ messageBubble.appendChild(contentDiv);
463
+ messageBubble.appendChild(controlsContainer);
464
+ messageWrapper.innerHTML = iconSVG;
465
+ messageWrapper.appendChild(messageBubble);
466
+
467
+ chatContent.appendChild(messageWrapper);
468
+ scrollToBottom();
469
+
470
+ return messageBubble;
471
+ }
472
+
473
+ function showTypingIndicator() {
474
+ const indicatorWrapper = document.createElement('div');
475
+ indicatorWrapper.className = `flex items-start gap-4`;
476
+ indicatorWrapper.id = 'typing-indicator';
477
+
478
+ const iconSVG = `<div class="bg-gray-200 dark:bg-gray-700 rounded-full flex-shrink-0 mt-1 text-xl flex items-center justify-center w-10 h-10">✨</div>`;
479
+
480
+ const messageBubble = document.createElement('div');
481
+ messageBubble.className = 'flex-1 pt-1';
482
+
483
+ const senderName = document.createElement('p');
484
+ senderName.className = 'font-medium text-sm mb-1';
485
+ senderName.textContent = 'CogniChat is thinking...';
486
+
487
+ const indicator = document.createElement('div');
488
+ indicator.className = 'typing-indicator';
489
+ indicator.innerHTML = '<span></span><span></span><span></span>';
490
+
491
+ messageBubble.appendChild(senderName);
492
+ messageBubble.appendChild(indicator);
493
+ indicatorWrapper.innerHTML = iconSVG;
494
+ indicatorWrapper.appendChild(messageBubble);
495
+
496
+ chatContent.appendChild(indicatorWrapper);
497
+ scrollToBottom();
498
+
499
+ return indicatorWrapper;
500
+ }
501
+
502
+ function scrollToBottom() {
503
+ chatWindow.scrollTo({
504
+ top: chatWindow.scrollHeight,
505
+ behavior: 'smooth'
506
+ });
507
+ }
508
+
509
+ function addCopyButton(pre) {
510
+ const button = document.createElement('button');
511
+ button.className = 'copy-code-btn';
512
+ button.textContent = 'Copy';
513
+ pre.appendChild(button);
514
+
515
+ button.addEventListener('click', () => {
516
+ const code = pre.querySelector('code').innerText;
517
+ navigator.clipboard.writeText(code).then(() => {
518
+ button.textContent = 'Copied!';
519
+ setTimeout(() => button.textContent = 'Copy', 2000);
520
+ });
521
+ });
522
+ }
523
+
524
+ // --- Text-to-Speech Logic ---
525
+ let currentAudio = null;
526
+ let currentPlayingButton = null;
527
+
528
+ const playIconSVG = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>`;
529
+ const pauseIconSVG = `<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
530
+
531
+
532
+ function addTextToSpeechControls(messageBubble, text) {
533
+ const ttsControls = messageBubble.querySelector('.tts-controls');
534
+ if (text.trim().length > 0) {
535
+ const speakButton = document.createElement('button');
536
+ speakButton.className = 'speak-btn px-4 py-2 bg-blue-700 text-white rounded-full text-sm font-medium hover:bg-blue-800 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed';
537
+ speakButton.title = 'Listen to this message';
538
+ speakButton.setAttribute('data-state', 'play');
539
+ speakButton.innerHTML = `${playIconSVG} <span>Play</span>`;
540
+ ttsControls.appendChild(speakButton);
541
+ speakButton.addEventListener('click', () => handleTTS(text, speakButton));
542
+ }
543
+ }
544
+
545
+ async function handleTTS(text, button) {
546
+ if (button === currentPlayingButton) {
547
+ if (currentAudio && !currentAudio.paused) {
548
+ currentAudio.pause();
549
+ button.setAttribute('data-state', 'paused');
550
+ button.innerHTML = `${playIconSVG} <span>Play</span>`;
551
+ } else if (currentAudio && currentAudio.paused) {
552
+ currentAudio.play();
553
+ button.setAttribute('data-state', 'playing');
554
+ button.innerHTML = `${pauseIconSVG} <span>Pause</span>`;
555
+ }
556
+ return;
557
+ }
558
+
559
+ resetAllSpeakButtons();
560
+
561
+ currentPlayingButton = button;
562
+ button.setAttribute('data-state', 'loading');
563
+ button.innerHTML = `<div class="tts-button-loader"></div> <span>Loading...</span>`;
564
+ button.disabled = true;
565
+
566
+ try {
567
+ const response = await fetch('/tts', {
568
+ method: 'POST',
569
+ headers: { 'Content-Type': 'application/json' },
570
+ body: JSON.stringify({ text: text })
571
+ });
572
+ if (!response.ok) throw new Error('Failed to generate audio.');
573
+
574
+ const blob = await response.blob();
575
+ const audioUrl = URL.createObjectURL(blob);
576
+ currentAudio = new Audio(audioUrl);
577
+ currentAudio.play();
578
+
579
+ button.setAttribute('data-state', 'playing');
580
+ button.innerHTML = `${pauseIconSVG} <span>Pause</span>`;
581
+
582
+ currentAudio.onended = () => {
583
+ button.setAttribute('data-state', 'play');
584
+ button.innerHTML = `${playIconSVG} <span>Play</span>`;
585
+ currentAudio = null;
586
+ currentPlayingButton = null;
587
+ };
588
+
589
+ } catch (error) {
590
+ console.error('TTS Error:', error);
591
+ button.setAttribute('data-state', 'error');
592
+ button.innerHTML = `${playIconSVG} <span>Error</span>`;
593
+ alert('Failed to play audio. Please try again.');
594
+ resetAllSpeakButtons();
595
+ } finally {
596
+ button.disabled = false;
597
+ }
598
+ }
599
+
600
+ function resetAllSpeakButtons() {
601
+ document.querySelectorAll('.speak-btn').forEach(btn => {
602
+ btn.setAttribute('data-state', 'play');
603
+ btn.innerHTML = `${playIconSVG} <span>Play</span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  btn.disabled = false;
605
+ });
606
+ if (currentAudio) {
607
+ currentAudio.pause();
608
+ currentAudio = null;
609
+ }
610
+ currentPlayingButton = null;
611
+ }
612
+ });
613
+ </script>
614
+ </body>
615
+ </html>