Hanan-Alnakhal commited on
Commit
d0f502e
·
verified ·
1 Parent(s): 67e4e82

Update static/script.js

Browse files
Files changed (1) hide show
  1. static/script.js +485 -385
static/script.js CHANGED
@@ -1,385 +1,485 @@
1
- // ===== GLOBAL STATE =====
2
- let currentResults = null;
3
- let explanations = null;
4
-
5
- // ===== INITIALIZATION =====
6
- document.addEventListener('DOMContentLoaded', () => {
7
- // Navigation is now handled via onclick="showSection", but we init other listeners
8
- initFileUpload();
9
- initScrollAnimations();
10
- initEnterKey();
11
- });
12
-
13
- // ===== NAVIGATION (Compatible with Tailwind HTML) =====
14
- function showSection(sectionId) {
15
- // Hide all sections
16
- document.querySelectorAll('.section').forEach(sec => {
17
- sec.classList.remove('active');
18
- sec.style.display = 'none';
19
- });
20
-
21
- // Show target section
22
- const target = document.getElementById(sectionId);
23
- if (target) {
24
- target.style.display = 'block';
25
- // Small timeout to allow display:block to apply before adding active class for animation
26
- setTimeout(() => {
27
- target.classList.add('active');
28
- }, 10);
29
-
30
- // Scroll to top
31
- window.scrollTo({ top: 0, behavior: 'smooth' });
32
- }
33
- }
34
-
35
- // ===== SCROLL ANIMATIONS =====
36
- function initScrollAnimations() {
37
- const observer = new IntersectionObserver((entries) => {
38
- entries.forEach(entry => {
39
- if (entry.isIntersecting) {
40
- entry.target.classList.remove('opacity-0', 'translate-y-4');
41
- entry.target.classList.add('opacity-100', 'translate-y-0');
42
- }
43
- });
44
- }, { threshold: 0.1 });
45
-
46
- // Add animation classes to cards
47
- document.querySelectorAll('.feature-card, .upload-card').forEach(card => {
48
- card.classList.add('transition-all', 'duration-700', 'opacity-0', 'translate-y-4');
49
- observer.observe(card);
50
- });
51
- }
52
-
53
- // ===== FILE UPLOAD =====
54
- function initFileUpload() {
55
- const fileInput = document.getElementById('fileInput');
56
- const uploadArea = document.getElementById('uploadArea');
57
-
58
- if (!fileInput || !uploadArea) return;
59
-
60
- // Click to upload (handled by onclick in HTML, but keeping listener just in case)
61
- fileInput.addEventListener('change', handleFileSelect);
62
-
63
- // Drag and drop Visuals
64
- uploadArea.addEventListener('dragover', (e) => {
65
- e.preventDefault();
66
- // Add Tailwind classes for drag state
67
- uploadArea.classList.add('border-secondary', 'bg-blue-50', 'scale-[1.02]');
68
- });
69
-
70
- uploadArea.addEventListener('dragleave', () => {
71
- // Remove Tailwind classes
72
- uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]');
73
- });
74
-
75
- uploadArea.addEventListener('drop', (e) => {
76
- e.preventDefault();
77
- uploadArea.classList.remove('border-secondary', 'bg-blue-50', 'scale-[1.02]');
78
-
79
- const file = e.dataTransfer.files[0];
80
- if (file && file.type === 'application/pdf') {
81
- uploadFile(file);
82
- } else {
83
- showToast('Please upload a PDF file', 'error');
84
- }
85
- });
86
- }
87
-
88
- function handleFileSelect(e) {
89
- const file = e.target.files[0];
90
- if (file) {
91
- uploadFile(file);
92
- }
93
- }
94
-
95
- async function uploadFile(file) {
96
- const formData = new FormData();
97
- formData.append('file', file);
98
-
99
- // Show progress
100
- document.getElementById('uploadArea').classList.add('hidden');
101
- document.getElementById('uploadProgress').classList.remove('hidden');
102
-
103
- try {
104
- const response = await fetch('/api/upload', {
105
- method: 'POST',
106
- body: formData
107
- });
108
-
109
- const data = await response.json();
110
-
111
- if (!response.ok) {
112
- throw new Error(data.error || 'Upload failed');
113
- }
114
-
115
- currentResults = data.results;
116
- showToast(`✓ Found ${data.count} lab results!`, 'success');
117
-
118
- // Generate explanations
119
- await generateExplanations();
120
-
121
- // Display results
122
- displayResults();
123
-
124
- // Switch to results tab automatically
125
- showSection('results-section');
126
-
127
- } catch (error) {
128
- showToast(error.message, 'error');
129
- resetUploadArea();
130
- }
131
- }
132
-
133
- async function generateExplanations() {
134
- // Optional: show a mini toast or loading indicator
135
- try {
136
- const response = await fetch('/api/explain', { method: 'POST' });
137
- const data = await response.json();
138
- if (!response.ok) throw new Error(data.error);
139
- explanations = data.explanations;
140
- } catch (error) {
141
- console.warn('Auto-explanation generation failed:', error);
142
- // We continue anyway, results will just say "Loading..." or show basic info
143
- }
144
- }
145
-
146
- function displayResults() {
147
- const container = document.getElementById('resultsContainer');
148
-
149
- if (!currentResults || currentResults.length === 0) {
150
- container.innerHTML = '<div class="text-center text-gray-500 py-10">No results found</div>';
151
- return;
152
- }
153
-
154
- // Update summary stats in the Summary tab
155
- updateSummaryStats();
156
-
157
- // Clear container
158
- container.innerHTML = '';
159
-
160
- // Create result cards with Tailwind Styling
161
- currentResults.forEach((result, index) => {
162
- const card = createResultCard(result, index);
163
- container.appendChild(card);
164
- });
165
-
166
- // Reset upload area for next time
167
- resetUploadArea();
168
- }
169
-
170
- function updateSummaryStats() {
171
- const stats = { normal: 0, high: 0, low: 0 };
172
-
173
- currentResults.forEach(result => {
174
- // Normalize status string
175
- const status = result.status ? result.status.toLowerCase() : 'normal';
176
- if (status.includes('high')) stats.high++;
177
- else if (status.includes('low')) stats.low++;
178
- else stats.normal++;
179
- });
180
-
181
- // Update the DOM elements in the Summary Section
182
- document.getElementById('normalCount').textContent = stats.normal;
183
- document.getElementById('highCount').textContent = stats.high;
184
- document.getElementById('lowCount').textContent = stats.low;
185
-
186
- // Reveal the stats container
187
- const statsContainer = document.getElementById('summaryStats');
188
- if(statsContainer) statsContainer.classList.remove('hidden');
189
-
190
- const generateBtn = document.getElementById('generateSummaryBtn');
191
- if(generateBtn) generateBtn.classList.remove('hidden');
192
- }
193
-
194
- function createResultCard(result, index) {
195
- const card = document.createElement('div');
196
- // TAILWIND STYLING: Card container
197
- card.className = 'bg-white rounded-xl shadow-sm border border-slate-100 p-5 mb-4 hover:shadow-md transition-shadow';
198
-
199
- // Status colors
200
- let statusColors = 'bg-green-100 text-green-700'; // Default normal
201
- let borderClass = 'border-l-4 border-green-500';
202
-
203
- const statusLower = result.status ? result.status.toLowerCase() : '';
204
- if (statusLower.includes('high')) {
205
- statusColors = 'bg-red-100 text-red-700';
206
- borderClass = 'border-l-4 border-red-500';
207
- } else if (statusLower.includes('low')) {
208
- statusColors = 'bg-yellow-100 text-yellow-800';
209
- borderClass = 'border-l-4 border-yellow-500';
210
- }
211
-
212
- const explanation = explanations && explanations[result.test_name]
213
- ? explanations[result.test_name]
214
- : 'Analysis available in Chat or Summary.';
215
-
216
- // HTML Structure using Tailwind
217
- card.innerHTML = `
218
- <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 ${borderClass} pl-4">
219
- <div class="flex-grow">
220
- <h3 class="text-lg font-bold text-slate-800">${escapeHtml(result.test_name)}</h3>
221
- <p class="text-sm text-slate-500">Ref Range: ${escapeHtml(result.reference_range || 'N/A')}</p>
222
- </div>
223
-
224
- <div class="flex items-center gap-3 w-full md:w-auto justify-between md:justify-end">
225
- <span class="text-xl font-bold text-slate-700">${escapeHtml(result.value)} <span class="text-sm font-normal text-slate-500">${escapeHtml(result.unit)}</span></span>
226
- <span class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide ${statusColors}">
227
- ${escapeHtml(result.status)}
228
- </span>
229
- </div>
230
- </div>
231
-
232
- <div class="mt-4 pt-3 border-t border-slate-50">
233
- <p class="text-sm text-slate-600 leading-relaxed">
234
- <span class="font-semibold text-primary">Insight:</span> ${escapeHtml(explanation)}
235
- </p>
236
- </div>
237
- `;
238
-
239
- return card;
240
- }
241
-
242
- function resetUploadArea() {
243
- document.getElementById('uploadArea').classList.remove('hidden');
244
- document.getElementById('uploadProgress').classList.add('hidden');
245
- document.getElementById('fileInput').value = '';
246
- }
247
-
248
- // ===== SUMMARY GENERATION =====
249
- async function generateSummary() {
250
- if (!currentResults) {
251
- showToast('Please upload a lab report first', 'error');
252
- return;
253
- }
254
-
255
- const contentDiv = document.getElementById('summaryContent');
256
- contentDiv.innerHTML = '<div class="flex justify-center p-8"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div></div>';
257
-
258
- try {
259
- const response = await fetch('/api/summary');
260
- const data = await response.json();
261
-
262
- if (!response.ok) throw new Error(data.error);
263
-
264
- // Render summary with Markdown-like paragraphs
265
- contentDiv.innerHTML = `
266
- <div class="prose prose-slate max-w-none">
267
- <h3 class="text-xl font-semibold mb-4 text-primary">Analysis Report</h3>
268
- <div class="text-slate-700 leading-relaxed whitespace-pre-line">
269
- ${escapeHtml(data.summary).replace(/\n/g, '<br>')}
270
- </div>
271
- </div>
272
- `;
273
-
274
- } catch (error) {
275
- showToast('Error: ' + error.message, 'error');
276
- contentDiv.innerHTML = '<p class="text-red-500 text-center">Failed to generate summary.</p>';
277
- }
278
- }
279
-
280
- // ===== CHAT FUNCTIONALITY =====
281
- function initEnterKey() {
282
- const input = document.getElementById('chatInput');
283
- if (input) {
284
- input.addEventListener('keypress', (e) => {
285
- if (e.key === 'Enter') askQuestion();
286
- });
287
- }
288
- }
289
-
290
- async function askQuestion() {
291
- if (!currentResults) {
292
- showToast('Please upload a lab report first', 'error');
293
- return;
294
- }
295
-
296
- const input = document.getElementById('chatInput');
297
- const question = input.value.trim();
298
-
299
- if (!question) return;
300
-
301
- // Clear input
302
- input.value = '';
303
-
304
- // Add user message
305
- addChatMessage(question, 'user');
306
-
307
- // Show loading
308
- const loadingId = addChatMessage('Analyzing...', 'assistant', true);
309
-
310
- try {
311
- const response = await fetch('/api/ask', {
312
- method: 'POST',
313
- headers: { 'Content-Type': 'application/json' },
314
- body: JSON.stringify({ question })
315
- });
316
-
317
- const data = await response.json();
318
- if (!response.ok) throw new Error(data.error);
319
-
320
- // Remove loading
321
- const loadingEl = document.getElementById(loadingId);
322
- if(loadingEl) loadingEl.remove();
323
-
324
- // Add response
325
- addChatMessage(data.answer, 'assistant');
326
-
327
- } catch (error) {
328
- const loadingEl = document.getElementById(loadingId);
329
- if(loadingEl) loadingEl.remove();
330
- showToast(error.message, 'error');
331
- }
332
- }
333
-
334
- function addChatMessage(text, sender, isLoading = false) {
335
- const container = document.getElementById('chatMessages');
336
-
337
- const wrapper = document.createElement('div');
338
- // Flex alignment based on sender
339
- wrapper.className = `flex w-full mb-4 ${sender === 'user' ? 'justify-end' : 'justify-start'}`;
340
-
341
- const bubble = document.createElement('div');
342
- // Bubble Styling
343
- const baseStyle = "max-w-[85%] rounded-2xl px-5 py-3 shadow-sm text-sm leading-relaxed";
344
- const userStyle = "bg-secondary text-white rounded-br-none"; // Blue bubble
345
- const aiStyle = "bg-slate-100 text-slate-800 rounded-tl-none border border-slate-200"; // Grey bubble
346
-
347
- bubble.className = `${baseStyle} ${sender === 'user' ? userStyle : aiStyle} ${isLoading ? 'animate-pulse' : ''}`;
348
-
349
- bubble.innerHTML = escapeHtml(text).replace(/\n/g, '<br>');
350
- if (isLoading) bubble.id = `loading-${Date.now()}`;
351
-
352
- wrapper.appendChild(bubble);
353
- container.appendChild(wrapper);
354
-
355
- // Scroll to bottom
356
- container.scrollTop = container.scrollHeight;
357
-
358
- return bubble.id;
359
- }
360
-
361
- // ===== TOAST NOTIFICATIONS =====
362
- function showToast(message, type = 'info') {
363
- const toast = document.getElementById('toast');
364
- toast.textContent = message;
365
-
366
- // Tailwind classes for toast
367
- toast.className = `fixed bottom-5 right-5 px-6 py-3 rounded-lg shadow-2xl transform transition-all duration-300 z-50 text-white font-medium translate-y-0 opacity-100`;
368
-
369
- if (type === 'error') toast.classList.add('bg-red-600');
370
- else if (type === 'success') toast.classList.add('bg-green-600');
371
- else toast.classList.add('bg-slate-800');
372
-
373
- setTimeout(() => {
374
- toast.classList.remove('translate-y-0', 'opacity-100');
375
- toast.classList.add('translate-y-20', 'opacity-0');
376
- }, 4000);
377
- }
378
-
379
- // ===== UTILS =====
380
- function escapeHtml(text) {
381
- if (!text) return '';
382
- const div = document.createElement('div');
383
- div.textContent = text;
384
- return div.innerHTML;
385
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== GLOBAL STATE =====
2
+ let currentResults = null;
3
+ let explanations = null;
4
+ let sessionId = null; // Store session ID
5
+
6
+ // ===== INITIALIZATION =====
7
+ document.addEventListener('DOMContentLoaded', () => {
8
+ initNavigation();
9
+ initFileUpload();
10
+ initScrollAnimations();
11
+ });
12
+
13
+ // ===== NAVIGATION =====
14
+ function initNavigation() {
15
+ const navLinks = document.querySelectorAll('.nav-link');
16
+
17
+ navLinks.forEach(link => {
18
+ link.addEventListener('click', (e) => {
19
+ e.preventDefault();
20
+ const target = link.getAttribute('href');
21
+
22
+ // Update active state
23
+ navLinks.forEach(l => l.classList.remove('active'));
24
+ link.classList.add('active');
25
+
26
+ // Smooth scroll
27
+ document.querySelector(target).scrollIntoView({
28
+ behavior: 'smooth',
29
+ block: 'start'
30
+ });
31
+ });
32
+ });
33
+
34
+ // Mobile menu toggle
35
+ const mobileToggle = document.querySelector('.mobile-menu-toggle');
36
+ const navMenu = document.querySelector('.nav-menu');
37
+
38
+ if (mobileToggle) {
39
+ mobileToggle.addEventListener('click', () => {
40
+ navMenu.classList.toggle('active');
41
+ });
42
+ }
43
+ }
44
+
45
+ // ===== SCROLL ANIMATIONS =====
46
+ function initScrollAnimations() {
47
+ const observer = new IntersectionObserver((entries) => {
48
+ entries.forEach(entry => {
49
+ if (entry.isIntersecting) {
50
+ entry.target.style.opacity = '1';
51
+ entry.target.style.transform = 'translateY(0)';
52
+ }
53
+ });
54
+ }, { threshold: 0.1 });
55
+
56
+ document.querySelectorAll('.about-card, .feature-card').forEach(card => {
57
+ card.style.opacity = '0';
58
+ card.style.transform = 'translateY(20px)';
59
+ card.style.transition = 'all 0.6s ease-out';
60
+ observer.observe(card);
61
+ });
62
+ }
63
+
64
+ // ===== FILE UPLOAD =====
65
+ function initFileUpload() {
66
+ const fileInput = document.getElementById('fileInput');
67
+ const uploadArea = document.getElementById('uploadArea');
68
+
69
+ if (!fileInput || !uploadArea) return;
70
+
71
+ // Click to upload
72
+ fileInput.addEventListener('change', handleFileSelect);
73
+
74
+ // Drag and drop
75
+ uploadArea.addEventListener('dragover', (e) => {
76
+ e.preventDefault();
77
+ uploadArea.classList.add('dragover');
78
+ });
79
+
80
+ uploadArea.addEventListener('dragleave', () => {
81
+ uploadArea.classList.remove('dragover');
82
+ });
83
+
84
+ uploadArea.addEventListener('drop', (e) => {
85
+ e.preventDefault();
86
+ uploadArea.classList.remove('dragover');
87
+
88
+ const file = e.dataTransfer.files[0];
89
+ if (file && file.type === 'application/pdf') {
90
+ uploadFile(file);
91
+ } else {
92
+ showToast('Please upload a PDF file', 'error');
93
+ }
94
+ });
95
+ }
96
+
97
+ function handleFileSelect(e) {
98
+ const file = e.target.files[0];
99
+ if (file) {
100
+ uploadFile(file);
101
+ }
102
+ }
103
+
104
+ async function uploadFile(file) {
105
+ const formData = new FormData();
106
+ formData.append('file', file);
107
+
108
+ // Show progress
109
+ document.getElementById('uploadArea').classList.add('hidden');
110
+ document.getElementById('uploadProgress').classList.remove('hidden');
111
+
112
+ try {
113
+ const response = await fetch('/api/upload', {
114
+ method: 'POST',
115
+ body: formData
116
+ });
117
+
118
+ const data = await response.json();
119
+
120
+ if (!response.ok) {
121
+ throw new Error(data.error || 'Upload failed');
122
+ }
123
+
124
+ // Store session ID and results
125
+ sessionId = data.session_id;
126
+ currentResults = data.results;
127
+
128
+ console.log('Session ID:', sessionId);
129
+ console.log('Results:', currentResults);
130
+
131
+ showToast(`✓ Found ${data.count} lab results!`, 'success');
132
+
133
+ // Generate explanations
134
+ await generateExplanations();
135
+
136
+ // Display results
137
+ displayResults();
138
+
139
+ // Scroll to results
140
+ setTimeout(() => {
141
+ document.getElementById('results').scrollIntoView({
142
+ behavior: 'smooth'
143
+ });
144
+ }, 500);
145
+
146
+ } catch (error) {
147
+ showToast(error.message, 'error');
148
+ resetUploadArea();
149
+ }
150
+ }
151
+
152
+ async function generateExplanations() {
153
+ showLoading('Generating AI explanations... This may take 30-60 seconds.');
154
+
155
+ try {
156
+ const response = await fetch('/api/explain', {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json'
160
+ },
161
+ body: JSON.stringify({ session_id: sessionId })
162
+ });
163
+
164
+ const data = await response.json();
165
+
166
+ if (!response.ok) {
167
+ throw new Error(data.error || 'Failed to generate explanations');
168
+ }
169
+
170
+ explanations = data.explanations;
171
+ console.log('Explanations:', explanations);
172
+
173
+ } catch (error) {
174
+ showToast('Error generating explanations: ' + error.message, 'error');
175
+ // Continue anyway to show results
176
+ } finally {
177
+ hideLoading();
178
+ }
179
+ }
180
+
181
+ function displayResults() {
182
+ const resultsSection = document.getElementById('results');
183
+ const container = document.getElementById('resultsContainer');
184
+
185
+ if (!currentResults || currentResults.length === 0) {
186
+ container.innerHTML = '<p class="empty-message">No results found</p>';
187
+ return;
188
+ }
189
+
190
+ // Show results section
191
+ resultsSection.classList.remove('hidden');
192
+
193
+ // Update summary stats
194
+ updateSummaryStats();
195
+
196
+ // Clear container
197
+ container.innerHTML = '';
198
+
199
+ // Create result cards
200
+ currentResults.forEach((result, index) => {
201
+ const card = createResultCard(result, index);
202
+ container.appendChild(card);
203
+ });
204
+
205
+ // Reset upload area
206
+ resetUploadArea();
207
+ }
208
+
209
+ function updateSummaryStats() {
210
+ const stats = {
211
+ normal: 0,
212
+ high: 0,
213
+ low: 0
214
+ };
215
+
216
+ currentResults.forEach(result => {
217
+ if (result.status in stats) {
218
+ stats[result.status]++;
219
+ }
220
+ });
221
+
222
+ document.getElementById('normalCount').textContent = stats.normal;
223
+ document.getElementById('highCount').textContent = stats.high;
224
+ document.getElementById('lowCount').textContent = stats.low;
225
+ }
226
+
227
+ function createResultCard(result, index) {
228
+ const card = document.createElement('div');
229
+ card.className = 'result-card';
230
+ card.style.setProperty('--i', index + 1);
231
+
232
+ const statusClass = `status-${result.status}`;
233
+ const explanation = explanations && explanations[result.test_name]
234
+ ? explanations[result.test_name]
235
+ : 'Explanation is being generated. Click "Ask Questions" to learn more about this result.';
236
+
237
+ card.innerHTML = `
238
+ <div class="result-header">
239
+ <div>
240
+ <h3 class="result-name">${escapeHtml(result.test_name)}</h3>
241
+ <p class="result-range">Reference: ${escapeHtml(result.reference_range || 'N/A')}</p>
242
+ </div>
243
+ <div class="result-value-container">
244
+ <div class="result-value">${escapeHtml(result.value)} ${escapeHtml(result.unit)}</div>
245
+ <span class="result-status ${statusClass}">${result.status}</span>
246
+ </div>
247
+ </div>
248
+ <div class="result-explanation">
249
+ <strong>💡 What does this mean?</strong>
250
+ <p>${escapeHtml(explanation)}</p>
251
+ </div>
252
+ `;
253
+
254
+ return card;
255
+ }
256
+
257
+ function resetUploadArea() {
258
+ document.getElementById('uploadArea').classList.remove('hidden');
259
+ document.getElementById('uploadProgress').classList.add('hidden');
260
+ document.getElementById('fileInput').value = '';
261
+ }
262
+
263
+ // ===== SUMMARY GENERATION =====
264
+ async function generateSummary() {
265
+ if (!sessionId) {
266
+ showToast('Please upload a lab report first', 'error');
267
+ return;
268
+ }
269
+
270
+ showLoading('Generating comprehensive summary...');
271
+
272
+ try {
273
+ const response = await fetch('/api/summary', {
274
+ method: 'POST',
275
+ headers: {
276
+ 'Content-Type': 'application/json'
277
+ },
278
+ body: JSON.stringify({ session_id: sessionId })
279
+ });
280
+
281
+ const data = await response.json();
282
+
283
+ if (!response.ok) {
284
+ throw new Error(data.error || 'Failed to generate summary');
285
+ }
286
+
287
+ // Show summary modal
288
+ showSummaryModal(data.summary, data.stats);
289
+
290
+ } catch (error) {
291
+ showToast('Error: ' + error.message, 'error');
292
+ } finally {
293
+ hideLoading();
294
+ }
295
+ }
296
+
297
+ function showSummaryModal(summary, stats) {
298
+ const modal = document.createElement('div');
299
+ modal.className = 'chat-modal';
300
+ modal.innerHTML = `
301
+ <div class="chat-modal-content">
302
+ <div class="chat-header">
303
+ <h3>📊 Complete Summary</h3>
304
+ <button class="chat-close" onclick="this.closest('.chat-modal').remove()">&times;</button>
305
+ </div>
306
+ <div class="chat-messages">
307
+ <div class="chat-message assistant">
308
+ <div class="chat-bubble">
309
+ <p>${escapeHtml(summary).replace(/\n/g, '<br><br>')}</p>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ `;
315
+ document.body.appendChild(modal);
316
+
317
+ // Close on click outside
318
+ modal.addEventListener('click', (e) => {
319
+ if (e.target === modal) {
320
+ modal.remove();
321
+ }
322
+ });
323
+ }
324
+
325
+ // ===== CHAT FUNCTIONALITY =====
326
+ function openChat() {
327
+ if (!sessionId) {
328
+ showToast('Please upload a lab report first', 'error');
329
+ return;
330
+ }
331
+
332
+ const modal = document.getElementById('chatModal');
333
+ modal.classList.remove('hidden');
334
+ document.getElementById('chatInput').focus();
335
+ }
336
+
337
+ function closeChat() {
338
+ document.getElementById('chatModal').classList.add('hidden');
339
+ }
340
+
341
+ function handleChatKeypress(event) {
342
+ if (event.key === 'Enter') {
343
+ sendChatMessage();
344
+ }
345
+ }
346
+
347
+ async function sendChatMessage() {
348
+ const input = document.getElementById('chatInput');
349
+ const question = input.value.trim();
350
+
351
+ if (!question) return;
352
+
353
+ if (!sessionId) {
354
+ showToast('Session expired. Please upload your report again.', 'error');
355
+ return;
356
+ }
357
+
358
+ // Clear input
359
+ input.value = '';
360
+
361
+ // Add user message
362
+ addChatMessage(question, 'user');
363
+
364
+ // Show loading message
365
+ const loadingId = addChatMessage('Thinking...', 'assistant', true);
366
+
367
+ try {
368
+ const response = await fetch('/api/ask', {
369
+ method: 'POST',
370
+ headers: {
371
+ 'Content-Type': 'application/json'
372
+ },
373
+ body: JSON.stringify({
374
+ question: question,
375
+ session_id: sessionId
376
+ })
377
+ });
378
+
379
+ const data = await response.json();
380
+
381
+ if (!response.ok) {
382
+ throw new Error(data.error || 'Failed to get answer');
383
+ }
384
+
385
+ // Remove loading message
386
+ document.getElementById(loadingId).remove();
387
+
388
+ // Add assistant response
389
+ addChatMessage(data.answer, 'assistant');
390
+
391
+ } catch (error) {
392
+ document.getElementById(loadingId).remove();
393
+ addChatMessage(`Sorry, I encountered an error: ${error.message}`, 'assistant');
394
+ showToast(error.message, 'error');
395
+ }
396
+ }
397
+
398
+ function addChatMessage(text, sender, isLoading = false) {
399
+ const messagesContainer = document.getElementById('chatMessages');
400
+
401
+ // Remove welcome message if exists
402
+ const welcome = messagesContainer.querySelector('.chat-welcome');
403
+ if (welcome) {
404
+ welcome.remove();
405
+ }
406
+
407
+ const messageId = `msg-${Date.now()}`;
408
+ const messageDiv = document.createElement('div');
409
+ messageDiv.id = messageId;
410
+ messageDiv.className = `chat-message ${sender}`;
411
+
412
+ const bubbleClass = isLoading ? 'chat-bubble loading' : 'chat-bubble';
413
+ messageDiv.innerHTML = `<div class="${bubbleClass}">${escapeHtml(text)}</div>`;
414
+
415
+ messagesContainer.appendChild(messageDiv);
416
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
417
+
418
+ return messageId;
419
+ }
420
+
421
+ // ===== LOADING OVERLAY =====
422
+ function showLoading(message = 'Processing...') {
423
+ const overlay = document.getElementById('loadingOverlay');
424
+ if (overlay) {
425
+ overlay.querySelector('p').textContent = message;
426
+ overlay.classList.remove('hidden');
427
+ }
428
+ }
429
+
430
+ function hideLoading() {
431
+ const overlay = document.getElementById('loadingOverlay');
432
+ if (overlay) {
433
+ overlay.classList.add('hidden');
434
+ }
435
+ }
436
+
437
+ // ===== TOAST NOTIFICATIONS =====
438
+ function showToast(message, type = 'info') {
439
+ const toast = document.getElementById('toast');
440
+ toast.textContent = message;
441
+ toast.className = `toast ${type} show`;
442
+
443
+ setTimeout(() => {
444
+ toast.classList.remove('show');
445
+ }, 4000);
446
+ }
447
+
448
+ // ===== UTILITY FUNCTIONS =====
449
+ function escapeHtml(text) {
450
+ const div = document.createElement('div');
451
+ div.textContent = text;
452
+ return div.innerHTML;
453
+ }
454
+
455
+ // ===== SMOOTH SCROLL FOR ALL LINKS =====
456
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
457
+ anchor.addEventListener('click', function (e) {
458
+ e.preventDefault();
459
+ const target = document.querySelector(this.getAttribute('href'));
460
+ if (target) {
461
+ target.scrollIntoView({
462
+ behavior: 'smooth',
463
+ block: 'start'
464
+ });
465
+ }
466
+ });
467
+ });
468
+
469
+ // ===== CLOSE MODALS ON ESC =====
470
+ document.addEventListener('keydown', (e) => {
471
+ if (e.key === 'Escape') {
472
+ const chatModal = document.getElementById('chatModal');
473
+ if (chatModal && !chatModal.classList.contains('hidden')) {
474
+ closeChat();
475
+ }
476
+ }
477
+ });
478
+
479
+ // ===== CLOSE MODALS ON OUTSIDE CLICK =====
480
+ document.addEventListener('click', (e) => {
481
+ const chatModal = document.getElementById('chatModal');
482
+ if (e.target === chatModal) {
483
+ closeChat();
484
+ }
485
+ });