avakanski commited on
Commit
8ba7476
·
verified ·
1 Parent(s): a3edfd8

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +6 -0
  2. query_index.py +5 -1
  3. static/index.html +552 -540
app.py CHANGED
@@ -71,6 +71,10 @@ app.mount("/static", StaticFiles(directory="static"), name="static")
71
  if os.path.exists("extracted_images"):
72
  app.mount("/extracted_images", StaticFiles(directory="extracted_images"), name="images")
73
 
 
 
 
 
74
  # Pydantic models
75
  class QueryRequest(BaseModel):
76
  question: str = Field(..., min_length=1, max_length=1000, description="Question to ask")
@@ -81,12 +85,14 @@ class ImageSource(BaseModel):
81
  score: Optional[float]
82
  page: Optional[Union[str, int]] # could be int or str depending on metadata
83
  file: Optional[str]
 
84
 
85
  class TextSource(BaseModel):
86
  text: str
87
  score: float
88
  page: Optional[Union[str, int]]
89
  file: Optional[str]
 
90
 
91
  class QueryResponse(BaseModel):
92
  answer: str
 
71
  if os.path.exists("extracted_images"):
72
  app.mount("/extracted_images", StaticFiles(directory="extracted_images"), name="images")
73
 
74
+ # Mount PDF documents
75
+ if os.path.exists("WHEC_Documents"):
76
+ app.mount("/documents", StaticFiles(directory="WHEC_Documents"), name="documents")
77
+
78
  # Pydantic models
79
  class QueryRequest(BaseModel):
80
  question: str = Field(..., min_length=1, max_length=1000, description="Question to ask")
 
85
  score: Optional[float]
86
  page: Optional[Union[str, int]] # could be int or str depending on metadata
87
  file: Optional[str]
88
+ link: Optional[str] = None
89
 
90
  class TextSource(BaseModel):
91
  text: str
92
  score: float
93
  page: Optional[Union[str, int]]
94
  file: Optional[str]
95
+ link: Optional[str] = None
96
 
97
  class QueryResponse(BaseModel):
98
  answer: str
query_index.py CHANGED
@@ -127,11 +127,15 @@ class MultimodalRAGSystem:
127
  })
128
  else:
129
  # Text node
 
 
 
130
  source_texts.append({
131
  "text": node.text[:200] + "...",
132
  "score": node_score.score,
133
  "page": node.metadata.get("page_number", "N/A"),
134
- "file": node.metadata.get("file_name", "N/A")
 
135
  })
136
 
137
  return {
 
127
  })
128
  else:
129
  # Text node
130
+ file_name = node.metadata.get("file_name", "N/A")
131
+ web_link = f"/documents/{file_name}" if file_name != "N/A" else None
132
+
133
  source_texts.append({
134
  "text": node.text[:200] + "...",
135
  "score": node_score.score,
136
  "page": node.metadata.get("page_number", "N/A"),
137
+ "file": file_name,
138
+ "link": web_link
139
  })
140
 
141
  return {
static/index.html CHANGED
@@ -1,541 +1,553 @@
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>WHEC Chatbot • 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
- font-weight: 600;
294
- color: var(--primary);
295
- margin-bottom: 0.5rem;
296
- font-size: 0.95rem;
297
- }
298
-
299
- .source-excerpt {
300
- font-style: italic;
301
- color: #cbd5e1;
302
- font-size: 0.9rem;
303
- background: rgba(0, 0, 0, 0.2);
304
- padding: 0.75rem;
305
- border-radius: 0.5rem;
306
- border-left: 3px solid var(--primary);
307
- }
308
-
309
- .source-meta {
310
- margin-top: 0.75rem;
311
- font-size: 0.8rem;
312
- color: var(--text-muted);
313
- text-align: right;
314
- }
315
-
316
- /* Animations */
317
- @keyframes fadeInDown {
318
- from {
319
- opacity: 0;
320
- transform: translateY(-20px);
321
- }
322
-
323
- to {
324
- opacity: 1;
325
- transform: translateY(0);
326
- }
327
- }
328
-
329
- @keyframes fadeInUp {
330
- from {
331
- opacity: 0;
332
- transform: translateY(20px);
333
- }
334
-
335
- to {
336
- opacity: 1;
337
- transform: translateY(0);
338
- }
339
- }
340
-
341
- /* Scrollbar */
342
- ::-webkit-scrollbar {
343
- width: 8px;
344
- }
345
-
346
- ::-webkit-scrollbar-track {
347
- background: var(--bg-dark);
348
- }
349
-
350
- ::-webkit-scrollbar-thumb {
351
- background: var(--border-color);
352
- border-radius: 4px;
353
- }
354
-
355
- ::-webkit-scrollbar-thumb:hover {
356
- background: #475569;
357
- }
358
- </style>
359
- </head>
360
-
361
- <body>
362
- <div class="container">
363
- <header>
364
- <h1>WHEC Chatbot</h1>
365
- <p class="subtitle">Exertion-Related Injuries System</p>
366
- </header>
367
-
368
- <div class="search-container">
369
- <div class="input-group">
370
- <input type="text" id="questionInput" placeholder="Ask a complex question about medical papers..."
371
- autocomplete="off">
372
- <button id="searchBtn" onclick="askQuestion()">
373
- <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
374
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
375
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
376
- </svg>
377
- Analyze
378
- </button>
379
- </div>
380
- </div>
381
-
382
- <div id="loading">
383
- <div class="spinner"></div>
384
- <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p>
385
- </div>
386
-
387
- <div id="results">
388
- <!-- ANSWER SECTION -->
389
- <div class="section-title">
390
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
391
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
392
- 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">
393
- </path>
394
- </svg>
395
- Consolidated Intelligence
396
- </div>
397
-
398
- <div class="answer-card">
399
- <div id="answerText" class="answer-text"></div>
400
- </div>
401
-
402
- <!-- IMAGES SECTION -->
403
- <div id="imagesSection" style="display: none;">
404
- <div class="section-title" style="margin-top: 3rem;">
405
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
406
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
407
- 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">
408
- </path>
409
- </svg>
410
- Relevant Visual Data
411
- </div>
412
- <div id="imagesGrid" class="images-grid"></div>
413
- </div>
414
-
415
- <!-- TEXT SOURCES SECTION -->
416
- <div id="textsSection" style="display: none;">
417
- <div class="section-title" style="margin-top: 3rem;">
418
- <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
419
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
420
- 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">
421
- </path>
422
- </svg>
423
- Source Documents
424
- </div>
425
- <div id="textsGrid" class="sources-grid"></div>
426
- </div>
427
- </div>
428
- </div>
429
-
430
- <script>
431
- const input = document.getElementById('questionInput');
432
- const searchBtn = document.getElementById('searchBtn');
433
- const loading = document.getElementById('loading');
434
- const results = document.getElementById('results');
435
- const answerText = document.getElementById('answerText');
436
-
437
- const imagesSection = document.getElementById('imagesSection');
438
- const imagesGrid = document.getElementById('imagesGrid');
439
-
440
- const textsSection = document.getElementById('textsSection');
441
- const textsGrid = document.getElementById('textsGrid');
442
-
443
- // Allow Enter key to submit
444
- input.addEventListener('keypress', function (e) {
445
- if (e.key === 'Enter') {
446
- askQuestion();
447
- }
448
- });
449
-
450
- async function askQuestion() {
451
- const question = input.value.trim();
452
- if (!question) return;
453
-
454
- // UI State updates
455
- searchBtn.disabled = true;
456
- searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>';
457
- input.disabled = true;
458
- results.style.display = 'none';
459
- loading.style.display = 'block';
460
-
461
- // Reset sections
462
- imagesSection.style.display = 'none';
463
- textsSection.style.display = 'none';
464
- imagesGrid.innerHTML = '';
465
- textsGrid.innerHTML = '';
466
-
467
- try {
468
- const response = await fetch('/query', {
469
- method: 'POST',
470
- headers: { 'Content-Type': 'application/json' },
471
- body: JSON.stringify({ question: question })
472
- });
473
-
474
- if (!response.ok) throw new Error('Failed to get response');
475
-
476
- const data = await response.json();
477
-
478
- // 1. Display Answer with simple markdown replacement for bold
479
- // Very basic MD parser for bold text if any
480
- let formattedAnswer = data.answer.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
481
- answerText.innerHTML = formattedAnswer;
482
-
483
- // 2. Display Images
484
- if (data.images && data.images.length > 0) {
485
- imagesSection.style.display = 'block';
486
- data.images.forEach(img => {
487
- const div = document.createElement('div');
488
- div.className = 'image-card';
489
- div.innerHTML = `
490
- <a href="${img.path}" target="_blank">
491
- <img src="${img.path}" alt="${img.filename}" loading="lazy">
492
- </a>
493
- <div class="image-meta">
494
- <div class="image-filename" title="${img.filename}">${img.filename}</div>
495
- <div class="image-stats">
496
- <span>Page ${img.page || '?'}</span>
497
- <span>${(img.score * 100).toFixed(0)}% Match</span>
498
- </div>
499
- </div>
500
- `;
501
- imagesGrid.appendChild(div);
502
- });
503
- }
504
-
505
- // 3. Display Texts
506
- if (data.texts && data.texts.length > 0) {
507
- textsSection.style.display = 'block';
508
- data.texts.forEach(txt => {
509
- const div = document.createElement('div');
510
- div.className = 'source-card';
511
- div.innerHTML = `
512
- <div class="source-title">${txt.file || 'Document'}</div>
513
- <div class="source-excerpt">"${txt.text}..."</div>
514
- <div class="source-meta">
515
- Page ${txt.page || 'N/A'} • Similarity: ${(txt.score * 100).toFixed(0)}%
516
- </div>
517
- `;
518
- textsGrid.appendChild(div);
519
- });
520
- }
521
-
522
- results.style.display = 'block';
523
-
524
- } catch (error) {
525
- alert('Error querying system. Please try again.');
526
- console.error(error);
527
- } finally {
528
- loading.style.display = 'none';
529
- searchBtn.disabled = false;
530
- searchBtn.innerHTML = `
531
- <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>
532
- Analyze
533
- `;
534
- input.disabled = false;
535
- input.focus();
536
- }
537
- }
538
- </script>
539
- </body>
540
-
 
 
 
 
 
 
 
 
 
 
 
 
541
  </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">Exertion-related Injuries Analysis System</p>
374
+ </header>
375
+
376
+ <div class="search-container">
377
+ <div class="input-group">
378
+ <input type="text" id="questionInput" placeholder="Ask a question about exertion-related injuries..."
379
+ autocomplete="off">
380
+ <button id="searchBtn" onclick="askQuestion()">
381
+ <svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
382
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
383
+ d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
384
+ </svg>
385
+ Analyze
386
+ </button>
387
+ </div>
388
+ </div>
389
+
390
+ <div id="loading">
391
+ <div class="spinner"></div>
392
+ <p style="color: var(--text-muted); font-size: 0.9rem;">Processing multimodal embeddings...</p>
393
+ </div>
394
+
395
+ <div id="results">
396
+ <!-- ANSWER SECTION -->
397
+ <div class="section-title">
398
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
399
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
400
+ 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">
401
+ </path>
402
+ </svg>
403
+ Consolidated Intelligence
404
+ </div>
405
+
406
+ <div class="answer-card">
407
+ <div id="answerText" class="answer-text"></div>
408
+ </div>
409
+
410
+ <!-- IMAGES SECTION -->
411
+ <div id="imagesSection" style="display: none;">
412
+ <div class="section-title" style="margin-top: 3rem;">
413
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
414
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
415
+ 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">
416
+ </path>
417
+ </svg>
418
+ Relevant Visual Data
419
+ </div>
420
+ <div id="imagesGrid" class="images-grid"></div>
421
+ </div>
422
+
423
+ <!-- TEXT SOURCES SECTION -->
424
+ <div id="textsSection" style="display: none;">
425
+ <div class="section-title" style="margin-top: 3rem;">
426
+ <svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">
427
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
428
+ 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">
429
+ </path>
430
+ </svg>
431
+ Source Documents
432
+ </div>
433
+ <div id="textsGrid" class="sources-grid"></div>
434
+ </div>
435
+ </div>
436
+ </div>
437
+
438
+ <script>
439
+ const input = document.getElementById('questionInput');
440
+ const searchBtn = document.getElementById('searchBtn');
441
+ const loading = document.getElementById('loading');
442
+ const results = document.getElementById('results');
443
+ const answerText = document.getElementById('answerText');
444
+
445
+ const imagesSection = document.getElementById('imagesSection');
446
+ const imagesGrid = document.getElementById('imagesGrid');
447
+
448
+ const textsSection = document.getElementById('textsSection');
449
+ const textsGrid = document.getElementById('textsGrid');
450
+
451
+ // Allow Enter key to submit
452
+ input.addEventListener('keypress', function (e) {
453
+ if (e.key === 'Enter') {
454
+ askQuestion();
455
+ }
456
+ });
457
+
458
+ async function askQuestion() {
459
+ const question = input.value.trim();
460
+ if (!question) return;
461
+
462
+ // UI State updates
463
+ searchBtn.disabled = true;
464
+ searchBtn.innerHTML = '<span style="font-size: 0.9em">Processing...</span>';
465
+ input.disabled = true;
466
+ results.style.display = 'none';
467
+ loading.style.display = 'block';
468
+
469
+ // Reset sections
470
+ imagesSection.style.display = 'none';
471
+ textsSection.style.display = 'none';
472
+ imagesGrid.innerHTML = '';
473
+ textsGrid.innerHTML = '';
474
+
475
+ try {
476
+ const response = await fetch('/query', {
477
+ method: 'POST',
478
+ headers: { 'Content-Type': 'application/json' },
479
+ body: JSON.stringify({ question: question })
480
+ });
481
+
482
+ if (!response.ok) throw new Error('Failed to get response');
483
+
484
+ const data = await response.json();
485
+
486
+ // 1. Display Answer with simple markdown replacement for bold
487
+ // Very basic MD parser for bold text if any
488
+ let formattedAnswer = data.answer.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
489
+ answerText.innerHTML = formattedAnswer;
490
+
491
+ // 2. Display Images
492
+ if (data.images && data.images.length > 0) {
493
+ imagesSection.style.display = 'block';
494
+ data.images.forEach(img => {
495
+ const div = document.createElement('div');
496
+ div.className = 'image-card';
497
+ div.innerHTML = `
498
+ <a href="${img.path}" target="_blank">
499
+ <img src="${img.path}" alt="${img.filename}" loading="lazy">
500
+ </a>
501
+ <div class="image-meta">
502
+ <div class="image-filename" title="${img.filename}">${img.filename}</div>
503
+ <div class="image-stats">
504
+ <span>Page ${img.page || '?'}</span>
505
+ <span>${(img.score * 100).toFixed(0)}% Match</span>
506
+ </div>
507
+ </div>
508
+ `;
509
+ imagesGrid.appendChild(div);
510
+ });
511
+ }
512
+
513
+ // 3. Display Texts
514
+ if (data.texts && data.texts.length > 0) {
515
+ textsSection.style.display = 'block';
516
+ data.texts.forEach(txt => {
517
+ const div = document.createElement('div');
518
+ div.className = 'source-card';
519
+ const titleHtml = txt.link
520
+ ? `<a href="${txt.link}" target="_blank" class="source-link">${txt.file || 'Document'}</a>`
521
+ : `<span class="source-title">${txt.file || 'Document'}</span>`;
522
+
523
+ div.innerHTML = `
524
+ <div class="source-title-wrapper">${titleHtml}</div>
525
+ <div class="source-excerpt">"${txt.text}..."</div>
526
+ <div class="source-meta">
527
+ Page ${txt.page || 'N/A'} Similarity: ${(txt.score * 100).toFixed(0)}%
528
+ </div>
529
+ `;
530
+ textsGrid.appendChild(div);
531
+ });
532
+ }
533
+
534
+ results.style.display = 'block';
535
+
536
+ } catch (error) {
537
+ alert('Error querying system. Please try again.');
538
+ console.error(error);
539
+ } finally {
540
+ loading.style.display = 'none';
541
+ searchBtn.disabled = false;
542
+ searchBtn.innerHTML = `
543
+ <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>
544
+ Analyze
545
+ `;
546
+ input.disabled = false;
547
+ input.focus();
548
+ }
549
+ }
550
+ </script>
551
+ </body>
552
+
553
  </html>