Files changed (1) hide show
  1. static/index.html +510 -570
static/index.html CHANGED
@@ -1,571 +1,511 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Multimodal RAG • AI Research Assistant</title>
8
- <link rel="preconnect" href="https://fonts.googleapis.com">
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link
11
- href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap"
12
- rel="stylesheet">
13
- <style>
14
- :root {
15
- --primary: #3b82f6;
16
- --primary-glow: rgba(59, 130, 246, 0.5);
17
- --bg-dark: #0f172a;
18
- --bg-card: #1e293b;
19
- --text-main: #f8fafc;
20
- --text-muted: #94a3b8;
21
- --border-color: #334155;
22
- --accent-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
23
- }
24
-
25
- * {
26
- box-sizing: border-box;
27
- margin: 0;
28
- padding: 0;
29
- }
30
-
31
- body {
32
- font-family: 'Outfit', sans-serif;
33
- background-color: var(--bg-dark);
34
- color: var(--text-main);
35
- min-height: 100vh;
36
- line-height: 1.6;
37
- background-image:
38
- radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 20%),
39
- radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 20%);
40
- }
41
-
42
- .container {
43
- max-width: 1200px;
44
- margin: 0 auto;
45
- padding: 2rem;
46
- display: flex;
47
- flex-direction: column;
48
- gap: 2rem;
49
- }
50
-
51
- header {
52
- text-align: center;
53
- padding: 4rem 0 2rem;
54
- animation: fadeInDown 0.8s ease-out;
55
- }
56
-
57
- h1 {
58
- font-family: 'Space Grotesk', sans-serif;
59
- font-size: 3.5rem;
60
- font-weight: 700;
61
- background: var(--accent-gradient);
62
- -webkit-background-clip: text;
63
- -webkit-text-fill-color: transparent;
64
- margin-bottom: 0.5rem;
65
- letter-spacing: -0.02em;
66
- }
67
-
68
- .subtitle {
69
- color: var(--text-muted);
70
- font-size: 1.25rem;
71
- font-weight: 300;
72
- }
73
-
74
- /* Search Section */
75
- .search-container {
76
- max-width: 800px;
77
- margin: 0 auto;
78
- width: 100%;
79
- position: relative;
80
- z-index: 10;
81
- }
82
-
83
- .input-group {
84
- position: relative;
85
- display: flex;
86
- gap: 1rem;
87
- background: rgba(30, 41, 59, 0.7);
88
- padding: 0.5rem;
89
- border-radius: 1rem;
90
- border: 1px solid var(--border-color);
91
- backdrop-filter: blur(12px);
92
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
93
- transition: all 0.3s ease;
94
- }
95
-
96
- .input-group:focus-within {
97
- border-color: var(--primary);
98
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
99
- transform: translateY(-2px);
100
- }
101
-
102
- input[type="text"] {
103
- flex: 1;
104
- background: transparent;
105
- border: none;
106
- padding: 1rem 1.5rem;
107
- font-size: 1.1rem;
108
- color: white;
109
- font-family: 'Outfit', sans-serif;
110
- width: 100%;
111
- }
112
-
113
- input[type="text"]:focus {
114
- outline: none;
115
- }
116
-
117
- input[type="text"]::placeholder {
118
- color: #64748b;
119
- }
120
-
121
- button#searchBtn {
122
- background: var(--accent-gradient);
123
- color: white;
124
- border: none;
125
- padding: 0 2rem;
126
- border-radius: 0.75rem;
127
- font-weight: 600;
128
- font-size: 1rem;
129
- cursor: pointer;
130
- transition: opacity 0.2s;
131
- display: flex;
132
- align-items: center;
133
- gap: 0.5rem;
134
- }
135
-
136
- button#searchBtn:hover {
137
- opacity: 0.9;
138
- }
139
-
140
- button#searchBtn:disabled {
141
- opacity: 0.5;
142
- cursor: not-allowed;
143
- }
144
-
145
- /* Loading State */
146
- #loading {
147
- display: none;
148
- text-align: center;
149
- padding: 2rem;
150
- }
151
-
152
- .spinner {
153
- width: 40px;
154
- height: 40px;
155
- border: 3px solid rgba(59, 130, 246, 0.3);
156
- border-radius: 50%;
157
- border-top-color: var(--primary);
158
- animation: spin 1s linear infinite;
159
- margin: 0 auto 1rem;
160
- }
161
-
162
- @keyframes spin {
163
- to {
164
- transform: rotate(360deg);
165
- }
166
- }
167
-
168
- /* Results Area */
169
- #results {
170
- display: none;
171
- /* Hidden by default */
172
- max-width: 1000px;
173
- margin: 0 auto;
174
- width: 100%;
175
- animation: fadeInUp 0.5s ease-out;
176
- }
177
-
178
- .section-title {
179
- display: flex;
180
- align-items: center;
181
- gap: 0.75rem;
182
- color: var(--text-muted);
183
- font-size: 0.9rem;
184
- text-transform: uppercase;
185
- letter-spacing: 0.05em;
186
- font-weight: 600;
187
- margin-bottom: 1rem;
188
- margin-top: 2rem;
189
- border-bottom: 1px solid var(--border-color);
190
- padding-bottom: 0.5rem;
191
- }
192
-
193
- .answer-card {
194
- background: var(--bg-card);
195
- border: 1px solid var(--border-color);
196
- padding: 2rem;
197
- border-radius: 1rem;
198
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
199
- position: relative;
200
- overflow: hidden;
201
- }
202
-
203
- .answer-text {
204
- font-size: 1.1rem;
205
- color: #e2e8f0;
206
- white-space: pre-wrap;
207
- line-height: 1.8;
208
- }
209
-
210
- /* Image Grids */
211
- .images-grid {
212
- display: grid;
213
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
214
- gap: 1.5rem;
215
- }
216
-
217
- .image-card {
218
- background: var(--bg-card);
219
- border-radius: 0.75rem;
220
- overflow: hidden;
221
- border: 1px solid var(--border-color);
222
- transition: all 0.3s ease;
223
- position: relative;
224
- group: card;
225
- }
226
-
227
- .image-card:hover {
228
- transform: translateY(-5px);
229
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
230
- border-color: var(--primary);
231
- }
232
-
233
- .image-card a {
234
- display: block;
235
- overflow: hidden;
236
- height: 180px;
237
- }
238
-
239
- .image-card img {
240
- width: 100%;
241
- height: 100%;
242
- object-fit: cover;
243
- transition: transform 0.5s ease;
244
- }
245
-
246
- .image-card:hover img {
247
- transform: scale(1.1);
248
- }
249
-
250
- .image-meta {
251
- padding: 1rem;
252
- border-top: 1px solid var(--border-color);
253
- }
254
-
255
- .image-filename {
256
- font-weight: 600;
257
- color: #f1f5f9;
258
- font-size: 0.95rem;
259
- white-space: nowrap;
260
- overflow: hidden;
261
- text-overflow: ellipsis;
262
- margin-bottom: 0.25rem;
263
- }
264
-
265
- .image-stats {
266
- display: flex;
267
- justify-content: space-between;
268
- font-size: 0.8rem;
269
- color: var(--text-muted);
270
- }
271
-
272
- /* Text Sources */
273
- .sources-grid {
274
- display: grid;
275
- gap: 1rem;
276
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
277
- }
278
-
279
- .source-card {
280
- background: rgba(30, 41, 59, 0.4);
281
- border: 1px solid var(--border-color);
282
- border-radius: 0.75rem;
283
- padding: 1.25rem;
284
- transition: background 0.2s;
285
- }
286
-
287
- .source-card:hover {
288
- background: rgba(30, 41, 59, 0.8);
289
- border-color: #475569;
290
- }
291
-
292
- .source-title,
293
- .source-link {
294
- font-weight: 600;
295
- color: var(--primary);
296
- margin-bottom: 0.5rem;
297
- font-size: 0.95rem;
298
- display: block;
299
- text-decoration: none;
300
- }
301
-
302
- .source-link:hover {
303
- text-decoration: underline;
304
- color: #60a5fa;
305
- }
306
-
307
- .source-excerpt {
308
- font-style: italic;
309
- color: #cbd5e1;
310
- font-size: 0.9rem;
311
- background: rgba(0, 0, 0, 0.2);
312
- padding: 0.75rem;
313
- border-radius: 0.5rem;
314
- border-left: 3px solid var(--primary);
315
- }
316
-
317
- .source-meta {
318
- margin-top: 0.75rem;
319
- font-size: 0.8rem;
320
- color: var(--text-muted);
321
- text-align: right;
322
- }
323
-
324
- /* Animations */
325
- @keyframes fadeInDown {
326
- from {
327
- opacity: 0;
328
- transform: translateY(-20px);
329
- }
330
-
331
- to {
332
- opacity: 1;
333
- transform: translateY(0);
334
- }
335
- }
336
-
337
- @keyframes fadeInUp {
338
- from {
339
- opacity: 0;
340
- transform: translateY(20px);
341
- }
342
-
343
- to {
344
- opacity: 1;
345
- transform: translateY(0);
346
- }
347
- }
348
-
349
- /* Scrollbar */
350
- ::-webkit-scrollbar {
351
- width: 8px;
352
- }
353
-
354
- ::-webkit-scrollbar-track {
355
- background: var(--bg-dark);
356
- }
357
-
358
- ::-webkit-scrollbar-thumb {
359
- background: var(--border-color);
360
- border-radius: 4px;
361
- }
362
-
363
- ::-webkit-scrollbar-thumb:hover {
364
- background: #475569;
365
- }
366
- </style>
367
- </head>
368
-
369
- <body>
370
- <div class="container">
371
- <header>
372
- <h1>WHEC - Chatbot</h1>
373
- <p class="subtitle">Based on documents at WHEC (Warrior Heat- and Exertion-Related Events Collaborative)
374
- <a href="https://www.hprc-online.org/resources-partners/whec" target="_blank">page</a>
375
- </p>
376
- </header>
377
-
378
- <div class="search-container">
379
- <div class="input-group">
380
- <input type="text" id="questionInput" placeholder="Ask a question about exertion-related injuries..."
381
- autocomplete="off">
382
- <button id="searchBtn" onclick="askQuestion()">
383
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
384
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
385
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
386
- </svg>
387
- Analyze
388
- </button>
389
- </div>
390
- </div>
391
-
392
- <div id="loading">
393
- <div class="spinner"></div>
394
- <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p>
395
- </div>
396
-
397
- <div id="results">
398
- <!-- ANSWER SECTION -->
399
- <div class="section-title">
400
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
401
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
402
- d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z">
403
- </path>
404
- </svg>
405
- Consolidated Intelligence
406
- </div>
407
-
408
- <div class="answer-card">
409
- <div id="answerText" class="answer-text"></div>
410
- </div>
411
-
412
- <!-- TEXT SOURCES SECTION -->
413
- <div id="textsSection" style="display: none;">
414
- <div class="section-title" style="margin-top: 3rem;">
415
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
416
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
417
- d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253">
418
- </path>
419
- </svg>
420
- Source Documents
421
- </div>
422
- <div id="textsGrid" class="sources-grid"></div>
423
- </div>
424
-
425
- <!-- IMAGES SECTION -->
426
- <div id="imagesSection" style="display: none;">
427
- <div class="section-title" style="margin-top: 3rem;">
428
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
429
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
430
- d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
431
- </path>
432
- </svg>
433
- Relevant Visual Data
434
- </div>
435
- <div id="imagesGrid" class="images-grid"></div>
436
- </div>
437
- </div>
438
- </div>
439
-
440
- <script>
441
- const input = document.getElementById('questionInput');
442
- const searchBtn = document.getElementById('searchBtn');
443
- const loading = document.getElementById('loading');
444
- const results = document.getElementById('results');
445
- const answerText = document.getElementById('answerText');
446
-
447
- const imagesSection = document.getElementById('imagesSection');
448
- const imagesGrid = document.getElementById('imagesGrid');
449
-
450
- const textsSection = document.getElementById('textsSection');
451
- const textsGrid = document.getElementById('textsGrid');
452
-
453
- // Allow Enter key to submit
454
- input.addEventListener('keypress', function (e) {
455
- if (e.key === 'Enter') {
456
- askQuestion();
457
- }
458
- });
459
-
460
- async function askQuestion() {
461
- const question = input.value.trim();
462
- if (!question) return;
463
-
464
- // UI State updates
465
- searchBtn.disabled = true;
466
- searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>';
467
- input.disabled = true;
468
- results.style.display = 'none';
469
- loading.style.display = 'block';
470
-
471
- // Reset sections
472
- imagesSection.style.display = 'none';
473
- textsSection.style.display = 'none';
474
- imagesGrid.innerHTML = '';
475
- textsGrid.innerHTML = '';
476
-
477
- try {
478
- const response = await fetch('/query', {
479
- method: 'POST',
480
- headers: { 'Content-Type': 'application/json' },
481
- body: JSON.stringify({ question: question })
482
- });
483
-
484
- if (!response.ok) throw new Error('Failed to get response');
485
-
486
- const data = await response.json();
487
-
488
- // 1. Display Answer with simple markdown replacement for bold
489
- // Very basic MD parser for bold text if any
490
- let formattedAnswer = data.answer.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
491
- answerText.innerHTML = formattedAnswer;
492
-
493
- // 2. Display Images
494
- if (data.images && data.images.length > 0) {
495
- // Sort by score descending just to be safe
496
- data.images.sort((a, b) => b.score - a.score);
497
-
498
- let imagesToShow = data.images.filter(img => img.score >= 0.3);
499
-
500
- // Fallback: if no images match threshold, show the top 1 most relevant
501
- if (imagesToShow.length === 0) {
502
- imagesToShow = [data.images[0]];
503
- }
504
-
505
- if (imagesToShow.length > 0) {
506
- imagesSection.style.display = 'block';
507
- imagesToShow.forEach(img => {
508
- const div = document.createElement('div');
509
- div.className = 'image-card';
510
- div.innerHTML = `
511
- <a href="${img.path}" target="_blank">
512
- <img src="${img.path}" alt="${img.filename}" loading="lazy">
513
- </a>
514
- <div class="image-meta">
515
- <div class="image-filename" title="${img.filename}">${img.filename}</div>
516
- <div class="image-stats">
517
- <span>Page ${img.page || '?'}</span>
518
- <span>${(img.score * 100).toFixed(0)}% Match</span>
519
- </div>
520
- </div>
521
- `;
522
- imagesGrid.appendChild(div);
523
- });
524
- }
525
- }
526
-
527
- // 3. Display Texts
528
- if (data.texts && data.texts.length > 0) {
529
- textsSection.style.display = 'block';
530
- data.texts.forEach(txt => {
531
- const div = document.createElement('div');
532
- div.className = 'source-card';
533
- const titleHtml = txt.link
534
- ? `<a href="${txt.link}" target="_blank" class="source-link">${txt.file || 'Document'}</a>`
535
- : `<span class="source-title">${txt.file || 'Document'}</span>`;
536
-
537
- const excerptHtml = txt.link
538
- ? `<a href="${txt.link}" target="_blank" style="text-decoration:none; color:inherit; display:block;">"${txt.text}..."</a>`
539
- : `"${txt.text}..."`;
540
-
541
- div.innerHTML = `
542
- <div class="source-title-wrapper">${titleHtml}</div>
543
- <div class="source-excerpt">${excerptHtml}</div>
544
- <div class="source-meta">
545
- Page ${txt.page || 'N/A'} • Similarity: ${(txt.score * 100).toFixed(0)}%
546
- </div>
547
- `;
548
- textsGrid.appendChild(div);
549
- });
550
- }
551
-
552
- results.style.display = 'block';
553
-
554
- } catch (error) {
555
- alert('Error querying system. Please try again.');
556
- console.error(error);
557
- } finally {
558
- loading.style.display = 'none';
559
- searchBtn.disabled = false;
560
- searchBtn.innerHTML = `
561
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
562
- Analyze
563
- `;
564
- input.disabled = false;
565
- input.focus();
566
- }
567
- }
568
- </script>
569
- </body>
570
-
571
  </html>
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Multimodal RAG • AI Research Assistant</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap"
12
+ rel="stylesheet">
13
+ <style>
14
+ :root {
15
+ --primary: #3b82f6;
16
+ --primary-glow: rgba(59, 130, 246, 0.5);
17
+ --bg-dark: #0f172a;
18
+ --bg-card: #1e293b;
19
+ --text-main: #f8fafc;
20
+ --text-muted: #94a3b8;
21
+ --border-color: #334155;
22
+ --accent-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
23
+ }
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ margin: 0;
28
+ padding: 0;
29
+ }
30
+
31
+ body {
32
+ font-family: 'Outfit', sans-serif;
33
+ background-color: var(--bg-dark);
34
+ color: var(--text-main);
35
+ min-height: 100vh;
36
+ line-height: 1.6;
37
+ background-image:
38
+ radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 20%),
39
+ radial-gradient(circle at 90% 80%, rgba(139, 92, 246, 0.1) 0%, transparent 20%);
40
+ }
41
+
42
+ .container {
43
+ max-width: 1200px;
44
+ margin: 0 auto;
45
+ padding: 2rem;
46
+ display: flex;
47
+ flex-direction: column;
48
+ gap: 2rem;
49
+ }
50
+
51
+ header {
52
+ text-align: center;
53
+ padding: 4rem 0 2rem;
54
+ animation: fadeInDown 0.8s ease-out;
55
+ }
56
+
57
+ h1 {
58
+ font-family: 'Space Grotesk', sans-serif;
59
+ font-size: 3.5rem;
60
+ font-weight: 700;
61
+ background: var(--accent-gradient);
62
+ -webkit-background-clip: text;
63
+ -webkit-text-fill-color: transparent;
64
+ background-clip: text;
65
+ margin-bottom: 0.5rem;
66
+ letter-spacing: -0.02em;
67
+ }
68
+
69
+ .subtitle {
70
+ color: var(--text-muted);
71
+ font-size: 1.25rem;
72
+ font-weight: 300;
73
+ }
74
+
75
+ .subtitle a {
76
+ color: var(--primary);
77
+ text-decoration: none;
78
+ }
79
+
80
+ .subtitle a:hover {
81
+ text-decoration: underline;
82
+ }
83
+
84
+ /* Search Section */
85
+ .search-container {
86
+ max-width: 800px;
87
+ margin: 0 auto;
88
+ width: 100%;
89
+ position: relative;
90
+ z-index: 10;
91
+ }
92
+
93
+ .input-group {
94
+ position: relative;
95
+ display: flex;
96
+ gap: 1rem;
97
+ background: rgba(30, 41, 59, 0.7);
98
+ padding: 0.5rem;
99
+ border-radius: 1rem;
100
+ border: 1px solid var(--border-color);
101
+ backdrop-filter: blur(12px);
102
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
103
+ transition: all 0.3s ease;
104
+ }
105
+
106
+ .input-group:focus-within {
107
+ border-color: var(--primary);
108
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
109
+ transform: translateY(-2px);
110
+ }
111
+
112
+ input[type="text"] {
113
+ flex: 1;
114
+ background: transparent;
115
+ border: none;
116
+ padding: 1rem 1.5rem;
117
+ font-size: 1.1rem;
118
+ color: white;
119
+ font-family: 'Outfit', sans-serif;
120
+ width: 100%;
121
+ }
122
+
123
+ input[type="text"]:focus {
124
+ outline: none;
125
+ }
126
+
127
+ input[type="text"]::placeholder {
128
+ color: #64748b;
129
+ }
130
+
131
+ button#searchBtn {
132
+ background: var(--accent-gradient);
133
+ color: white;
134
+ border: none;
135
+ padding: 0 2rem;
136
+ border-radius: 0.75rem;
137
+ font-weight: 600;
138
+ font-size: 1rem;
139
+ cursor: pointer;
140
+ transition: opacity 0.2s;
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 0.5rem;
144
+ }
145
+
146
+ button#searchBtn:hover {
147
+ opacity: 0.9;
148
+ }
149
+
150
+ button#searchBtn:disabled {
151
+ opacity: 0.5;
152
+ cursor: not-allowed;
153
+ }
154
+
155
+ /* Loading State */
156
+ #loading {
157
+ display: none;
158
+ text-align: center;
159
+ padding: 2rem;
160
+ }
161
+
162
+ .spinner {
163
+ width: 40px;
164
+ height: 40px;
165
+ border: 3px solid rgba(59, 130, 246, 0.3);
166
+ border-radius: 50%;
167
+ border-top-color: var(--primary);
168
+ animation: spin 1s linear infinite;
169
+ margin: 0 auto 1rem;
170
+ }
171
+
172
+ @keyframes spin {
173
+ to {
174
+ transform: rotate(360deg);
175
+ }
176
+ }
177
+
178
+ /* Chat History */
179
+ #chatHistory {
180
+ max-width: 1000px;
181
+ margin: 0 auto;
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 2rem;
185
+ }
186
+
187
+ .message-block {
188
+ border: 1px solid var(--border-color);
189
+ border-radius: 1rem;
190
+ padding: 1.5rem;
191
+ background: var(--bg-card);
192
+ animation: fadeInUp 0.4s ease-out;
193
+ }
194
+
195
+ .user-question {
196
+ margin-bottom: 1rem;
197
+ color: var(--text-main);
198
+ }
199
+
200
+ .user-question strong {
201
+ color: var(--primary);
202
+ }
203
+
204
+ .assistant-answer {
205
+ margin-bottom: 1rem;
206
+ line-height: 1.8;
207
+ }
208
+
209
+ .assistant-answer strong {
210
+ color: #8b5cf6;
211
+ }
212
+
213
+ /* Image Grids */
214
+ .images-grid {
215
+ display: grid;
216
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
217
+ gap: 1.5rem;
218
+ margin-top: 1rem;
219
+ }
220
+
221
+ .image-card {
222
+ background: rgba(30, 41, 59, 0.6);
223
+ border-radius: 0.75rem;
224
+ overflow: hidden;
225
+ border: 1px solid var(--border-color);
226
+ transition: all 0.3s ease;
227
+ }
228
+
229
+ .image-card:hover {
230
+ transform: translateY(-5px);
231
+ box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
232
+ border-color: var(--primary);
233
+ }
234
+
235
+ .image-card a {
236
+ display: block;
237
+ overflow: hidden;
238
+ height: 180px;
239
+ }
240
+
241
+ .image-card img {
242
+ width: 100%;
243
+ height: 100%;
244
+ object-fit: cover;
245
+ transition: transform 0.5s ease;
246
+ }
247
+
248
+ .image-card:hover img {
249
+ transform: scale(1.1);
250
+ }
251
+
252
+ .image-meta {
253
+ padding: 1rem;
254
+ border-top: 1px solid var(--border-color);
255
+ }
256
+
257
+ .image-filename {
258
+ font-weight: 600;
259
+ color: #f1f5f9;
260
+ font-size: 0.95rem;
261
+ white-space: nowrap;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ }
265
+
266
+ /* Text Sources */
267
+ .source-card {
268
+ background: rgba(30, 41, 59, 0.4);
269
+ border: 1px solid var(--border-color);
270
+ border-radius: 0.75rem;
271
+ padding: 1.25rem;
272
+ margin-top: 0.75rem;
273
+ transition: background 0.2s;
274
+ }
275
+
276
+ .source-card:hover {
277
+ background: rgba(30, 41, 59, 0.8);
278
+ border-color: #475569;
279
+ }
280
+
281
+ .source-title {
282
+ font-weight: 600;
283
+ color: var(--primary);
284
+ margin-bottom: 0.5rem;
285
+ font-size: 0.95rem;
286
+ }
287
+
288
+ .source-excerpt {
289
+ font-style: italic;
290
+ color: #cbd5e1;
291
+ font-size: 0.9rem;
292
+ background: rgba(0, 0, 0, 0.2);
293
+ padding: 0.75rem;
294
+ border-radius: 0.5rem;
295
+ border-left: 3px solid var(--primary);
296
+ word-wrap: break-word;
297
+ }
298
+
299
+ .source-meta {
300
+ margin-top: 0.75rem;
301
+ font-size: 0.8rem;
302
+ color: var(--text-muted);
303
+ text-align: right;
304
+ }
305
+
306
+ /* Animations */
307
+ @keyframes fadeInDown {
308
+ from {
309
+ opacity: 0;
310
+ transform: translateY(-20px);
311
+ }
312
+ to {
313
+ opacity: 1;
314
+ transform: translateY(0);
315
+ }
316
+ }
317
+
318
+ @keyframes fadeInUp {
319
+ from {
320
+ opacity: 0;
321
+ transform: translateY(20px);
322
+ }
323
+ to {
324
+ opacity: 1;
325
+ transform: translateY(0);
326
+ }
327
+ }
328
+
329
+ /* Scrollbar */
330
+ ::-webkit-scrollbar {
331
+ width: 8px;
332
+ }
333
+
334
+ ::-webkit-scrollbar-track {
335
+ background: var(--bg-dark);
336
+ }
337
+
338
+ ::-webkit-scrollbar-thumb {
339
+ background: var(--border-color);
340
+ border-radius: 4px;
341
+ }
342
+
343
+ ::-webkit-scrollbar-thumb:hover {
344
+ background: #475569;
345
+ }
346
+ </style>
347
+ </head>
348
+
349
+ <body>
350
+ <div class="container">
351
+ <header>
352
+ <h1>WHEC - Chatbot</h1>
353
+ <p class="subtitle">Based on documents at WHEC (Warrior Heat- and Exertion-Related Events Collaborative)
354
+ <a href="https://www.hprc-online.org/resources-partners/whec" target="_blank">page</a>
355
+ </p>
356
+ </header>
357
+
358
+ <div class="search-container">
359
+ <div class="input-group">
360
+ <input type="text" id="questionInput" placeholder="Ask a question about exertion-related injuries..."
361
+ autocomplete="off">
362
+ <button id="searchBtn" onclick="askQuestion()">
363
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
364
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
365
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
366
+ </svg>
367
+ Analyze
368
+ </button>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- CHAT HISTORY -->
373
+ <div id="chatHistory"></div>
374
+
375
+ <div id="loading">
376
+ <div class="spinner"></div>
377
+ <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p>
378
+ </div>
379
+ </div>
380
+
381
+ <script>
382
+ const input = document.getElementById('questionInput');
383
+ const searchBtn = document.getElementById('searchBtn');
384
+ const loading = document.getElementById('loading');
385
+ const chatHistoryContainer = document.getElementById('chatHistory');
386
+
387
+ // Allow Enter key to submit
388
+ input.addEventListener('keypress', function (e) {
389
+ if (e.key === 'Enter') {
390
+ askQuestion();
391
+ }
392
+ });
393
+
394
+ function escapeHtml(text) {
395
+ const div = document.createElement('div');
396
+ div.textContent = text;
397
+ return div.innerHTML;
398
+ }
399
+
400
+ async function askQuestion() {
401
+ const question = input.value.trim();
402
+ if (!question) return;
403
+
404
+ // UI State updates
405
+ searchBtn.disabled = true;
406
+ searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>';
407
+ input.disabled = true;
408
+ loading.style.display = 'block';
409
+
410
+ try {
411
+ const response = await fetch('/query', {
412
+ method: 'POST',
413
+ headers: { 'Content-Type': 'application/json' },
414
+ body: JSON.stringify({ question: question })
415
+ });
416
+
417
+ if (!response.ok) {
418
+ throw new Error(`Server error: ${response.status}`);
419
+ }
420
+
421
+ const data = await response.json();
422
+
423
+ // Create a chat message container
424
+ const messageBlock = document.createElement('div');
425
+ messageBlock.className = 'message-block';
426
+
427
+ // User question
428
+ const userQuestion = document.createElement('div');
429
+ userQuestion.className = 'user-question';
430
+ userQuestion.innerHTML = `<strong>You:</strong> ${escapeHtml(question)}`;
431
+ messageBlock.appendChild(userQuestion);
432
+
433
+ // Assistant answer
434
+ const assistantAnswer = document.createElement('div');
435
+ assistantAnswer.className = 'assistant-answer';
436
+ assistantAnswer.innerHTML = `<strong>Assistant:</strong><br>${escapeHtml(data.answer).replace(/\n/g, '<br>')}`;
437
+ messageBlock.appendChild(assistantAnswer);
438
+
439
+ // Display Images
440
+ if (data.images && data.images.length > 0) {
441
+ const relevantImages = data.images.filter(img => (img.score || 0) >= 0.3).slice(0, 3);
442
+
443
+ if (relevantImages.length > 0) {
444
+ const imagesWrapper = document.createElement('div');
445
+ imagesWrapper.className = 'images-grid';
446
+
447
+ relevantImages.forEach(img => {
448
+ const div = document.createElement('div');
449
+ div.className = 'image-card';
450
+ div.innerHTML = `
451
+ <a href="${escapeHtml(img.path || '')}" target="_blank">
452
+ <img src="${escapeHtml(img.path || '')}" alt="${escapeHtml(img.filename || 'Image')}" onerror="this.style.display='none'">
453
+ </a>
454
+ <div class="image-meta">
455
+ <div class="image-filename">${escapeHtml(img.filename || 'Unknown')}</div>
456
+ </div>
457
+ `;
458
+ imagesWrapper.appendChild(div);
459
+ });
460
+
461
+ messageBlock.appendChild(imagesWrapper);
462
+ }
463
+ }
464
+
465
+ // Display Texts
466
+ if (data.texts && data.texts.length > 0) {
467
+ const topTexts = data.texts.slice(0, 3);
468
+
469
+ topTexts.forEach(txt => {
470
+ const div = document.createElement('div');
471
+ div.className = 'source-card';
472
+ const truncatedText = txt.text.length > 200 ? txt.text.substring(0, 200) + '...' : txt.text;
473
+ div.innerHTML = `
474
+ <div class="source-title">${escapeHtml(txt.file || 'Document')}</div>
475
+ <div class="source-excerpt">"${escapeHtml(truncatedText)}"</div>
476
+ <div class="source-meta">
477
+ Page ${escapeHtml(String(txt.page || 'N/A'))} • ${Math.round((txt.score || 0) * 100)}% match
478
+ </div>
479
+ `;
480
+ messageBlock.appendChild(div);
481
+ });
482
+ }
483
+
484
+ chatHistoryContainer.appendChild(messageBlock);
485
+
486
+ // Scroll to the new message
487
+ setTimeout(() => {
488
+ messageBlock.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
489
+ }, 100);
490
+
491
+ } catch (error) {
492
+ alert('Error querying system. Please try again.');
493
+ console.error('Query error:', error);
494
+ } finally {
495
+ loading.style.display = 'none';
496
+ searchBtn.disabled = false;
497
+ searchBtn.innerHTML = `
498
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
499
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
500
+ </svg>
501
+ Analyze
502
+ `;
503
+ input.disabled = false;
504
+ input.focus();
505
+ input.value = '';
506
+ }
507
+ }
508
+ </script>
509
+ </body>
510
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  </html>