adeyemi001 commited on
Commit
ff30890
·
verified ·
1 Parent(s): 5b54c93

Update frontend/script.js

Browse files
Files changed (1) hide show
  1. frontend/script.js +508 -523
frontend/script.js CHANGED
@@ -1,524 +1,509 @@
1
- // API Configuration - Auto-detect URL for Hugging Face
2
- const API_BASE_URL = window.location.origin;
3
-
4
- // Session Management
5
- let currentSessionId = null;
6
- let currentAnswer = null;
7
- let currentSources = null;
8
- let currentQuery = null;
9
- let queryHistory = [];
10
-
11
- // DOM Elements
12
- const queryInput = document.getElementById('queryInput');
13
- const submitBtn = document.getElementById('submitBtn');
14
- const btnText = submitBtn.querySelector('.btn-text');
15
- const loader = submitBtn.querySelector('.loader');
16
- const responseSection = document.getElementById('responseSection');
17
- const initialState = document.getElementById('initialState');
18
- const loadingState = document.getElementById('loadingState');
19
- const answerDiv = document.getElementById('answer');
20
- const sourcesDiv = document.getElementById('sources');
21
- const errorToast = document.getElementById('errorToast');
22
- const errorMessage = document.getElementById('errorMessage');
23
- const expandedQueriesDiv = document.getElementById('expandedQueries');
24
- const queriesList = document.getElementById('queriesList');
25
- const exportBtn = document.getElementById('exportBtn');
26
- const githubBtn = document.getElementById('githubBtn');
27
- const historySelect = document.getElementById('historySelect');
28
-
29
- // Initialize
30
- function init() {
31
- currentSessionId = sessionStorage.getItem('sessionId') || generateSessionId();
32
- sessionStorage.setItem('sessionId', currentSessionId);
33
-
34
- // Load history
35
- const saved = sessionStorage.getItem('queryHistory');
36
- if (saved) {
37
- try {
38
- queryHistory = JSON.parse(saved);
39
- updateHistoryDropdown();
40
- } catch (e) {
41
- queryHistory = [];
42
- }
43
- }
44
-
45
- setupEventListeners();
46
- checkHealth();
47
-
48
- // Log API URL for debugging
49
- console.log('API Base URL:', API_BASE_URL);
50
- }
51
-
52
- function generateSessionId() {
53
- return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
54
- }
55
-
56
- function setupEventListeners() {
57
- // Submit button
58
- if (submitBtn) {
59
- submitBtn.addEventListener('click', handleSubmit);
60
- }
61
-
62
- // Enter key (Ctrl+Enter)
63
- if (queryInput) {
64
- queryInput.addEventListener('keydown', (e) => {
65
- if (e.key === 'Enter' && e.ctrlKey) {
66
- handleSubmit();
67
- }
68
- });
69
- }
70
-
71
- // Example queries
72
- document.querySelectorAll('.example-item').forEach(item => {
73
- item.addEventListener('click', () => {
74
- const query = item.getAttribute('data-query');
75
- if (query && queryInput) {
76
- queryInput.value = query;
77
- handleSubmit();
78
- }
79
- });
80
- });
81
-
82
- // Export button
83
- if (exportBtn) {
84
- exportBtn.addEventListener('click', exportToPDF);
85
- }
86
-
87
- // GitHub button
88
- if (githubBtn) {
89
- githubBtn.addEventListener('click', () => {
90
- window.open('https://github.com/Adeyemi0/FinSight-RAG-Application-', '_blank');
91
- });
92
- }
93
-
94
- // History dropdown
95
- if (historySelect) {
96
- historySelect.addEventListener('change', (e) => {
97
- const query = e.target.value;
98
- if (query && queryInput) {
99
- queryInput.value = query;
100
- }
101
- });
102
- }
103
- }
104
-
105
- // Update history dropdown
106
- function updateHistoryDropdown() {
107
- if (!historySelect) return;
108
-
109
- historySelect.innerHTML = '<option value="">Select a recent query</option>';
110
-
111
- queryHistory.slice().reverse().forEach((item, index) => {
112
- const option = document.createElement('option');
113
- option.value = item.query;
114
- option.textContent = item.query.substring(0, 60) + (item.query.length > 60 ? '...' : '');
115
- historySelect.appendChild(option);
116
- });
117
- }
118
-
119
- // Save to history
120
- function saveToHistory(query, answer) {
121
- queryHistory.push({
122
- query,
123
- answer: answer.substring(0, 500),
124
- timestamp: new Date().toISOString()
125
- });
126
-
127
- // Keep last 20
128
- if (queryHistory.length > 20) {
129
- queryHistory = queryHistory.slice(-20);
130
- }
131
-
132
- try {
133
- sessionStorage.setItem('queryHistory', JSON.stringify(queryHistory));
134
- updateHistoryDropdown();
135
- } catch (e) {
136
- console.error('Failed to save history:', e);
137
- }
138
- }
139
-
140
- // Main submit handler
141
- async function handleSubmit() {
142
- if (!queryInput) return;
143
-
144
- const query = queryInput.value.trim();
145
-
146
- if (!query) {
147
- showError('Please enter a question');
148
- return;
149
- }
150
-
151
- // Show loading
152
- setLoading(true);
153
- hideError();
154
-
155
- // Hide initial state, show loading
156
- if (initialState) initialState.style.display = 'none';
157
- if (loadingState) loadingState.style.display = 'flex';
158
- if (responseSection) responseSection.style.display = 'none';
159
-
160
- try {
161
- const requestData = {
162
- query,
163
- ticker: 'ACM', // Fixed ticker
164
- doc_types: null, // No filter
165
- top_k: 10,
166
- session_id: currentSessionId
167
- };
168
-
169
- // Use API_BASE_URL which auto-detects for Hugging Face
170
- const response = await fetch(`${API_BASE_URL}/query`, {
171
- method: 'POST',
172
- headers: { 'Content-Type': 'application/json' },
173
- body: JSON.stringify(requestData)
174
- });
175
-
176
- if (!response.ok) {
177
- const errorData = await response.json().catch(() => ({}));
178
- throw new Error(errorData.detail || `HTTP ${response.status}`);
179
- }
180
-
181
- const data = await response.json();
182
-
183
- if (!data) {
184
- throw new Error('Empty response from server');
185
- }
186
-
187
- // Store current data for export
188
- currentQuery = query;
189
- currentAnswer = data.answer;
190
- currentSources = data.sources;
191
-
192
- // Save to history
193
- saveToHistory(query, data.answer || '');
194
-
195
- // Display results
196
- displayResults(data);
197
-
198
- // Clear input
199
- queryInput.value = '';
200
-
201
- } catch (error) {
202
- console.error('Error:', error);
203
- showError(error.message || 'Failed to process query');
204
-
205
- // Show initial state again
206
- if (loadingState) loadingState.style.display = 'none';
207
- if (initialState) initialState.style.display = 'flex';
208
-
209
- } finally {
210
- setLoading(false);
211
- }
212
- }
213
-
214
- // Display results
215
- function displayResults(data) {
216
- if (!data) return;
217
-
218
- // Hide loading, show response
219
- if (loadingState) loadingState.style.display = 'none';
220
- if (responseSection) responseSection.style.display = 'block';
221
-
222
- // Display answer with cache indicator
223
- if (answerDiv) {
224
- let answerHTML = formatAnswer(data.answer || 'No answer available');
225
-
226
- // Add cache indicator if from cache
227
- if (data.from_cache) {
228
- const cacheAge = Math.floor((data.cache_age_seconds || 0) / 60);
229
- const ageText = cacheAge < 1 ? 'just now' : `${cacheAge}m ago`;
230
- answerHTML = `
231
- <div class="cache-indicator">
232
- <span class="cache-badge">⚡ Cached</span>
233
- <span class="cache-details">Retrieved ${ageText} • Hit ${data.cache_hits || 1}x</span>
234
- </div>
235
- ` + answerHTML;
236
- }
237
-
238
- answerDiv.innerHTML = answerHTML;
239
- }
240
-
241
- // Display expanded queries
242
- if (expandedQueriesDiv && queriesList) {
243
- if (data.expanded_queries && data.expanded_queries.length > 1) {
244
- expandedQueriesDiv.style.display = 'block';
245
- queriesList.innerHTML = data.expanded_queries
246
- .map(q => `<li>${escapeHtml(q)}</li>`)
247
- .join('');
248
- } else {
249
- expandedQueriesDiv.style.display = 'none';
250
- }
251
- }
252
-
253
- // Display sources
254
- if (sourcesDiv) {
255
- displaySources(data.sources || []);
256
- }
257
-
258
- // Scroll to top of right panel
259
- const rightPanel = document.querySelector('.right-panel');
260
- if (rightPanel) {
261
- rightPanel.scrollTop = 0;
262
- }
263
- }
264
-
265
- // Format answer with enhanced styling
266
- function formatAnswer(answer) {
267
- if (!answer) return '';
268
-
269
- let formatted = escapeHtml(answer);
270
-
271
- // Line breaks
272
- formatted = formatted.replace(/\n/g, '<br>');
273
-
274
- // Citations
275
- formatted = formatted.replace(/\[Source (\d+)\]/g,
276
- '<span class="citation">[Source $1]</span>');
277
-
278
- // Highlight numbers (currency, percentages, ratios)
279
- formatted = formatted.replace(/\$[\d,]+\.?\d*[BM]?/g,
280
- match => `<span class="highlight-number">${match}</span>`);
281
- formatted = formatted.replace(/\d+\.?\d*%/g,
282
- match => `<span class="highlight-number">${match}</span>`);
283
-
284
- // Color-code metrics
285
- formatted = formatted.replace(/(\d+\.?\d+)(x|:1)/g,
286
- '<span class="metric-green">$1$2</span>');
287
-
288
- // Bold headers (lines ending with:)
289
- formatted = formatted.replace(/^(.+:)$/gm, '<strong>$1</strong>');
290
-
291
- // Create calculation boxes for formulas
292
- formatted = formatted.replace(/Formula: ([^\n]+)/g,
293
- '<div class="calculation-box"><h4>Formula</h4><div class="calculation-step">$1</div></div>');
294
-
295
- return formatted;
296
- }
297
-
298
- // Display sources with collapsible cards
299
- function displaySources(sources) {
300
- if (!sourcesDiv) return;
301
-
302
- if (!sources || sources.length === 0) {
303
- sourcesDiv.innerHTML = '<p style="color: var(--text-muted);">No sources available</p>';
304
- return;
305
- }
306
-
307
- sourcesDiv.innerHTML = sources.map((source, index) => {
308
- if (!source) return '';
309
-
310
- const docTypeLabel = source.doc_type === '10k' ? '10-K Report' :
311
- source.doc_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
312
-
313
- return `
314
- <div class="source-card" id="source-${index}">
315
- <div class="source-header" onclick="toggleSource(${index})">
316
- <div class="source-title">
317
- <span class="source-badge">${docTypeLabel}</span>
318
- ${escapeHtml(source.filename || 'Unknown')}
319
- </div>
320
- <div class="source-similarity">
321
- Similarity Score: <strong>${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}</strong>
322
- </div>
323
- </div>
324
- <div class="source-content">
325
- <div class="source-details">
326
- ${source.ticker ? `<span class="source-detail"><strong>Ticker:</strong> ${escapeHtml(source.ticker)}</span>` : ''}
327
- ${source.chunk_id ? `<span class="source-detail"><strong>Chunk:</strong> ${escapeHtml(source.chunk_id)}</span>` : ''}
328
- </div>
329
- <div class="source-preview">
330
- "${escapeHtml(source.text_preview || 'No preview available')}"
331
- </div>
332
- </div>
333
- </div>
334
- `;
335
- }).filter(Boolean).join('');
336
- }
337
-
338
- // Toggle source expansion
339
- function toggleSource(index) {
340
- const card = document.getElementById(`source-${index}`);
341
- if (card) {
342
- card.classList.toggle('expanded');
343
- }
344
- }
345
-
346
- // Export to PDF function
347
- function exportToPDF() {
348
- if (!currentAnswer) {
349
- showError('No answer to export. Please ask a question first.');
350
- return;
351
- }
352
-
353
- try {
354
- // Check if jsPDF is loaded
355
- if (typeof window.jspdf === 'undefined') {
356
- showError('PDF library not loaded. Please refresh the page.');
357
- return;
358
- }
359
-
360
- const { jsPDF } = window.jspdf;
361
- const doc = new jsPDF();
362
-
363
- // Set title
364
- doc.setFontSize(18);
365
- doc.setFont(undefined, 'bold');
366
- doc.text('FinSight Analytics Report', 20, 20);
367
-
368
- // Set subtitle
369
- doc.setFontSize(10);
370
- doc.setFont(undefined, 'normal');
371
- doc.setTextColor(100);
372
- doc.text(`Generated on ${new Date().toLocaleString()}`, 20, 28);
373
-
374
- // Query section
375
- doc.setFontSize(12);
376
- doc.setFont(undefined, 'bold');
377
- doc.setTextColor(0);
378
- doc.text('Query:', 20, 40);
379
-
380
- doc.setFont(undefined, 'normal');
381
- doc.setFontSize(10);
382
- const queryLines = doc.splitTextToSize(currentQuery || '', 170);
383
- doc.text(queryLines, 20, 48);
384
-
385
- // Answer section
386
- let yPos = 48 + (queryLines.length * 7) + 10;
387
- doc.setFontSize(12);
388
- doc.setFont(undefined, 'bold');
389
- doc.text('Answer:', 20, yPos);
390
-
391
- yPos += 8;
392
- doc.setFont(undefined, 'normal');
393
- doc.setFontSize(10);
394
-
395
- // Clean answer text (remove HTML tags and format)
396
- let cleanAnswer = currentAnswer
397
- .replace(/<[^>]*>/g, '') // Remove HTML tags
398
- .replace(/\[Source \d+\]/g, '') // Remove citation markers
399
- .replace(/&nbsp;/g, ' ')
400
- .replace(/&lt;/g, '<')
401
- .replace(/&gt;/g, '>')
402
- .replace(/&amp;/g, '&');
403
-
404
- const answerLines = doc.splitTextToSize(cleanAnswer, 170);
405
-
406
- // Add answer with page breaks if needed
407
- answerLines.forEach((line, index) => {
408
- if (yPos > 270) {
409
- doc.addPage();
410
- yPos = 20;
411
- }
412
- doc.text(line, 20, yPos);
413
- yPos += 7;
414
- });
415
-
416
- // Sources section
417
- if (currentSources && currentSources.length > 0) {
418
- yPos += 10;
419
- if (yPos > 250) {
420
- doc.addPage();
421
- yPos = 20;
422
- }
423
-
424
- doc.setFontSize(12);
425
- doc.setFont(undefined, 'bold');
426
- doc.text('Sources:', 20, yPos);
427
- yPos += 8;
428
-
429
- doc.setFontSize(9);
430
- doc.setFont(undefined, 'normal');
431
-
432
- currentSources.forEach((source, index) => {
433
- if (yPos > 270) {
434
- doc.addPage();
435
- yPos = 20;
436
- }
437
-
438
- doc.setFont(undefined, 'bold');
439
- doc.text(`[${index + 1}] ${source.filename || 'Unknown'}`, 20, yPos);
440
- yPos += 5;
441
-
442
- doc.setFont(undefined, 'normal');
443
- doc.setTextColor(100);
444
- doc.text(`Type: ${source.doc_type || 'N/A'} | Similarity: ${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}`, 20, yPos);
445
- yPos += 8;
446
- doc.setTextColor(0);
447
- });
448
- }
449
-
450
- // Save PDF
451
- const filename = `FinSight_Analysis_${Date.now()}.pdf`;
452
- doc.save(filename);
453
-
454
- } catch (error) {
455
- console.error('PDF Export Error:', error);
456
- showError('Failed to export PDF. Please try again.');
457
- }
458
- }
459
-
460
- // Loading state
461
- function setLoading(isLoading) {
462
- if (!submitBtn) return;
463
-
464
- submitBtn.disabled = isLoading;
465
-
466
- if (btnText && loader) {
467
- btnText.style.display = isLoading ? 'none' : 'inline';
468
- loader.style.display = isLoading ? 'inline-block' : 'none';
469
- }
470
- }
471
-
472
- // Error handling
473
- function showError(message) {
474
- if (!errorToast || !errorMessage) {
475
- alert(message);
476
- return;
477
- }
478
-
479
- errorMessage.textContent = message;
480
- errorToast.style.display = 'block';
481
-
482
- setTimeout(() => {
483
- errorToast.style.display = 'none';
484
- }, 5000);
485
- }
486
-
487
- function hideError() {
488
- if (errorToast) {
489
- errorToast.style.display = 'none';
490
- }
491
- }
492
-
493
- // Utility: Escape HTML
494
- function escapeHtml(text) {
495
- if (!text) return '';
496
- const div = document.createElement('div');
497
- div.textContent = text;
498
- return div.innerHTML;
499
- }
500
-
501
- // Health check
502
- async function checkHealth() {
503
- try {
504
- const response = await fetch(`${API_BASE_URL}/health`, {
505
- signal: AbortSignal.timeout(5000)
506
- });
507
-
508
- if (!response.ok) {
509
- console.warn('API health check failed');
510
- } else {
511
- console.log('API health check passed');
512
- }
513
- } catch (error) {
514
- console.error('Cannot connect to API:', error);
515
- // Don't show error on page load for Hugging Face
516
- // The space might still be starting up
517
- }
518
- }
519
-
520
- // Make toggleSource available globally
521
- window.toggleSource = toggleSource;
522
-
523
- // Initialize on load
524
  init();
 
1
+ // API Configuration - Use relative URL for HuggingFace
2
+ const API_BASE_URL = window.location.origin;
3
+
4
+ // Session Management
5
+ let currentSessionId = null;
6
+ let currentAnswer = null;
7
+ let currentSources = null;
8
+ let currentQuery = null;
9
+ let queryHistory = [];
10
+
11
+ // DOM Elements
12
+ const queryInput = document.getElementById('queryInput');
13
+ const submitBtn = document.getElementById('submitBtn');
14
+ const btnText = submitBtn.querySelector('.btn-text');
15
+ const loader = submitBtn.querySelector('.loader');
16
+ const responseSection = document.getElementById('responseSection');
17
+ const initialState = document.getElementById('initialState');
18
+ const loadingState = document.getElementById('loadingState');
19
+ const answerDiv = document.getElementById('answer');
20
+ const sourcesDiv = document.getElementById('sources');
21
+ const errorToast = document.getElementById('errorToast');
22
+ const errorMessage = document.getElementById('errorMessage');
23
+ const expandedQueriesDiv = document.getElementById('expandedQueries');
24
+ const queriesList = document.getElementById('queriesList');
25
+ const exportBtn = document.getElementById('exportBtn');
26
+ const githubBtn = document.getElementById('githubBtn');
27
+ const historySelect = document.getElementById('historySelect');
28
+
29
+ // Initialize
30
+ function init() {
31
+ currentSessionId = sessionStorage.getItem('sessionId') || generateSessionId();
32
+ sessionStorage.setItem('sessionId', currentSessionId);
33
+
34
+ // Load history
35
+ const saved = sessionStorage.getItem('queryHistory');
36
+ if (saved) {
37
+ try {
38
+ queryHistory = JSON.parse(saved);
39
+ updateHistoryDropdown();
40
+ } catch (e) {
41
+ queryHistory = [];
42
+ }
43
+ }
44
+
45
+ setupEventListeners();
46
+ checkHealth();
47
+ }
48
+
49
+ function generateSessionId() {
50
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
51
+ }
52
+
53
+ function setupEventListeners() {
54
+ // Submit button
55
+ if (submitBtn) {
56
+ submitBtn.addEventListener('click', handleSubmit);
57
+ }
58
+
59
+ // Enter key (Ctrl+Enter)
60
+ if (queryInput) {
61
+ queryInput.addEventListener('keydown', (e) => {
62
+ if (e.key === 'Enter' && e.ctrlKey) {
63
+ handleSubmit();
64
+ }
65
+ });
66
+ }
67
+
68
+ // Example queries
69
+ document.querySelectorAll('.example-item').forEach(item => {
70
+ item.addEventListener('click', () => {
71
+ const query = item.getAttribute('data-query');
72
+ if (query && queryInput) {
73
+ queryInput.value = query;
74
+ handleSubmit();
75
+ }
76
+ });
77
+ });
78
+
79
+ // Export button
80
+ if (exportBtn) {
81
+ exportBtn.addEventListener('click', exportToPDF);
82
+ }
83
+
84
+ // GitHub button
85
+ if (githubBtn) {
86
+ githubBtn.addEventListener('click', () => {
87
+ window.open('https://github.com/Adeyemi0/FinSight-RAG-Application-', '_blank');
88
+ });
89
+ }
90
+
91
+ // History dropdown
92
+ if (historySelect) {
93
+ historySelect.addEventListener('change', (e) => {
94
+ const query = e.target.value;
95
+ if (query && queryInput) {
96
+ queryInput.value = query;
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ // Update history dropdown
103
+ function updateHistoryDropdown() {
104
+ if (!historySelect) return;
105
+
106
+ historySelect.innerHTML = '<option value="">Select a recent query</option>';
107
+
108
+ queryHistory.slice().reverse().forEach((item, index) => {
109
+ const option = document.createElement('option');
110
+ option.value = item.query;
111
+ option.textContent = item.query.substring(0, 60) + (item.query.length > 60 ? '...' : '');
112
+ historySelect.appendChild(option);
113
+ });
114
+ }
115
+
116
+ // Save to history
117
+ function saveToHistory(query, answer) {
118
+ queryHistory.push({
119
+ query,
120
+ answer: answer.substring(0, 500),
121
+ timestamp: new Date().toISOString()
122
+ });
123
+
124
+ // Keep last 20
125
+ if (queryHistory.length > 20) {
126
+ queryHistory = queryHistory.slice(-20);
127
+ }
128
+
129
+ try {
130
+ sessionStorage.setItem('queryHistory', JSON.stringify(queryHistory));
131
+ updateHistoryDropdown();
132
+ } catch (e) {
133
+ console.error('Failed to save history:', e);
134
+ }
135
+ }
136
+
137
+ // Main submit handler
138
+ async function handleSubmit() {
139
+ if (!queryInput) return;
140
+
141
+ const query = queryInput.value.trim();
142
+
143
+ if (!query) {
144
+ showError('Please enter a question');
145
+ return;
146
+ }
147
+
148
+ // Show loading
149
+ setLoading(true);
150
+ hideError();
151
+
152
+ // Hide initial state, show loading
153
+ if (initialState) initialState.style.display = 'none';
154
+ if (loadingState) loadingState.style.display = 'flex';
155
+ if (responseSection) responseSection.style.display = 'none';
156
+
157
+ try {
158
+ // Extract ticker from query if mentioned (e.g., "What is AAPL's revenue?")
159
+ const tickerMatch = query.match(/\b([A-Z]{1,5})\b/);
160
+ const detectedTicker = tickerMatch ? tickerMatch[1] : null;
161
+
162
+ const requestData = {
163
+ query,
164
+ ticker: detectedTicker, // Auto-detect ticker or null for all companies
165
+ doc_types: null, // No filter
166
+ top_k: 10,
167
+ session_id: currentSessionId
168
+ };
169
+
170
+ console.log('Request data:', requestData);
171
+
172
+ const response = await fetch(`${API_BASE_URL}/query`, {
173
+ method: 'POST',
174
+ headers: { 'Content-Type': 'application/json' },
175
+ body: JSON.stringify(requestData)
176
+ });
177
+
178
+ if (!response.ok) {
179
+ const errorData = await response.json().catch(() => ({}));
180
+ throw new Error(errorData.detail || `HTTP ${response.status}`);
181
+ }
182
+
183
+ const data = await response.json();
184
+
185
+ if (!data) {
186
+ throw new Error('Empty response from server');
187
+ }
188
+
189
+ // Store current data for export
190
+ currentQuery = query;
191
+ currentAnswer = data.answer;
192
+ currentSources = data.sources;
193
+
194
+ // Save to history
195
+ saveToHistory(query, data.answer || '');
196
+
197
+ // Display results
198
+ displayResults(data);
199
+
200
+ // Clear input
201
+ queryInput.value = '';
202
+
203
+ } catch (error) {
204
+ console.error('Error:', error);
205
+ showError(error.message || 'Failed to process query');
206
+
207
+ // Show initial state again
208
+ if (loadingState) loadingState.style.display = 'none';
209
+ if (initialState) initialState.style.display = 'flex';
210
+
211
+ } finally {
212
+ setLoading(false);
213
+ }
214
+ }
215
+
216
+ // Display results
217
+ function displayResults(data) {
218
+ if (!data) return;
219
+
220
+ // Hide loading, show response
221
+ if (loadingState) loadingState.style.display = 'none';
222
+ if (responseSection) responseSection.style.display = 'block';
223
+
224
+ // Display answer
225
+ if (answerDiv) {
226
+ answerDiv.innerHTML = formatAnswer(data.answer || 'No answer available');
227
+ }
228
+
229
+ // Display expanded queries
230
+ if (expandedQueriesDiv && queriesList) {
231
+ if (data.expanded_queries && data.expanded_queries.length > 1) {
232
+ expandedQueriesDiv.style.display = 'block';
233
+ queriesList.innerHTML = data.expanded_queries
234
+ .map(q => `<li>${escapeHtml(q)}</li>`)
235
+ .join('');
236
+ } else {
237
+ expandedQueriesDiv.style.display = 'none';
238
+ }
239
+ }
240
+
241
+ // Display sources
242
+ if (sourcesDiv) {
243
+ displaySources(data.sources || []);
244
+ }
245
+
246
+ // Scroll to top of right panel
247
+ const rightPanel = document.querySelector('.right-panel');
248
+ if (rightPanel) {
249
+ rightPanel.scrollTop = 0;
250
+ }
251
+ }
252
+
253
+ // Format answer with enhanced styling
254
+ function formatAnswer(answer) {
255
+ if (!answer) return '';
256
+
257
+ let formatted = escapeHtml(answer);
258
+
259
+ // Line breaks
260
+ formatted = formatted.replace(/\n/g, '<br>');
261
+
262
+ // Citations
263
+ formatted = formatted.replace(/\[Source (\d+)\]/g,
264
+ '<span class="citation">[Source $1]</span>');
265
+
266
+ // Highlight numbers (currency, percentages, ratios)
267
+ formatted = formatted.replace(/\$[\d,]+\.?\d*[BM]?/g,
268
+ match => `<span class="highlight-number">${match}</span>`);
269
+ formatted = formatted.replace(/\d+\.?\d*%/g,
270
+ match => `<span class="highlight-number">${match}</span>`);
271
+
272
+ // Color-code metrics
273
+ formatted = formatted.replace(/(\d+\.?\d+)(x|:1)/g,
274
+ '<span class="metric-green">$1$2</span>');
275
+
276
+ // Bold headers (lines ending with:)
277
+ formatted = formatted.replace(/^(.+:)$/gm, '<strong>$1</strong>');
278
+
279
+ // Create calculation boxes for formulas
280
+ formatted = formatted.replace(/Formula: ([^\n]+)/g,
281
+ '<div class="calculation-box"><h4>Formula</h4><div class="calculation-step">$1</div></div>');
282
+
283
+ return formatted;
284
+ }
285
+
286
+ // Display sources with collapsible cards
287
+ function displaySources(sources) {
288
+ if (!sourcesDiv) return;
289
+
290
+ if (!sources || sources.length === 0) {
291
+ sourcesDiv.innerHTML = '<p style="color: var(--text-muted);">No sources available</p>';
292
+ return;
293
+ }
294
+
295
+ sourcesDiv.innerHTML = sources.map((source, index) => {
296
+ if (!source) return '';
297
+
298
+ const docTypeLabel = source.doc_type === '10k' ? '10-K Report' :
299
+ source.doc_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
300
+
301
+ return `
302
+ <div class="source-card" id="source-${index}">
303
+ <div class="source-header" onclick="toggleSource(${index})">
304
+ <div class="source-title">
305
+ <span class="source-badge">${docTypeLabel}</span>
306
+ ${escapeHtml(source.filename || 'Unknown')}
307
+ </div>
308
+ <div class="source-similarity">
309
+ Similarity Score: <strong>${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}</strong>
310
+ </div>
311
+ </div>
312
+ <div class="source-content">
313
+ <div class="source-details">
314
+ ${source.ticker ? `<span class="source-detail"><strong>Ticker:</strong> ${escapeHtml(source.ticker)}</span>` : ''}
315
+ ${source.chunk_id ? `<span class="source-detail"><strong>Chunk:</strong> ${escapeHtml(source.chunk_id)}</span>` : ''}
316
+ </div>
317
+ <div class="source-preview">
318
+ "${escapeHtml(source.text_preview || 'No preview available')}"
319
+ </div>
320
+ </div>
321
+ </div>
322
+ `;
323
+ }).filter(Boolean).join('');
324
+ }
325
+
326
+ // Toggle source expansion
327
+ function toggleSource(index) {
328
+ const card = document.getElementById(`source-${index}`);
329
+ if (card) {
330
+ card.classList.toggle('expanded');
331
+ }
332
+ }
333
+
334
+ // Export to PDF function
335
+ function exportToPDF() {
336
+ if (!currentAnswer) {
337
+ showError('No answer to export. Please ask a question first.');
338
+ return;
339
+ }
340
+
341
+ try {
342
+ // Check if jsPDF is loaded
343
+ if (typeof window.jspdf === 'undefined') {
344
+ showError('PDF library not loaded. Please refresh the page.');
345
+ return;
346
+ }
347
+
348
+ const { jsPDF } = window.jspdf;
349
+ const doc = new jsPDF();
350
+
351
+ // Set title
352
+ doc.setFontSize(18);
353
+ doc.setFont(undefined, 'bold');
354
+ doc.text('FinSage Analytics Report', 20, 20);
355
+
356
+ // Set subtitle
357
+ doc.setFontSize(10);
358
+ doc.setFont(undefined, 'normal');
359
+ doc.setTextColor(100);
360
+ doc.text(`Generated on ${new Date().toLocaleString()}`, 20, 28);
361
+
362
+ // Query section
363
+ doc.setFontSize(12);
364
+ doc.setFont(undefined, 'bold');
365
+ doc.setTextColor(0);
366
+ doc.text('Query:', 20, 40);
367
+
368
+ doc.setFont(undefined, 'normal');
369
+ doc.setFontSize(10);
370
+ const queryLines = doc.splitTextToSize(currentQuery || '', 170);
371
+ doc.text(queryLines, 20, 48);
372
+
373
+ // Answer section
374
+ let yPos = 48 + (queryLines.length * 7) + 10;
375
+ doc.setFontSize(12);
376
+ doc.setFont(undefined, 'bold');
377
+ doc.text('Answer:', 20, yPos);
378
+
379
+ yPos += 8;
380
+ doc.setFont(undefined, 'normal');
381
+ doc.setFontSize(10);
382
+
383
+ // Clean answer text (remove HTML tags and format)
384
+ let cleanAnswer = currentAnswer
385
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
386
+ .replace(/\[Source \d+\]/g, '') // Remove citation markers
387
+ .replace(/&nbsp;/g, ' ')
388
+ .replace(/&lt;/g, '<')
389
+ .replace(/&gt;/g, '>')
390
+ .replace(/&amp;/g, '&');
391
+
392
+ const answerLines = doc.splitTextToSize(cleanAnswer, 170);
393
+
394
+ // Add answer with page breaks if needed
395
+ answerLines.forEach((line, index) => {
396
+ if (yPos > 270) {
397
+ doc.addPage();
398
+ yPos = 20;
399
+ }
400
+ doc.text(line, 20, yPos);
401
+ yPos += 7;
402
+ });
403
+
404
+ // Sources section
405
+ if (currentSources && currentSources.length > 0) {
406
+ yPos += 10;
407
+ if (yPos > 250) {
408
+ doc.addPage();
409
+ yPos = 20;
410
+ }
411
+
412
+ doc.setFontSize(12);
413
+ doc.setFont(undefined, 'bold');
414
+ doc.text('Sources:', 20, yPos);
415
+ yPos += 8;
416
+
417
+ doc.setFontSize(9);
418
+ doc.setFont(undefined, 'normal');
419
+
420
+ currentSources.forEach((source, index) => {
421
+ if (yPos > 270) {
422
+ doc.addPage();
423
+ yPos = 20;
424
+ }
425
+
426
+ doc.setFont(undefined, 'bold');
427
+ doc.text(`[${index + 1}] ${source.filename || 'Unknown'}`, 20, yPos);
428
+ yPos += 5;
429
+
430
+ doc.setFont(undefined, 'normal');
431
+ doc.setTextColor(100);
432
+ doc.text(`Type: ${source.doc_type || 'N/A'} | Similarity: ${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}`, 20, yPos);
433
+ yPos += 8;
434
+ doc.setTextColor(0);
435
+ });
436
+ }
437
+
438
+ // Save PDF
439
+ const filename = `FinSage_Analysis_${Date.now()}.pdf`;
440
+ doc.save(filename);
441
+
442
+ } catch (error) {
443
+ console.error('PDF Export Error:', error);
444
+ showError('Failed to export PDF. Please try again.');
445
+ }
446
+ }
447
+
448
+ // Loading state
449
+ function setLoading(isLoading) {
450
+ if (!submitBtn) return;
451
+
452
+ submitBtn.disabled = isLoading;
453
+
454
+ if (btnText && loader) {
455
+ btnText.style.display = isLoading ? 'none' : 'inline';
456
+ loader.style.display = isLoading ? 'inline-block' : 'none';
457
+ }
458
+ }
459
+
460
+ // Error handling
461
+ function showError(message) {
462
+ if (!errorToast || !errorMessage) {
463
+ alert(message);
464
+ return;
465
+ }
466
+
467
+ errorMessage.textContent = message;
468
+ errorToast.style.display = 'block';
469
+
470
+ setTimeout(() => {
471
+ errorToast.style.display = 'none';
472
+ }, 5000);
473
+ }
474
+
475
+ function hideError() {
476
+ if (errorToast) {
477
+ errorToast.style.display = 'none';
478
+ }
479
+ }
480
+
481
+ // Utility: Escape HTML
482
+ function escapeHtml(text) {
483
+ if (!text) return '';
484
+ const div = document.createElement('div');
485
+ div.textContent = text;
486
+ return div.innerHTML;
487
+ }
488
+
489
+ // Health check
490
+ async function checkHealth() {
491
+ try {
492
+ const response = await fetch(`${API_BASE_URL}/health`, {
493
+ signal: AbortSignal.timeout(5000)
494
+ });
495
+
496
+ if (!response.ok) {
497
+ console.warn('API health check failed');
498
+ }
499
+ } catch (error) {
500
+ console.error('Cannot connect to API:', error);
501
+ showError('Backend API is not responding. Please start the server.');
502
+ }
503
+ }
504
+
505
+ // Make toggleSource available globally
506
+ window.toggleSource = toggleSource;
507
+
508
+ // Initialize on load
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
  init();