Files changed (1) hide show
  1. static/index.html +564 -570
static/index.html CHANGED
@@ -1,571 +1,565 @@
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
+ 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
+ <!-- CHAT HISTORY -->
393
+ <div id="chatHistory" style="max-width: 1000px; margin: 0 auto; display: flex; flex-direction: column; gap: 2rem;">
394
+ </div>
395
+
396
+ <div id="loading">
397
+ <div class="spinner"></div>
398
+ <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p>
399
+ </div>
400
+
401
+ <div class="answer-card">
402
+ <div id="answerText" class="answer-text"></div>
403
+ </div>
404
+
405
+ <!-- TEXT SOURCES SECTION -->
406
+ <div id="textsSection" style="display: none;">
407
+ <div class="section-title" style="margin-top: 3rem;">
408
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
409
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
410
+ 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">
411
+ </path>
412
+ </svg>
413
+ Source Documents
414
+ </div>
415
+ <div id="textsGrid" class="sources-grid"></div>
416
+ </div>
417
+
418
+ <!-- IMAGES SECTION -->
419
+ <div id="imagesSection" style="display: none;">
420
+ <div class="section-title" style="margin-top: 3rem;">
421
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
422
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
423
+ 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">
424
+ </path>
425
+ </svg>
426
+ Relevant Visual Data
427
+ </div>
428
+ <div id="imagesGrid" class="images-grid"></div>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <script>
434
+ const input = document.getElementById('questionInput');
435
+ const searchBtn = document.getElementById('searchBtn');
436
+ const loading = document.getElementById('loading');
437
+ const chatHistoryContainer = document.getElementById('chatHistory');
438
+ imagesSection.style.display = 'none';
439
+ textsSection.style.display = 'none';
440
+
441
+
442
+
443
+
444
+ // Allow Enter key to submit
445
+ input.addEventListener('keypress', function (e) {
446
+ if (e.key === 'Enter') {
447
+ askQuestion();
448
+ }
449
+ });
450
+
451
+ async function askQuestion() {
452
+ const question = input.value.trim();
453
+ if (!question) return;
454
+
455
+ // UI State updates
456
+ searchBtn.disabled = true;
457
+ searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>';
458
+ input.disabled = true;
459
+ loading.style.display = 'block';
460
+
461
+ // Reset sections
462
+ imagesSection.style.display = 'none';
463
+ textsSection.style.display = 'none';
464
+
465
+ try {
466
+ const response = await fetch('/query', {
467
+ method: 'POST',
468
+ headers: { 'Content-Type': 'application/json' },
469
+ body: JSON.stringify({ question: question })
470
+ });
471
+
472
+ if (!response.ok) throw new Error('Failed to get response');
473
+
474
+ const data = await response.json();
475
+
476
+ // Create a chat message container
477
+ const messageBlock = document.createElement('div');
478
+ messageBlock.style.border = '1px solid var(--border-color)';
479
+ messageBlock.style.borderRadius = '1rem';
480
+ messageBlock.style.padding = '1.5rem';
481
+ messageBlock.style.background = 'var(--bg-card)';
482
+ messageBlock.style.animation = 'fadeInUp 0.4s ease-out';
483
+
484
+ // User question
485
+ const userQuestion = document.createElement('div');
486
+ userQuestion.style.marginBottom = '1rem';
487
+ userQuestion.innerHTML = `<strong>You:</strong> ${question}`;
488
+ messageBlock.appendChild(userQuestion);
489
+
490
+ // Assistant answer
491
+ const assistantAnswer = document.createElement('div');
492
+ assistantAnswer.style.marginBottom = '1rem';
493
+ assistantAnswer.innerHTML = `<strong>Assistant:</strong><br>${data.answer}`;
494
+ messageBlock.appendChild(assistantAnswer);
495
+
496
+ // 2. Display Images
497
+ if (data.images && data.images.length > 0) {
498
+ const imagesWrapper = document.createElement('div');
499
+ imagesWrapper.className = 'images-grid';
500
+ imagesWrapper.style.marginTop = '1rem';
501
+
502
+ data.images
503
+ .filter(img => img.score >= 0.3)
504
+ .slice(0, 3)
505
+ .forEach(img => {
506
+ const div = document.createElement('div');
507
+ div.className = 'image-card';
508
+ div.innerHTML = `
509
+ <a href="${img.path}" target="_blank">
510
+ <img src="${img.path}" alt="${img.filename}">
511
+ </a>
512
+ <div class="image-meta">
513
+ <div class="image-filename">${img.filename}</div>
514
+ </div>
515
+ `;
516
+ imagesWrapper.appendChild(div);
517
+ });
518
+
519
+ messageBlock.appendChild(imagesWrapper);
520
+ }
521
+
522
+
523
+ // 3. Display Texts
524
+ if (data.texts && data.texts.length > 0) {
525
+ const sourcesWrapper = document.createElement('div');
526
+ sourcesWrapper.style.marginTop = '1rem';
527
+
528
+ data.texts.slice(0, 3).forEach(txt => {
529
+ const div = document.createElement('div');
530
+ div.className = 'source-card';
531
+ div.innerHTML = `
532
+ <div class="source-title">${txt.file || 'Document'}</div>
533
+ <div class="source-excerpt">"${txt.text}..."</div>
534
+ <div class="source-meta">
535
+ Page ${txt.page || 'N/A'} • ${(txt.score * 100).toFixed(0)}%
536
+ </div>
537
+ `;
538
+ sourcesWrapper.appendChild(div);
539
+ });
540
+
541
+ messageBlock.appendChild(sourcesWrapper);
542
+ }
543
+
544
+ chatHistoryContainer.appendChild(messageBlock);
545
+ chatHistoryContainer.scrollTop = chatHistoryContainer.scrollHeight;
546
+
547
+ } catch (error) {
548
+ alert('Error querying system. Please try again.');
549
+ console.error(error);
550
+ } finally {
551
+ loading.style.display = 'none';
552
+ searchBtn.disabled = false;
553
+ searchBtn.innerHTML = `
554
+ <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>
555
+ Analyze
556
+ `;
557
+ input.disabled = false;
558
+ input.focus();
559
+ input.value = '';
560
+ }
561
+ }
562
+ </script>
563
+ </body>
564
+
 
 
 
 
 
 
565
  </html>