adeyemi001 commited on
Commit
dfcd245
·
verified ·
1 Parent(s): 297c2b0

Update frontend/script.js

Browse files
Files changed (1) hide show
  1. frontend/script.js +267 -170
frontend/script.js CHANGED
@@ -3,9 +3,7 @@ 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
@@ -149,25 +147,26 @@ async function handleSubmit() {
149
  setLoading(true);
150
  hideError();
151
 
152
- // DON'T hide the current response - just show loading indicator
153
- // Users can still see their previous answer while waiting
154
  if (loadingState) loadingState.style.display = 'flex';
155
 
156
- // Keep response section visible if it exists (for follow-up questions)
157
- // Only hide initial state on first query
158
- if (initialState && !responseSection.style.display) {
159
  initialState.style.display = 'none';
160
  }
161
 
 
 
 
162
  try {
163
- // Extract ticker from query if mentioned (e.g., "What is AAPL's revenue?")
164
  const tickerMatch = query.match(/\b([A-Z]{1,5})\b/);
165
  const detectedTicker = tickerMatch ? tickerMatch[1] : null;
166
 
167
  const requestData = {
168
  query,
169
- ticker: detectedTicker, // Auto-detect ticker or null for all companies
170
- doc_types: null, // No filter
171
  top_k: 10,
172
  session_id: currentSessionId
173
  };
@@ -191,67 +190,237 @@ async function handleSubmit() {
191
  throw new Error('Empty response from server');
192
  }
193
 
194
- // Store current data for export
195
- currentQuery = query;
196
- currentAnswer = data.answer;
197
- currentSources = data.sources;
 
 
 
 
198
 
199
  // Save to history
200
  saveToHistory(query, data.answer || '');
201
 
202
- // Display results
203
- displayResults(data);
204
 
205
  // Clear input
206
  queryInput.value = '';
207
 
 
 
 
 
 
 
 
 
208
  } catch (error) {
209
  console.error('Error:', error);
210
  showError(error.message || 'Failed to process query');
211
 
212
- // Show initial state again
213
- if (loadingState) loadingState.style.display = 'none';
214
- if (initialState) initialState.style.display = 'flex';
 
 
215
 
216
  } finally {
217
  setLoading(false);
 
218
  }
219
  }
220
 
221
- // Display results
222
- function displayResults(data) {
223
- if (!data) return;
224
-
225
- // Hide loading, show response
226
- if (loadingState) loadingState.style.display = 'none';
227
- if (responseSection) responseSection.style.display = 'block';
228
-
229
- // Display answer
230
- if (answerDiv) {
231
- answerDiv.innerHTML = formatAnswer(data.answer || 'No answer available');
232
- }
233
-
234
- // Display expanded queries
235
- if (expandedQueriesDiv && queriesList) {
236
- if (data.expanded_queries && data.expanded_queries.length > 1) {
237
- expandedQueriesDiv.style.display = 'block';
238
- queriesList.innerHTML = data.expanded_queries
239
- .map(q => `<li>${escapeHtml(q)}</li>`)
240
- .join('');
241
- } else {
242
- expandedQueriesDiv.style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
- // Display sources
247
- if (sourcesDiv) {
248
- displaySources(data.sources || []);
249
  }
250
-
251
- // Scroll to top of right panel
252
- const rightPanel = document.querySelector('.right-panel');
253
- if (rightPanel) {
254
- rightPanel.scrollTop = 0;
 
 
 
 
 
 
 
 
 
 
 
255
  }
256
  }
257
 
@@ -266,85 +435,32 @@ function formatAnswer(answer) {
266
 
267
  // Citations
268
  formatted = formatted.replace(/\[Source (\d+)\]/g,
269
- '<span class="citation">[Source $1]</span>');
270
 
271
  // Highlight numbers (currency, percentages, ratios)
272
  formatted = formatted.replace(/\$[\d,]+\.?\d*[BM]?/g,
273
- match => `<span class="highlight-number">${match}</span>`);
274
  formatted = formatted.replace(/\d+\.?\d*%/g,
275
- match => `<span class="highlight-number">${match}</span>`);
276
 
277
  // Color-code metrics
278
  formatted = formatted.replace(/(\d+\.?\d+)(x|:1)/g,
279
- '<span class="metric-green">$1$2</span>');
280
 
281
  // Bold headers (lines ending with:)
282
- formatted = formatted.replace(/^(.+:)$/gm, '<strong>$1</strong>');
283
-
284
- // Create calculation boxes for formulas
285
- formatted = formatted.replace(/Formula: ([^\n]+)/g,
286
- '<div class="calculation-box"><h4>Formula</h4><div class="calculation-step">$1</div></div>');
287
 
288
  return formatted;
289
  }
290
 
291
- // Display sources with collapsible cards
292
- function displaySources(sources) {
293
- if (!sourcesDiv) return;
294
-
295
- if (!sources || sources.length === 0) {
296
- sourcesDiv.innerHTML = '<p style="color: var(--text-muted);">No sources available</p>';
297
- return;
298
- }
299
-
300
- sourcesDiv.innerHTML = sources.map((source, index) => {
301
- if (!source) return '';
302
-
303
- const docTypeLabel = source.doc_type === '10k' ? '10-K Report' :
304
- source.doc_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
305
-
306
- return `
307
- <div class="source-card" id="source-${index}">
308
- <div class="source-header" onclick="toggleSource(${index})">
309
- <div class="source-title">
310
- <span class="source-badge">${docTypeLabel}</span>
311
- ${escapeHtml(source.filename || 'Unknown')}
312
- </div>
313
- <div class="source-similarity">
314
- Similarity Score: <strong>${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}</strong>
315
- </div>
316
- </div>
317
- <div class="source-content">
318
- <div class="source-details">
319
- ${source.ticker ? `<span class="source-detail"><strong>Ticker:</strong> ${escapeHtml(source.ticker)}</span>` : ''}
320
- ${source.chunk_id ? `<span class="source-detail"><strong>Chunk:</strong> ${escapeHtml(source.chunk_id)}</span>` : ''}
321
- </div>
322
- <div class="source-preview">
323
- "${escapeHtml(source.text_preview || 'No preview available')}"
324
- </div>
325
- </div>
326
- </div>
327
- `;
328
- }).filter(Boolean).join('');
329
- }
330
-
331
- // Toggle source expansion
332
- function toggleSource(index) {
333
- const card = document.getElementById(`source-${index}`);
334
- if (card) {
335
- card.classList.toggle('expanded');
336
- }
337
- }
338
-
339
  // Export to PDF function
340
  function exportToPDF() {
341
- if (!currentAnswer) {
342
- showError('No answer to export. Please ask a question first.');
343
  return;
344
  }
345
 
346
  try {
347
- // Check if jsPDF is loaded
348
  if (typeof window.jspdf === 'undefined') {
349
  showError('PDF library not loaded. Please refresh the page.');
350
  return;
@@ -358,90 +474,71 @@ function exportToPDF() {
358
  doc.setFont(undefined, 'bold');
359
  doc.text('FinSage Analytics Report', 20, 20);
360
 
361
- // Set subtitle
362
  doc.setFontSize(10);
363
  doc.setFont(undefined, 'normal');
364
  doc.setTextColor(100);
365
  doc.text(`Generated on ${new Date().toLocaleString()}`, 20, 28);
 
366
 
367
- // Query section
368
- doc.setFontSize(12);
369
- doc.setFont(undefined, 'bold');
370
- doc.setTextColor(0);
371
- doc.text('Query:', 20, 40);
372
-
373
- doc.setFont(undefined, 'normal');
374
- doc.setFontSize(10);
375
- const queryLines = doc.splitTextToSize(currentQuery || '', 170);
376
- doc.text(queryLines, 20, 48);
377
-
378
- // Answer section
379
- let yPos = 48 + (queryLines.length * 7) + 10;
380
- doc.setFontSize(12);
381
- doc.setFont(undefined, 'bold');
382
- doc.text('Answer:', 20, yPos);
383
-
384
- yPos += 8;
385
- doc.setFont(undefined, 'normal');
386
- doc.setFontSize(10);
387
-
388
- // Clean answer text (remove HTML tags and format)
389
- let cleanAnswer = currentAnswer
390
- .replace(/<[^>]*>/g, '') // Remove HTML tags
391
- .replace(/\[Source \d+\]/g, '') // Remove citation markers
392
- .replace(/&nbsp;/g, ' ')
393
- .replace(/&lt;/g, '<')
394
- .replace(/&gt;/g, '>')
395
- .replace(/&amp;/g, '&');
396
 
397
- const answerLines = doc.splitTextToSize(cleanAnswer, 170);
398
-
399
- // Add answer with page breaks if needed
400
- answerLines.forEach((line, index) => {
401
- if (yPos > 270) {
402
  doc.addPage();
403
  yPos = 20;
404
  }
405
- doc.text(line, 20, yPos);
406
- yPos += 7;
407
- });
408
-
409
- // Sources section
410
- if (currentSources && currentSources.length > 0) {
411
- yPos += 10;
 
 
 
 
 
 
 
 
412
  if (yPos > 250) {
413
  doc.addPage();
414
  yPos = 20;
415
  }
416
 
417
- doc.setFontSize(12);
418
  doc.setFont(undefined, 'bold');
419
- doc.text('Sources:', 20, yPos);
420
  yPos += 8;
421
 
422
- doc.setFontSize(9);
423
  doc.setFont(undefined, 'normal');
 
 
 
 
 
 
 
 
 
424
 
425
- currentSources.forEach((source, index) => {
 
 
426
  if (yPos > 270) {
427
  doc.addPage();
428
  yPos = 20;
429
  }
430
-
431
- doc.setFont(undefined, 'bold');
432
- doc.text(`[${index + 1}] ${source.filename || 'Unknown'}`, 20, yPos);
433
- yPos += 5;
434
-
435
- doc.setFont(undefined, 'normal');
436
- doc.setTextColor(100);
437
- doc.text(`Type: ${source.doc_type || 'N/A'} | Similarity: ${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}`, 20, yPos);
438
- yPos += 8;
439
- doc.setTextColor(0);
440
  });
441
- }
 
 
442
 
443
- // Save PDF
444
- const filename = `FinSage_Analysis_${Date.now()}.pdf`;
445
  doc.save(filename);
446
 
447
  } catch (error) {
 
3
 
4
  // Session Management
5
  let currentSessionId = null;
6
+ let conversationHistory = []; // Store all Q&A pairs
 
 
7
  let queryHistory = [];
8
 
9
  // DOM Elements
 
147
  setLoading(true);
148
  hideError();
149
 
150
+ // Show loading indicator
 
151
  if (loadingState) loadingState.style.display = 'flex';
152
 
153
+ // Hide initial state on first query
154
+ if (initialState && conversationHistory.length === 0) {
 
155
  initialState.style.display = 'none';
156
  }
157
 
158
+ // Show response section
159
+ if (responseSection) responseSection.style.display = 'block';
160
+
161
  try {
162
+ // Extract ticker from query if mentioned
163
  const tickerMatch = query.match(/\b([A-Z]{1,5})\b/);
164
  const detectedTicker = tickerMatch ? tickerMatch[1] : null;
165
 
166
  const requestData = {
167
  query,
168
+ ticker: detectedTicker,
169
+ doc_types: null,
170
  top_k: 10,
171
  session_id: currentSessionId
172
  };
 
190
  throw new Error('Empty response from server');
191
  }
192
 
193
+ // Add to conversation history
194
+ conversationHistory.push({
195
+ query,
196
+ answer: data.answer,
197
+ sources: data.sources,
198
+ expanded_queries: data.expanded_queries,
199
+ timestamp: new Date()
200
+ });
201
 
202
  // Save to history
203
  saveToHistory(query, data.answer || '');
204
 
205
+ // Display all conversation history
206
+ displayConversationHistory();
207
 
208
  // Clear input
209
  queryInput.value = '';
210
 
211
+ // Scroll to latest answer
212
+ setTimeout(() => {
213
+ const rightPanel = document.querySelector('.right-panel');
214
+ if (rightPanel) {
215
+ rightPanel.scrollTop = rightPanel.scrollHeight;
216
+ }
217
+ }, 100);
218
+
219
  } catch (error) {
220
  console.error('Error:', error);
221
  showError(error.message || 'Failed to process query');
222
 
223
+ // Show initial state again if no history
224
+ if (conversationHistory.length === 0) {
225
+ if (loadingState) loadingState.style.display = 'none';
226
+ if (initialState) initialState.style.display = 'flex';
227
+ }
228
 
229
  } finally {
230
  setLoading(false);
231
+ if (loadingState) loadingState.style.display = 'none';
232
  }
233
  }
234
 
235
+ // Display entire conversation history
236
+ function displayConversationHistory() {
237
+ if (!answerDiv || !sourcesDiv) return;
238
+
239
+ // Clear existing content
240
+ answerDiv.innerHTML = '';
241
+ sourcesDiv.innerHTML = '';
242
+
243
+ // Render each Q&A pair
244
+ conversationHistory.forEach((item, index) => {
245
+ // Create conversation item container
246
+ const conversationItem = document.createElement('div');
247
+ conversationItem.className = 'conversation-item';
248
+ conversationItem.style.cssText = `
249
+ margin-bottom: 2rem;
250
+ padding-bottom: 2rem;
251
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
252
+ `;
253
+
254
+ // Add question
255
+ const questionDiv = document.createElement('div');
256
+ questionDiv.className = 'question-block';
257
+ questionDiv.style.cssText = `
258
+ background: rgba(59, 130, 246, 0.1);
259
+ border-left: 3px solid #3b82f6;
260
+ padding: 1rem 1.5rem;
261
+ border-radius: 0.5rem;
262
+ margin-bottom: 1.5rem;
263
+ `;
264
+ questionDiv.innerHTML = `
265
+ <div style="color: #60a5fa; font-size: 0.875rem; margin-bottom: 0.5rem;">Question ${index + 1}</div>
266
+ <div style="color: #e5e7eb; font-size: 1rem;">${escapeHtml(item.query)}</div>
267
+ `;
268
+ conversationItem.appendChild(questionDiv);
269
+
270
+ // Add answer
271
+ const answerBlock = document.createElement('div');
272
+ answerBlock.className = 'answer-block';
273
+ answerBlock.style.cssText = `
274
+ background: rgba(16, 185, 129, 0.05);
275
+ border-left: 3px solid #10b981;
276
+ padding: 1rem 1.5rem;
277
+ border-radius: 0.5rem;
278
+ margin-bottom: 1.5rem;
279
+ `;
280
+ answerBlock.innerHTML = `
281
+ <div style="color: #34d399; font-size: 0.875rem; margin-bottom: 0.5rem;">Answer</div>
282
+ <div style="color: #e5e7eb; line-height: 1.7;">${formatAnswer(item.answer || 'No answer available')}</div>
283
+ `;
284
+ conversationItem.appendChild(answerBlock);
285
+
286
+ // Add expanded queries if available
287
+ if (item.expanded_queries && item.expanded_queries.length > 1) {
288
+ const expandedDiv = document.createElement('div');
289
+ expandedDiv.className = 'expanded-queries-block';
290
+ expandedDiv.style.cssText = `
291
+ background: rgba(139, 92, 246, 0.05);
292
+ border-left: 3px solid #8b5cf6;
293
+ padding: 1rem 1.5rem;
294
+ border-radius: 0.5rem;
295
+ margin-bottom: 1.5rem;
296
+ `;
297
+ expandedDiv.innerHTML = `
298
+ <div style="color: #a78bfa; font-size: 0.875rem; margin-bottom: 0.5rem;">Expanded Queries</div>
299
+ <ul style="margin: 0; padding-left: 1.5rem; color: #d1d5db;">
300
+ ${item.expanded_queries.map(q => `<li style="margin-bottom: 0.5rem;">${escapeHtml(q)}</li>`).join('')}
301
+ </ul>
302
+ `;
303
+ conversationItem.appendChild(expandedDiv);
304
  }
305
+
306
+ // Add sources
307
+ if (item.sources && item.sources.length > 0) {
308
+ const sourcesHeader = document.createElement('div');
309
+ sourcesHeader.style.cssText = `
310
+ color: #f59e0b;
311
+ font-size: 0.875rem;
312
+ margin-bottom: 1rem;
313
+ font-weight: 600;
314
+ `;
315
+ sourcesHeader.textContent = `Sources (${item.sources.length})`;
316
+ conversationItem.appendChild(sourcesHeader);
317
+
318
+ const sourcesContainer = document.createElement('div');
319
+ sourcesContainer.innerHTML = item.sources.map((source, sourceIndex) => {
320
+ if (!source) return '';
321
+
322
+ const docTypeLabel = source.doc_type === '10k' ? '10-K Report' :
323
+ source.doc_type.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase());
324
+
325
+ return `
326
+ <div class="source-card" id="source-${index}-${sourceIndex}" style="
327
+ background: rgba(31, 41, 55, 0.5);
328
+ border: 1px solid rgba(75, 85, 99, 0.5);
329
+ border-radius: 0.5rem;
330
+ margin-bottom: 0.75rem;
331
+ overflow: hidden;
332
+ transition: all 0.3s ease;
333
+ ">
334
+ <div class="source-header" onclick="toggleSource(${index}, ${sourceIndex})" style="
335
+ padding: 0.75rem 1rem;
336
+ cursor: pointer;
337
+ display: flex;
338
+ justify-content: space-between;
339
+ align-items: center;
340
+ background: rgba(17, 24, 39, 0.5);
341
+ ">
342
+ <div class="source-title" style="display: flex; align-items: center; gap: 0.5rem;">
343
+ <span class="source-badge" style="
344
+ background: rgba(59, 130, 246, 0.2);
345
+ color: #60a5fa;
346
+ padding: 0.25rem 0.5rem;
347
+ border-radius: 0.25rem;
348
+ font-size: 0.75rem;
349
+ font-weight: 600;
350
+ ">${docTypeLabel}</span>
351
+ <span style="color: #e5e7eb; font-size: 0.875rem;">${escapeHtml(source.filename || 'Unknown')}</span>
352
+ </div>
353
+ <div class="source-similarity" style="color: #9ca3af; font-size: 0.875rem;">
354
+ Score: <strong style="color: #10b981;">${source.similarity_score ? (source.similarity_score * 100).toFixed(0) + '%' : 'N/A'}</strong>
355
+ </div>
356
+ </div>
357
+ <div class="source-content" style="
358
+ max-height: 0;
359
+ overflow: hidden;
360
+ transition: max-height 0.3s ease;
361
+ padding: 0 1rem;
362
+ ">
363
+ <div class="source-details" style="
364
+ padding: 0.75rem 0;
365
+ display: flex;
366
+ gap: 1rem;
367
+ flex-wrap: wrap;
368
+ border-bottom: 1px solid rgba(75, 85, 99, 0.3);
369
+ ">
370
+ ${source.ticker ? `<span style="color: #9ca3af; font-size: 0.875rem;"><strong style="color: #e5e7eb;">Ticker:</strong> ${escapeHtml(source.ticker)}</span>` : ''}
371
+ ${source.chunk_id ? `<span style="color: #9ca3af; font-size: 0.875rem;"><strong style="color: #e5e7eb;">Chunk:</strong> ${escapeHtml(source.chunk_id)}</span>` : ''}
372
+ </div>
373
+ <div class="source-preview" style="
374
+ padding: 1rem 0;
375
+ color: #d1d5db;
376
+ font-size: 0.875rem;
377
+ line-height: 1.6;
378
+ font-style: italic;
379
+ ">
380
+ "${escapeHtml(source.text_preview || 'No preview available')}"
381
+ </div>
382
+ </div>
383
+ </div>
384
+ `;
385
+ }).filter(Boolean).join('');
386
+
387
+ conversationItem.appendChild(sourcesContainer);
388
+ }
389
+
390
+ // Add timestamp
391
+ const timestamp = document.createElement('div');
392
+ timestamp.style.cssText = `
393
+ color: #6b7280;
394
+ font-size: 0.75rem;
395
+ text-align: right;
396
+ margin-top: 1rem;
397
+ `;
398
+ timestamp.textContent = item.timestamp.toLocaleTimeString();
399
+ conversationItem.appendChild(timestamp);
400
+
401
+ answerDiv.appendChild(conversationItem);
402
+ });
403
 
404
+ // Show export button if there's content
405
+ if (exportBtn && conversationHistory.length > 0) {
406
+ exportBtn.style.display = 'flex';
407
  }
408
+ }
409
+
410
+ // Toggle source expansion
411
+ function toggleSource(conversationIndex, sourceIndex) {
412
+ const card = document.getElementById(`source-${conversationIndex}-${sourceIndex}`);
413
+ if (!card) return;
414
+
415
+ const content = card.querySelector('.source-content');
416
+ if (!content) return;
417
+
418
+ if (content.style.maxHeight && content.style.maxHeight !== '0px') {
419
+ content.style.maxHeight = '0px';
420
+ content.style.padding = '0 1rem';
421
+ } else {
422
+ content.style.maxHeight = content.scrollHeight + 'px';
423
+ content.style.padding = '0 1rem';
424
  }
425
  }
426
 
 
435
 
436
  // Citations
437
  formatted = formatted.replace(/\[Source (\d+)\]/g,
438
+ '<span class="citation" style="background: rgba(59, 130, 246, 0.2); color: #60a5fa; padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875rem; font-weight: 600;">[Source $1]</span>');
439
 
440
  // Highlight numbers (currency, percentages, ratios)
441
  formatted = formatted.replace(/\$[\d,]+\.?\d*[BM]?/g,
442
+ match => `<span style="color: #10b981; font-weight: 600;">${match}</span>`);
443
  formatted = formatted.replace(/\d+\.?\d*%/g,
444
+ match => `<span style="color: #10b981; font-weight: 600;">${match}</span>`);
445
 
446
  // Color-code metrics
447
  formatted = formatted.replace(/(\d+\.?\d+)(x|:1)/g,
448
+ '<span style="color: #34d399; font-weight: 600;">$1$2</span>');
449
 
450
  // Bold headers (lines ending with:)
451
+ formatted = formatted.replace(/^(.+:)$/gm, '<strong style="color: #f3f4f6;">$1</strong>');
 
 
 
 
452
 
453
  return formatted;
454
  }
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  // Export to PDF function
457
  function exportToPDF() {
458
+ if (conversationHistory.length === 0) {
459
+ showError('No conversation to export. Please ask a question first.');
460
  return;
461
  }
462
 
463
  try {
 
464
  if (typeof window.jspdf === 'undefined') {
465
  showError('PDF library not loaded. Please refresh the page.');
466
  return;
 
474
  doc.setFont(undefined, 'bold');
475
  doc.text('FinSage Analytics Report', 20, 20);
476
 
 
477
  doc.setFontSize(10);
478
  doc.setFont(undefined, 'normal');
479
  doc.setTextColor(100);
480
  doc.text(`Generated on ${new Date().toLocaleString()}`, 20, 28);
481
+ doc.text(`Total Questions: ${conversationHistory.length}`, 20, 34);
482
 
483
+ let yPos = 45;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
484
 
485
+ conversationHistory.forEach((item, index) => {
486
+ // Check for page break
487
+ if (yPos > 250) {
 
 
488
  doc.addPage();
489
  yPos = 20;
490
  }
491
+
492
+ // Question
493
+ doc.setFontSize(12);
494
+ doc.setFont(undefined, 'bold');
495
+ doc.setTextColor(0);
496
+ doc.text(`Question ${index + 1}:`, 20, yPos);
497
+ yPos += 8;
498
+
499
+ doc.setFont(undefined, 'normal');
500
+ doc.setFontSize(10);
501
+ const queryLines = doc.splitTextToSize(item.query, 170);
502
+ doc.text(queryLines, 20, yPos);
503
+ yPos += queryLines.length * 7 + 5;
504
+
505
+ // Answer
506
  if (yPos > 250) {
507
  doc.addPage();
508
  yPos = 20;
509
  }
510
 
511
+ doc.setFontSize(11);
512
  doc.setFont(undefined, 'bold');
513
+ doc.text('Answer:', 20, yPos);
514
  yPos += 8;
515
 
 
516
  doc.setFont(undefined, 'normal');
517
+ doc.setFontSize(10);
518
+
519
+ let cleanAnswer = item.answer
520
+ .replace(/<[^>]*>/g, '')
521
+ .replace(/\[Source \d+\]/g, '')
522
+ .replace(/&nbsp;/g, ' ')
523
+ .replace(/&lt;/g, '<')
524
+ .replace(/&gt;/g, '>')
525
+ .replace(/&amp;/g, '&');
526
 
527
+ const answerLines = doc.splitTextToSize(cleanAnswer, 170);
528
+
529
+ answerLines.forEach(line => {
530
  if (yPos > 270) {
531
  doc.addPage();
532
  yPos = 20;
533
  }
534
+ doc.text(line, 20, yPos);
535
+ yPos += 7;
 
 
 
 
 
 
 
 
536
  });
537
+
538
+ yPos += 10;
539
+ });
540
 
541
+ const filename = `FinSage_Conversation_${Date.now()}.pdf`;
 
542
  doc.save(filename);
543
 
544
  } catch (error) {