local-deep-research 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. local_deep_research/__init__.py +24 -0
  2. local_deep_research/citation_handler.py +113 -0
  3. local_deep_research/config.py +166 -0
  4. local_deep_research/defaults/__init__.py +44 -0
  5. local_deep_research/defaults/llm_config.py +269 -0
  6. local_deep_research/defaults/local_collections.toml +47 -0
  7. local_deep_research/defaults/main.toml +57 -0
  8. local_deep_research/defaults/search_engines.toml +244 -0
  9. local_deep_research/local_collections.py +141 -0
  10. local_deep_research/main.py +113 -0
  11. local_deep_research/report_generator.py +206 -0
  12. local_deep_research/search_system.py +241 -0
  13. local_deep_research/utilties/__init__.py +0 -0
  14. local_deep_research/utilties/enums.py +9 -0
  15. local_deep_research/utilties/llm_utils.py +116 -0
  16. local_deep_research/utilties/search_utilities.py +115 -0
  17. local_deep_research/utilties/setup_utils.py +6 -0
  18. local_deep_research/web/__init__.py +2 -0
  19. local_deep_research/web/app.py +1209 -0
  20. local_deep_research/web/static/css/styles.css +1008 -0
  21. local_deep_research/web/static/js/app.js +2078 -0
  22. local_deep_research/web/templates/api_keys_config.html +82 -0
  23. local_deep_research/web/templates/collections_config.html +90 -0
  24. local_deep_research/web/templates/index.html +312 -0
  25. local_deep_research/web/templates/llm_config.html +120 -0
  26. local_deep_research/web/templates/main_config.html +89 -0
  27. local_deep_research/web/templates/search_engines_config.html +154 -0
  28. local_deep_research/web/templates/settings.html +519 -0
  29. local_deep_research/web/templates/settings_dashboard.html +207 -0
  30. local_deep_research/web_search_engines/__init__.py +0 -0
  31. local_deep_research/web_search_engines/engines/__init__.py +0 -0
  32. local_deep_research/web_search_engines/engines/full_search.py +128 -0
  33. local_deep_research/web_search_engines/engines/meta_search_engine.py +274 -0
  34. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +367 -0
  35. local_deep_research/web_search_engines/engines/search_engine_brave.py +245 -0
  36. local_deep_research/web_search_engines/engines/search_engine_ddg.py +123 -0
  37. local_deep_research/web_search_engines/engines/search_engine_github.py +663 -0
  38. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +283 -0
  39. local_deep_research/web_search_engines/engines/search_engine_guardian.py +337 -0
  40. local_deep_research/web_search_engines/engines/search_engine_local.py +901 -0
  41. local_deep_research/web_search_engines/engines/search_engine_local_all.py +153 -0
  42. local_deep_research/web_search_engines/engines/search_engine_medrxiv.py +623 -0
  43. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +992 -0
  44. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +230 -0
  45. local_deep_research/web_search_engines/engines/search_engine_wayback.py +474 -0
  46. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +242 -0
  47. local_deep_research/web_search_engines/full_search.py +254 -0
  48. local_deep_research/web_search_engines/search_engine_base.py +197 -0
  49. local_deep_research/web_search_engines/search_engine_factory.py +233 -0
  50. local_deep_research/web_search_engines/search_engines_config.py +54 -0
  51. local_deep_research-0.1.0.dist-info/LICENSE +21 -0
  52. local_deep_research-0.1.0.dist-info/METADATA +328 -0
  53. local_deep_research-0.1.0.dist-info/RECORD +56 -0
  54. local_deep_research-0.1.0.dist-info/WHEEL +5 -0
  55. local_deep_research-0.1.0.dist-info/entry_points.txt +3 -0
  56. local_deep_research-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2078 @@
1
+ // Main application functionality
2
+ document.addEventListener('DOMContentLoaded', () => {
3
+ // Global socket variable - initialize as null
4
+ let socket = null;
5
+ let socketConnected = false;
6
+
7
+ // Global state variables
8
+ let isResearchInProgress = false;
9
+ let currentResearchId = null;
10
+ window.currentResearchId = null;
11
+
12
+ // Polling interval for research status
13
+ let pollingInterval = null;
14
+
15
+ // Sound notification variables
16
+ let successSound = null;
17
+ let errorSound = null;
18
+ let notificationsEnabled = true;
19
+
20
+ // Initialize notification sounds
21
+ function initializeSounds() {
22
+ successSound = new Audio('/research/static/sounds/success.mp3');
23
+ errorSound = new Audio('/research/static/sounds/error.mp3');
24
+ successSound.volume = 0.7;
25
+ errorSound.volume = 0.7;
26
+ }
27
+
28
+ // Function to play a notification sound
29
+ function playNotificationSound(type) {
30
+ console.log(`Attempting to play ${type} notification sound`);
31
+ if (!notificationsEnabled) {
32
+ console.log('Notifications are disabled');
33
+ return;
34
+ }
35
+
36
+ // Play sounds regardless of tab focus
37
+ if (type === 'success' && successSound) {
38
+ console.log('Playing success sound');
39
+ successSound.play().catch(err => console.error('Error playing success sound:', err));
40
+ } else if (type === 'error' && errorSound) {
41
+ console.log('Playing error sound');
42
+ errorSound.play().catch(err => console.error('Error playing error sound:', err));
43
+ } else {
44
+ console.warn(`Unknown sound type or sound not initialized: ${type}`);
45
+ }
46
+ }
47
+
48
+ // Initialize socket only when needed with a timeout for safety
49
+ function initializeSocket() {
50
+ if (socket) return socket; // Return existing socket if already initialized
51
+
52
+ console.log('Initializing socket connection...');
53
+ // Create new socket connection with optimized settings for threading mode
54
+ socket = io({
55
+ path: '/research/socket.io',
56
+ transports: ['websocket', 'polling'],
57
+ reconnection: true,
58
+ reconnectionAttempts: 3,
59
+ reconnectionDelay: 1000,
60
+ timeout: 5000,
61
+ autoConnect: true,
62
+ forceNew: true
63
+ });
64
+
65
+ // Add event handlers
66
+ socket.on('connect', () => {
67
+ console.log('Socket connected');
68
+ socketConnected = true;
69
+ });
70
+
71
+ socket.on('disconnect', () => {
72
+ console.log('Socket disconnected');
73
+ socketConnected = false;
74
+ });
75
+
76
+ socket.on('connect_error', (error) => {
77
+ console.error('Socket connection error:', error);
78
+ socketConnected = false;
79
+ });
80
+
81
+ // Set a timeout to detect hanging connections
82
+ setTimeout(() => {
83
+ if (!socketConnected) {
84
+ console.log('Socket connection timeout - forcing reconnect');
85
+ try {
86
+ socket.disconnect();
87
+ socket.connect();
88
+ } catch (e) {
89
+ console.error('Error during forced reconnect:', e);
90
+ }
91
+ }
92
+ }, 5000);
93
+
94
+ return socket;
95
+ }
96
+
97
+ // Function to safely disconnect socket
98
+ window.disconnectSocket = function() {
99
+ try {
100
+ if (socket) {
101
+ console.log('Manually disconnecting socket');
102
+ socket.removeAllListeners();
103
+ socket.disconnect();
104
+ socket = null;
105
+ socketConnected = false;
106
+ }
107
+ } catch (e) {
108
+ console.error('Error disconnecting socket:', e);
109
+ }
110
+ };
111
+
112
+ // Helper function to connect to research updates
113
+ window.connectToResearchSocket = function(researchId) {
114
+ try {
115
+ // Initialize socket if needed
116
+ if (!socket) {
117
+ socket = initializeSocket();
118
+ }
119
+
120
+ // Subscribe to research updates
121
+ socket.emit('subscribe_to_research', { research_id: researchId });
122
+
123
+ // Set up event listener for research progress
124
+ const progressEventName = `research_progress_${researchId}`;
125
+
126
+ // Remove existing listeners to prevent duplicates
127
+ socket.off(progressEventName);
128
+
129
+ // Add new listener
130
+ socket.on(progressEventName, (data) => {
131
+ console.log('Received research progress update:', data);
132
+ updateProgressUI(data.progress, data.status, data.message);
133
+
134
+ // If research is complete, show the completion buttons
135
+ if (data.status === 'completed' || data.status === 'terminated' || data.status === 'failed' || data.status === 'suspended') {
136
+ console.log(`Socket received research final state: ${data.status}`);
137
+
138
+ // Clear polling interval if it exists
139
+ if (pollingInterval) {
140
+ console.log('Clearing polling interval from socket event');
141
+ clearInterval(pollingInterval);
142
+ pollingInterval = null;
143
+ }
144
+
145
+ // Update navigation state
146
+ if (data.status === 'completed') {
147
+ isResearchInProgress = false;
148
+ }
149
+
150
+ // Update UI for completion
151
+ if (data.status === 'completed') {
152
+ console.log('Research completed via socket, loading results automatically');
153
+
154
+ // Hide terminate button
155
+ const terminateBtn = document.getElementById('terminate-research-btn');
156
+ if (terminateBtn) {
157
+ terminateBtn.style.display = 'none';
158
+ }
159
+
160
+ // Auto-load the results
161
+ loadResearch(researchId);
162
+ } else if (data.status === 'failed' || data.status === 'suspended') {
163
+ console.log(`Showing error message for status: ${data.status} from socket event`);
164
+ const errorMessage = document.getElementById('error-message');
165
+ if (errorMessage) {
166
+ errorMessage.style.display = 'block';
167
+ errorMessage.textContent = data.status === 'failed' ?
168
+ (data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
169
+ 'Research was suspended';
170
+ } else {
171
+ console.error('error-message element not found in socket handler');
172
+ }
173
+ }
174
+
175
+ updateNavigationBasedOnResearchStatus();
176
+
177
+ // Play notification sounds based on status
178
+ if (data.status === 'completed') {
179
+ console.log('Playing success notification sound from socket event');
180
+ playNotificationSound('success');
181
+ } else if (data.status === 'failed') {
182
+ console.log('Playing error notification sound from socket event');
183
+ playNotificationSound('error');
184
+ }
185
+
186
+ // Force the UI to update with a manual trigger
187
+ document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
188
+ }
189
+
190
+ // Update the detailed log if on details page
191
+ if (data.log_entry && document.getElementById('research-log')) {
192
+ updateDetailLogEntry(data.log_entry);
193
+ }
194
+ });
195
+
196
+ return true;
197
+ } catch (e) {
198
+ console.error('Error connecting to research socket:', e);
199
+ return false;
200
+ }
201
+ };
202
+
203
+ // Check for active research on page load
204
+ async function checkActiveResearch() {
205
+ try {
206
+ const response = await fetch(getApiUrl('/api/history'));
207
+ const history = await response.json();
208
+
209
+ // Find in-progress research
210
+ const activeResearch = history.find(item => item.status === 'in_progress');
211
+
212
+ if (activeResearch) {
213
+ isResearchInProgress = true;
214
+ currentResearchId = activeResearch.id;
215
+ window.currentResearchId = currentResearchId;
216
+
217
+ // Check if we're on the new research page and redirect to progress
218
+ const currentPage = document.querySelector('.page.active');
219
+
220
+ if (currentPage && currentPage.id === 'new-research') {
221
+ // Navigate to progress page
222
+ switchPage('research-progress');
223
+
224
+ // Connect to socket for this research
225
+ window.connectToResearchSocket(currentResearchId);
226
+
227
+ // Start polling for updates
228
+ pollResearchStatus(currentResearchId);
229
+ }
230
+ }
231
+ } catch (error) {
232
+ console.error('Error checking for active research:', error);
233
+ }
234
+ }
235
+
236
+ // Add unload event listener
237
+ window.addEventListener('beforeunload', function() {
238
+ window.disconnectSocket();
239
+ });
240
+
241
+ // Function to start research
242
+ async function startResearch(query, mode) {
243
+ // Check if research is already in progress
244
+ if (isResearchInProgress) {
245
+ alert('Another research is already in progress. Please wait for it to complete or check its status in the history tab.');
246
+ return;
247
+ }
248
+
249
+ // Get the start button
250
+ const startResearchBtn = document.getElementById('start-research-btn');
251
+ if (!startResearchBtn) return;
252
+
253
+ // Disable the start button while we attempt to start the research
254
+ startResearchBtn.disabled = true;
255
+ startResearchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
256
+
257
+ try {
258
+ const response = await fetch(getApiUrl('/api/start_research'), {
259
+ method: 'POST',
260
+ headers: {
261
+ 'Content-Type': 'application/json'
262
+ },
263
+ body: JSON.stringify({
264
+ query: query,
265
+ mode: mode
266
+ })
267
+ });
268
+
269
+ const data = await response.json();
270
+
271
+ if (data.status === 'success') {
272
+ isResearchInProgress = true;
273
+ currentResearchId = data.research_id;
274
+
275
+ // Also update the window object
276
+ window.currentResearchId = data.research_id;
277
+
278
+ // Update the navigation to show Research in Progress
279
+ updateNavigationBasedOnResearchStatus();
280
+
281
+ // Update progress page
282
+ document.getElementById('current-query').textContent = query;
283
+ document.getElementById('progress-fill').style.width = '0%';
284
+ document.getElementById('progress-percentage').textContent = '0%';
285
+ document.getElementById('progress-status').textContent = 'Initializing research process...';
286
+
287
+ // Navigate to progress page
288
+ switchPage('research-progress');
289
+
290
+ // Connect to socket for this research
291
+ window.connectToResearchSocket(data.research_id);
292
+
293
+ // Start polling for status
294
+ pollResearchStatus(data.research_id);
295
+
296
+ // Show the terminate button
297
+ const terminateBtn = document.getElementById('terminate-research-btn');
298
+ if (terminateBtn) {
299
+ terminateBtn.style.display = 'inline-flex';
300
+ terminateBtn.disabled = false;
301
+ }
302
+ } else {
303
+ alert('Error starting research: ' + (data.message || 'Unknown error'));
304
+ startResearchBtn.disabled = false;
305
+ startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
306
+ }
307
+ } catch (error) {
308
+ console.error('Error:', error);
309
+ alert('An error occurred while starting the research. Please try again.');
310
+ startResearchBtn.disabled = false;
311
+ startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
312
+ }
313
+ }
314
+
315
+ // Function to poll research status (as a backup to socket.io)
316
+ function pollResearchStatus(researchId) {
317
+ console.log(`Polling research status for ID: ${researchId}`);
318
+ fetch(getApiUrl(`/api/research/${researchId}`))
319
+ .then(response => {
320
+ if (!response.ok) {
321
+ throw new Error(`Server returned ${response.status}`);
322
+ }
323
+ return response.json();
324
+ })
325
+ .then(data => {
326
+ console.log('Research status response:', data);
327
+ // Update the UI with the current progress
328
+ updateProgressUI(data.progress, data.status, data.message);
329
+
330
+ // If research is complete, show the completion buttons
331
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'suspended') {
332
+ console.log(`Research is in final state: ${data.status}`);
333
+ // Clear the polling interval
334
+ if (pollingInterval) {
335
+ console.log('Clearing polling interval');
336
+ clearInterval(pollingInterval);
337
+ pollingInterval = null;
338
+ }
339
+
340
+ // Update UI for completion
341
+ if (data.status === 'completed') {
342
+ console.log('Research completed, loading results automatically');
343
+ // Hide the terminate button
344
+ const terminateBtn = document.getElementById('terminate-research-btn');
345
+ if (terminateBtn) {
346
+ terminateBtn.style.display = 'none';
347
+ }
348
+
349
+ // Auto-load the results instead of showing a button
350
+ loadResearch(researchId);
351
+ } else if (data.status === 'failed' || data.status === 'suspended') {
352
+ console.log(`Showing error message for status: ${data.status}`);
353
+ const errorMessage = document.getElementById('error-message');
354
+ if (errorMessage) {
355
+ errorMessage.style.display = 'block';
356
+ errorMessage.textContent = data.status === 'failed' ?
357
+ (data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
358
+ 'Research was suspended';
359
+ } else {
360
+ console.error('error-message element not found');
361
+ }
362
+ }
363
+
364
+ // Play notification sound based on status
365
+ if (data.status === 'completed') {
366
+ console.log('Playing success notification sound');
367
+ playNotificationSound('success');
368
+ } else if (data.status === 'failed') {
369
+ console.log('Playing error notification sound');
370
+ playNotificationSound('error');
371
+ }
372
+
373
+ // Update the navigation
374
+ console.log('Updating navigation based on research status');
375
+ updateNavigationBasedOnResearchStatus();
376
+
377
+ // Force the UI to update with a manual trigger
378
+ document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
379
+
380
+ return;
381
+ }
382
+
383
+ // Continue polling if still in progress
384
+ if (data.status === 'in_progress') {
385
+ console.log('Research is still in progress, continuing polling');
386
+ if (!pollingInterval) {
387
+ console.log('Setting up polling interval');
388
+ pollingInterval = setInterval(() => {
389
+ pollResearchStatus(researchId);
390
+ }, 10000);
391
+ }
392
+ }
393
+ })
394
+ .catch(error => {
395
+ console.error('Error polling research status:', error);
396
+ });
397
+ }
398
+
399
+ // Main initialization function
400
+ function initializeApp() {
401
+ console.log('Initializing application...');
402
+
403
+ // Initialize the sounds
404
+ initializeSounds();
405
+
406
+ // Get navigation elements
407
+ const navItems = document.querySelectorAll('.sidebar-nav li');
408
+ const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
409
+ const pages = document.querySelectorAll('.page');
410
+ const mobileTabBar = document.querySelector('.mobile-tab-bar');
411
+
412
+ // Handle responsive navigation based on screen size
413
+ function handleResponsiveNavigation() {
414
+ // Mobile tab bar should only be visible on small screens
415
+ if (window.innerWidth <= 767) {
416
+ if (mobileTabBar) {
417
+ mobileTabBar.style.display = 'flex';
418
+ }
419
+ } else {
420
+ if (mobileTabBar) {
421
+ mobileTabBar.style.display = 'none';
422
+ }
423
+ }
424
+ }
425
+
426
+ // Call on initial load
427
+ handleResponsiveNavigation();
428
+
429
+ // Add resize listener for responsive design
430
+ window.addEventListener('resize', handleResponsiveNavigation);
431
+
432
+ // Setup navigation click handlers
433
+ navItems.forEach(item => {
434
+ if (!item.classList.contains('external-link')) {
435
+ item.addEventListener('click', function() {
436
+ const pageId = this.dataset.page;
437
+ if (pageId) {
438
+ switchPage(pageId);
439
+ }
440
+ });
441
+ }
442
+ });
443
+
444
+ mobileNavItems.forEach(item => {
445
+ if (!item.classList.contains('external-link')) {
446
+ item.addEventListener('click', function() {
447
+ const pageId = this.dataset.page;
448
+ if (pageId) {
449
+ switchPage(pageId);
450
+ }
451
+ });
452
+ }
453
+ });
454
+
455
+ // Setup form submission
456
+ const researchForm = document.getElementById('research-form');
457
+ if (researchForm) {
458
+ researchForm.addEventListener('submit', function(e) {
459
+ e.preventDefault();
460
+ const query = document.getElementById('query').value.trim();
461
+ if (!query) {
462
+ alert('Please enter a research query');
463
+ return;
464
+ }
465
+
466
+ const mode = document.querySelector('.mode-option.active')?.dataset.mode || 'quick';
467
+ startResearch(query, mode);
468
+ });
469
+ }
470
+
471
+ // Initialize research mode selection
472
+ const modeOptions = document.querySelectorAll('.mode-option');
473
+ modeOptions.forEach(option => {
474
+ option.addEventListener('click', function() {
475
+ modeOptions.forEach(opt => opt.classList.remove('active'));
476
+ this.classList.add('active');
477
+ });
478
+ });
479
+
480
+ // Load research history initially
481
+ if (document.getElementById('history-list')) {
482
+ loadResearchHistory();
483
+ }
484
+
485
+ // Check for active research
486
+ checkActiveResearch();
487
+
488
+ // Setup notification toggle and other form elements
489
+ setupResearchForm();
490
+
491
+ console.log('Application initialized');
492
+ }
493
+
494
+ // Initialize the app
495
+ initializeApp();
496
+
497
+ // Function to switch between pages
498
+ function switchPage(pageId) {
499
+ // Get elements directly from the DOM
500
+ const pages = document.querySelectorAll('.page');
501
+ const navItems = document.querySelectorAll('.sidebar-nav li');
502
+ const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
503
+
504
+ // Remove active class from all pages
505
+ pages.forEach(page => {
506
+ page.classList.remove('active');
507
+ });
508
+
509
+ // Add active class to target page
510
+ const targetPage = document.getElementById(pageId);
511
+ if (targetPage) {
512
+ targetPage.classList.add('active');
513
+ }
514
+
515
+ // Update sidebar navigation active states
516
+ navItems.forEach(item => {
517
+ if (item.getAttribute('data-page') === pageId) {
518
+ item.classList.add('active');
519
+ } else {
520
+ item.classList.remove('active');
521
+ }
522
+ });
523
+
524
+ // Update mobile tab bar active states
525
+ mobileNavItems.forEach(item => {
526
+ if (item.getAttribute('data-page') === pageId) {
527
+ item.classList.add('active');
528
+ } else {
529
+ item.classList.remove('active');
530
+ }
531
+ });
532
+
533
+ // Special handling for history page
534
+ if (pageId === 'history') {
535
+ loadResearchHistory();
536
+ }
537
+ }
538
+
539
+ // Track termination status
540
+ let isTerminating = false;
541
+
542
+ // Check if we're on the history page and load history if needed
543
+ const historyPage = document.getElementById('history');
544
+ if (historyPage && historyPage.classList.contains('active')) {
545
+ // Use setTimeout to ensure the DOM is fully loaded
546
+ setTimeout(() => loadResearchHistory(), 100);
547
+ }
548
+
549
+ // Add a prefix helper function at the top of the file
550
+ function getApiUrl(path) {
551
+ // This function adds the /research prefix to all API URLs
552
+ return `/research${path}`;
553
+ }
554
+
555
+ // Function to terminate research - exposed to window object
556
+ async function terminateResearch(researchId) {
557
+ if (!researchId) {
558
+ console.error('No research ID provided for termination');
559
+ return;
560
+ }
561
+
562
+ // Prevent multiple termination requests
563
+ if (isTerminating) {
564
+ console.log('Termination already in progress');
565
+ return;
566
+ }
567
+
568
+ // Confirm with the user
569
+ if (!confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
570
+ return;
571
+ }
572
+
573
+ try {
574
+ // Set terminating flag
575
+ isTerminating = true;
576
+
577
+ // Update UI to show we're processing
578
+ const terminateBtn = document.getElementById('terminate-research-btn');
579
+ if (terminateBtn) {
580
+ terminateBtn.disabled = true;
581
+ terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
582
+ }
583
+
584
+ // Find all terminate buttons in history items and disable them
585
+ const allTerminateBtns = document.querySelectorAll('.terminate-btn');
586
+ allTerminateBtns.forEach(btn => {
587
+ if (btn !== terminateBtn) {
588
+ btn.disabled = true;
589
+ btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
590
+ }
591
+ });
592
+
593
+ // Call the terminate API
594
+ const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
595
+ method: 'POST'
596
+ });
597
+
598
+ const data = await response.json();
599
+
600
+ if (data.status === 'success') {
601
+ // The UI will be updated via socket events or polling
602
+ console.log('Termination request sent successfully');
603
+
604
+ // If we're on the history page, update the status of this item
605
+ if (document.getElementById('history').classList.contains('active')) {
606
+ updateHistoryItemStatus(researchId, 'terminating', 'Terminating...');
607
+ }
608
+ } else {
609
+ console.error('Termination request failed:', data.message);
610
+ alert(`Failed to terminate research: ${data.message}`);
611
+
612
+ // Reset the terminating flag
613
+ isTerminating = false;
614
+
615
+ // Reset the button
616
+ if (terminateBtn) {
617
+ terminateBtn.disabled = false;
618
+ terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
619
+ }
620
+
621
+ // Reset history button states
622
+ const allTerminateBtns = document.querySelectorAll('.terminate-btn');
623
+ allTerminateBtns.forEach(btn => {
624
+ if (btn !== terminateBtn) {
625
+ btn.disabled = false;
626
+ btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
627
+ }
628
+ });
629
+ }
630
+ } catch (error) {
631
+ console.error('Error terminating research:', error);
632
+ alert('An error occurred while trying to terminate the research.');
633
+
634
+ // Reset the terminating flag
635
+ isTerminating = false;
636
+
637
+ // Reset the button
638
+ const terminateBtn = document.getElementById('terminate-research-btn');
639
+ if (terminateBtn) {
640
+ terminateBtn.disabled = false;
641
+ terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
642
+ }
643
+
644
+ // Reset history button states
645
+ const allTerminateBtns = document.querySelectorAll('.terminate-btn');
646
+ allTerminateBtns.forEach(btn => {
647
+ if (btn !== terminateBtn) {
648
+ btn.disabled = false;
649
+ btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
650
+ }
651
+ });
652
+ }
653
+ }
654
+
655
+ // Expose the terminate function to the window object
656
+ window.terminateResearch = terminateResearch;
657
+
658
+ // Function to update the progress UI
659
+ function updateProgressUI(progress, status, message) {
660
+ const progressFill = document.getElementById('progress-fill');
661
+ const progressPercentage = document.getElementById('progress-percentage');
662
+ const progressStatus = document.getElementById('progress-status');
663
+
664
+ if (progressFill && progressPercentage) {
665
+ progressFill.style.width = `${progress}%`;
666
+ progressPercentage.textContent = `${progress}%`;
667
+ }
668
+
669
+ if (progressStatus && message) {
670
+ progressStatus.textContent = message;
671
+
672
+ // Update status class
673
+ progressStatus.className = 'progress-status';
674
+ if (status) {
675
+ progressStatus.classList.add(`status-${status}`);
676
+ }
677
+ }
678
+
679
+ // Show/hide terminate button based on status
680
+ const terminateBtn = document.getElementById('terminate-research-btn');
681
+ if (terminateBtn) {
682
+ if (status === 'in_progress') {
683
+ terminateBtn.style.display = 'inline-flex';
684
+ terminateBtn.disabled = false;
685
+ terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
686
+ } else if (status === 'terminating') {
687
+ terminateBtn.style.display = 'inline-flex';
688
+ terminateBtn.disabled = true;
689
+ terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
690
+ } else {
691
+ terminateBtn.style.display = 'none';
692
+ }
693
+ }
694
+ }
695
+
696
+ // Completely rewritten function to ensure reliable history loading
697
+ async function loadResearchHistory() {
698
+ const historyList = document.getElementById('history-list');
699
+
700
+ // Make sure we have the history list element
701
+ if (!historyList) {
702
+ console.error('History list element not found');
703
+ return;
704
+ }
705
+
706
+ historyList.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
707
+
708
+ try {
709
+ const response = await fetch(getApiUrl('/api/history'));
710
+
711
+ if (!response.ok) {
712
+ throw new Error(`Server returned ${response.status}`);
713
+ }
714
+
715
+ const data = await response.json();
716
+
717
+ // Clear the loading spinner
718
+ historyList.innerHTML = '';
719
+
720
+ // Handle empty data
721
+ if (!data || !Array.isArray(data) || data.length === 0) {
722
+ historyList.innerHTML = '<div class="empty-state">No research history found. Start a new research project!</div>';
723
+ return;
724
+ }
725
+
726
+ // Check if any research is in progress
727
+ const inProgressResearch = data.find(item => item.status === 'in_progress');
728
+
729
+ // Get the start research button
730
+ const startResearchBtn = document.getElementById('start-research-btn');
731
+
732
+ if (inProgressResearch) {
733
+ isResearchInProgress = true;
734
+ currentResearchId = inProgressResearch.id;
735
+ if (startResearchBtn) {
736
+ startResearchBtn.disabled = true;
737
+ }
738
+ } else {
739
+ isResearchInProgress = false;
740
+ if (startResearchBtn) {
741
+ startResearchBtn.disabled = false;
742
+ }
743
+ }
744
+
745
+ // Display each history item
746
+ data.forEach(item => {
747
+ try {
748
+ // Skip if item is invalid
749
+ if (!item || !item.id) {
750
+ return;
751
+ }
752
+
753
+ // Create container
754
+ const historyItem = document.createElement('div');
755
+ historyItem.className = 'history-item';
756
+ historyItem.dataset.researchId = item.id;
757
+
758
+ // Create header with title and status
759
+ const header = document.createElement('div');
760
+ header.className = 'history-item-header';
761
+
762
+ const title = document.createElement('div');
763
+ title.className = 'history-item-title';
764
+ title.textContent = item.query || 'Untitled Research';
765
+
766
+ const status = document.createElement('div');
767
+ status.className = `history-item-status status-${item.status || 'unknown'}`;
768
+ status.textContent = item.status ? (item.status.charAt(0).toUpperCase() + item.status.slice(1)) : 'Unknown';
769
+
770
+ header.appendChild(title);
771
+ header.appendChild(status);
772
+ historyItem.appendChild(header);
773
+
774
+ // Create meta section
775
+ const meta = document.createElement('div');
776
+ meta.className = 'history-item-meta';
777
+
778
+ const date = document.createElement('div');
779
+ date.className = 'history-item-date';
780
+ try {
781
+ // Use completed_at if available, fall back to created_at if not
782
+ const dateToUse = item.completed_at || item.created_at;
783
+ date.textContent = dateToUse ? formatDate(new Date(dateToUse)) : 'Unknown date';
784
+ } catch (e) {
785
+ date.textContent = item.completed_at || item.created_at || 'Unknown date';
786
+ }
787
+
788
+ const mode = document.createElement('div');
789
+ mode.className = 'history-item-mode';
790
+ const modeIcon = item.mode === 'quick' ? 'bolt' : 'microscope';
791
+ const modeText = item.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
792
+ mode.innerHTML = `<i class="fas fa-${modeIcon}"></i> ${modeText}`;
793
+
794
+ meta.appendChild(date);
795
+ meta.appendChild(mode);
796
+ historyItem.appendChild(meta);
797
+
798
+ // Create actions section
799
+ const actions = document.createElement('div');
800
+ actions.className = 'history-item-actions';
801
+
802
+ // View button
803
+ const viewBtn = document.createElement('button');
804
+ viewBtn.className = 'btn btn-sm btn-outline view-btn';
805
+
806
+ if (item.status === 'completed') {
807
+ viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
808
+ viewBtn.addEventListener('click', (e) => {
809
+ e.stopPropagation();
810
+ loadResearch(item.id);
811
+ });
812
+
813
+ // PDF button for completed research
814
+ const pdfBtn = document.createElement('button');
815
+ pdfBtn.className = 'btn btn-sm btn-outline pdf-btn';
816
+ pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> PDF';
817
+ pdfBtn.addEventListener('click', (e) => {
818
+ e.stopPropagation();
819
+ generatePdfFromResearch(item.id);
820
+ });
821
+ actions.appendChild(pdfBtn);
822
+ } else if (item.status === 'in_progress') {
823
+ viewBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> In Progress';
824
+ viewBtn.addEventListener('click', (e) => {
825
+ e.stopPropagation();
826
+ navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
827
+ });
828
+ } else {
829
+ viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
830
+ viewBtn.disabled = true;
831
+ }
832
+
833
+ actions.appendChild(viewBtn);
834
+
835
+ // Delete button
836
+ const deleteBtn = document.createElement('button');
837
+ deleteBtn.className = 'btn btn-sm btn-outline delete-btn';
838
+ deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
839
+ deleteBtn.addEventListener('click', (e) => {
840
+ e.stopPropagation();
841
+ if (confirm(`Are you sure you want to delete this research: "${item.query}"?`)) {
842
+ deleteResearch(item.id);
843
+ }
844
+ });
845
+ actions.appendChild(deleteBtn);
846
+
847
+ historyItem.appendChild(actions);
848
+
849
+ // Add click handler for the entire item
850
+ historyItem.addEventListener('click', (e) => {
851
+ // Skip if clicking on a button
852
+ if (e.target.closest('button')) {
853
+ return;
854
+ }
855
+
856
+ if (item.status === 'completed') {
857
+ loadResearch(item.id);
858
+ } else if (item.status === 'in_progress') {
859
+ navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
860
+ }
861
+ });
862
+
863
+ // Add to the history list
864
+ historyList.appendChild(historyItem);
865
+ } catch (itemError) {
866
+ console.error('Error processing history item:', itemError, item);
867
+ }
868
+ });
869
+
870
+ } catch (error) {
871
+ console.error('Error loading history:', error);
872
+ historyList.innerHTML = `
873
+ <div class="error-message">
874
+ Error loading history: ${error.message}
875
+ </div>
876
+ <div style="text-align: center; margin-top: 1rem;">
877
+ <button id="retry-history-btn" class="btn btn-primary">
878
+ <i class="fas fa-sync"></i> Retry
879
+ </button>
880
+ </div>`;
881
+
882
+ const retryBtn = document.getElementById('retry-history-btn');
883
+ if (retryBtn) {
884
+ retryBtn.addEventListener('click', () => {
885
+ loadResearchHistory();
886
+ });
887
+ }
888
+ }
889
+
890
+ // Add a fallback in case something goes wrong and the history list is still empty
891
+ setTimeout(() => {
892
+ if (historyList.innerHTML === '' || historyList.innerHTML.includes('loading-spinner')) {
893
+ console.warn('History list is still empty or showing spinner after load attempt - applying fallback');
894
+ historyList.innerHTML = `
895
+ <div class="error-message">
896
+ Something went wrong while loading the history.
897
+ </div>
898
+ <div style="text-align: center; margin-top: 1rem;">
899
+ <button id="fallback-retry-btn" class="btn btn-primary">
900
+ <i class="fas fa-sync"></i> Retry
901
+ </button>
902
+ </div>`;
903
+
904
+ const fallbackRetryBtn = document.getElementById('fallback-retry-btn');
905
+ if (fallbackRetryBtn) {
906
+ fallbackRetryBtn.addEventListener('click', () => {
907
+ loadResearchHistory();
908
+ });
909
+ }
910
+ }
911
+ }, 5000); // Check after 5 seconds
912
+ }
913
+
914
+ // Function to navigate to research progress
915
+ function navigateToResearchProgress(research) {
916
+ document.getElementById('current-query').textContent = research.query;
917
+ document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
918
+ document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
919
+
920
+ // Navigate to progress page
921
+ switchPage('research-progress');
922
+
923
+ // Connect to socket for this research
924
+ window.connectToResearchSocket(research.id);
925
+
926
+ // Start polling for status
927
+ pollResearchStatus(research.id);
928
+ }
929
+
930
+ // Function to delete a research record
931
+ async function deleteResearch(researchId) {
932
+ try {
933
+ const response = await fetch(getApiUrl(`/api/research/${researchId}/delete`), {
934
+ method: 'DELETE'
935
+ });
936
+
937
+ if (response.ok) {
938
+ // Reload the history
939
+ loadResearchHistory();
940
+ } else {
941
+ alert('Failed to delete research. Please try again.');
942
+ }
943
+ } catch (error) {
944
+ console.error('Error deleting research:', error);
945
+ alert('An error occurred while deleting the research.');
946
+ }
947
+ }
948
+
949
+ // Function to load a specific research result
950
+ async function loadResearch(researchId) {
951
+ // Navigate to results page
952
+ switchPage('research-results');
953
+
954
+ const resultsContent = document.getElementById('results-content');
955
+ resultsContent.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
956
+
957
+ try {
958
+ // Load research details
959
+ const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
960
+ const details = await detailsResponse.json();
961
+
962
+ // Display metadata
963
+ document.getElementById('result-query').textContent = details.query;
964
+
965
+ // Format date with duration if available
966
+ let dateText = formatDate(new Date(details.completed_at || details.created_at));
967
+ if (details.duration_seconds) {
968
+ // Format duration
969
+ let durationText = '';
970
+ const duration = parseInt(details.duration_seconds);
971
+
972
+ if (duration < 60) { // less than a minute
973
+ durationText = `${duration}s`;
974
+ } else if (duration < 3600) { // less than an hour
975
+ durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
976
+ } else { // hours
977
+ durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
978
+ }
979
+
980
+ dateText += ` (Duration: ${durationText})`;
981
+ }
982
+ document.getElementById('result-date').textContent = dateText;
983
+
984
+ document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
985
+
986
+ // Load the report content
987
+ const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
988
+ const reportData = await reportResponse.json();
989
+
990
+ if (reportData.status === 'success') {
991
+ // Render markdown
992
+ const renderedContent = marked.parse(reportData.content);
993
+ resultsContent.innerHTML = renderedContent;
994
+
995
+ // Apply syntax highlighting
996
+ document.querySelectorAll('pre code').forEach((block) => {
997
+ hljs.highlightElement(block);
998
+ });
999
+ } else {
1000
+ resultsContent.innerHTML = '<div class="error-message">Error loading report. Please try again later.</div>';
1001
+ }
1002
+ } catch (error) {
1003
+ console.error('Error loading research:', error);
1004
+ resultsContent.innerHTML = '<div class="error-message">Error loading research results. Please try again later.</div>';
1005
+ }
1006
+ }
1007
+
1008
+ // Function to load research details page
1009
+ async function loadResearchDetails(researchId) {
1010
+ // Navigate to details page
1011
+ switchPage('research-details');
1012
+
1013
+ // Initialize the research log area
1014
+ const researchLog = document.getElementById('research-log');
1015
+ researchLog.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1016
+
1017
+ try {
1018
+ // Load research details
1019
+ const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
1020
+ console.log('Research details API response status:', response.status);
1021
+
1022
+ if (!response.ok) {
1023
+ console.error('API error:', response.status, response.statusText);
1024
+ researchLog.innerHTML = `<div class="error-message">Error loading research details. Status: ${response.status}</div>`;
1025
+ return;
1026
+ }
1027
+
1028
+ const data = await response.json();
1029
+ console.log('Research details data:', data);
1030
+
1031
+ if (data.status !== 'success') {
1032
+ researchLog.innerHTML = `<div class="error-message">Error loading research details: ${data.message || 'Unknown error'}</div>`;
1033
+ return;
1034
+ }
1035
+
1036
+ // Display metadata
1037
+ document.getElementById('detail-query').textContent = data.query || 'N/A';
1038
+ document.getElementById('detail-status').textContent = capitalizeFirstLetter(data.status || 'unknown');
1039
+ document.getElementById('detail-status').className = `metadata-value status-${data.status || 'unknown'}`;
1040
+ document.getElementById('detail-mode').textContent = (data.mode === 'quick' ? 'Quick Summary' : 'Detailed Report') || 'N/A';
1041
+
1042
+ // Update progress bar
1043
+ const progress = data.progress || 0;
1044
+ document.getElementById('detail-progress-fill').style.width = `${progress}%`;
1045
+ document.getElementById('detail-progress-percentage').textContent = `${progress}%`;
1046
+
1047
+ // Render log entries
1048
+ renderLogEntries(data.log || []);
1049
+
1050
+ // Connect to socket for real-time updates
1051
+ window.connectToResearchSocket(researchId);
1052
+
1053
+ // Add appropriate actions based on research status
1054
+ const detailActions = document.getElementById('detail-actions');
1055
+ detailActions.innerHTML = '';
1056
+
1057
+ if (data.status === 'completed') {
1058
+ const viewResultsBtn = document.createElement('button');
1059
+ viewResultsBtn.className = 'btn btn-primary';
1060
+ viewResultsBtn.innerHTML = '<i class="fas fa-eye"></i> View Results';
1061
+ viewResultsBtn.addEventListener('click', () => loadResearch(researchId));
1062
+ detailActions.appendChild(viewResultsBtn);
1063
+
1064
+ // Add download PDF button
1065
+ const downloadPdfBtn = document.createElement('button');
1066
+ downloadPdfBtn.className = 'btn btn-outline';
1067
+ downloadPdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
1068
+ downloadPdfBtn.addEventListener('click', () => generatePdfFromResearch(researchId));
1069
+ detailActions.appendChild(downloadPdfBtn);
1070
+ } else if (data.status === 'in_progress') {
1071
+ const viewProgressBtn = document.createElement('button');
1072
+ viewProgressBtn.className = 'btn btn-primary';
1073
+ viewProgressBtn.innerHTML = '<i class="fas fa-sync"></i> View Live Progress';
1074
+ viewProgressBtn.addEventListener('click', () => {
1075
+ document.getElementById('current-query').textContent = data.query || '';
1076
+
1077
+ // Navigate to progress page
1078
+ switchPage('research-progress');
1079
+
1080
+ // Connect to socket
1081
+ window.connectToResearchSocket(researchId);
1082
+ });
1083
+ detailActions.appendChild(viewProgressBtn);
1084
+ }
1085
+ } catch (error) {
1086
+ console.error('Error loading research details:', error);
1087
+ researchLog.innerHTML = `<div class="error-message">Error loading research details: ${error.message}</div>`;
1088
+ }
1089
+ }
1090
+
1091
+ // Function to render log entries
1092
+ function renderLogEntries(logEntries) {
1093
+ const researchLog = document.getElementById('research-log');
1094
+ researchLog.innerHTML = '';
1095
+
1096
+ if (!logEntries || logEntries.length === 0) {
1097
+ researchLog.innerHTML = '<div class="empty-state">No log entries available.</div>';
1098
+ return;
1099
+ }
1100
+
1101
+ try {
1102
+ // Use a document fragment for better performance
1103
+ const fragment = document.createDocumentFragment();
1104
+ const template = document.getElementById('log-entry-template');
1105
+
1106
+ if (!template) {
1107
+ console.error('Log entry template not found');
1108
+ researchLog.innerHTML = '<div class="error-message">Error rendering log entries: Template not found</div>';
1109
+ return;
1110
+ }
1111
+
1112
+ logEntries.forEach(entry => {
1113
+ if (!entry) return; // Skip invalid entries
1114
+
1115
+ try {
1116
+ const clone = document.importNode(template.content, true);
1117
+
1118
+ // Format the timestamp
1119
+ let timeStr = 'N/A';
1120
+ try {
1121
+ if (entry.time) {
1122
+ const time = new Date(entry.time);
1123
+ timeStr = time.toLocaleTimeString();
1124
+ }
1125
+ } catch (timeErr) {
1126
+ console.warn('Error formatting time:', timeErr);
1127
+ }
1128
+
1129
+ const timeEl = clone.querySelector('.log-entry-time');
1130
+ if (timeEl) timeEl.textContent = timeStr;
1131
+
1132
+ // Add message with phase highlighting if available
1133
+ const messageEl = clone.querySelector('.log-entry-message');
1134
+ if (messageEl) {
1135
+ let phaseClass = '';
1136
+ if (entry.metadata && entry.metadata.phase) {
1137
+ phaseClass = `phase-${entry.metadata.phase}`;
1138
+ }
1139
+ messageEl.textContent = entry.message || 'No message';
1140
+ messageEl.classList.add(phaseClass);
1141
+ }
1142
+
1143
+ // Add progress information if available
1144
+ const progressEl = clone.querySelector('.log-entry-progress');
1145
+ if (progressEl) {
1146
+ if (entry.progress !== null && entry.progress !== undefined) {
1147
+ progressEl.textContent = `Progress: ${entry.progress}%`;
1148
+ } else {
1149
+ progressEl.textContent = '';
1150
+ }
1151
+ }
1152
+
1153
+ fragment.appendChild(clone);
1154
+ } catch (entryError) {
1155
+ console.error('Error processing log entry:', entryError, entry);
1156
+ // Continue with other entries
1157
+ }
1158
+ });
1159
+
1160
+ researchLog.appendChild(fragment);
1161
+
1162
+ // Scroll to the bottom
1163
+ researchLog.scrollTop = researchLog.scrollHeight;
1164
+ } catch (error) {
1165
+ console.error('Error rendering log entries:', error);
1166
+ researchLog.innerHTML = '<div class="error-message">Error rendering log entries. Please try again later.</div>';
1167
+ }
1168
+
1169
+ // Connect to socket for updates if this is an in-progress research
1170
+ if (research && research.status === 'in_progress') {
1171
+ window.connectToResearchSocket(researchId);
1172
+ }
1173
+ }
1174
+
1175
+ // Function to update detail log with a new entry
1176
+ function updateDetailLogEntry(logEntry) {
1177
+ if (!logEntry || !document.getElementById('research-details').classList.contains('active')) {
1178
+ return;
1179
+ }
1180
+
1181
+ const researchLog = document.getElementById('research-log');
1182
+ const template = document.getElementById('log-entry-template');
1183
+ const clone = document.importNode(template.content, true);
1184
+
1185
+ // Format the timestamp
1186
+ const time = new Date(logEntry.time);
1187
+ clone.querySelector('.log-entry-time').textContent = time.toLocaleTimeString();
1188
+
1189
+ // Add message with phase highlighting if available
1190
+ const messageEl = clone.querySelector('.log-entry-message');
1191
+ let phaseClass = '';
1192
+ if (logEntry.metadata && logEntry.metadata.phase) {
1193
+ phaseClass = `phase-${logEntry.metadata.phase}`;
1194
+ }
1195
+ messageEl.textContent = logEntry.message;
1196
+ messageEl.classList.add(phaseClass);
1197
+
1198
+ // Add progress information if available
1199
+ const progressEl = clone.querySelector('.log-entry-progress');
1200
+ if (logEntry.progress !== null && logEntry.progress !== undefined) {
1201
+ progressEl.textContent = `Progress: ${logEntry.progress}%`;
1202
+
1203
+ // Also update the progress bar in the details view
1204
+ document.getElementById('detail-progress-fill').style.width = `${logEntry.progress}%`;
1205
+ document.getElementById('detail-progress-percentage').textContent = `${logEntry.progress}%`;
1206
+ } else {
1207
+ progressEl.textContent = '';
1208
+ }
1209
+
1210
+ researchLog.appendChild(clone);
1211
+
1212
+ // Scroll to the bottom
1213
+ researchLog.scrollTop = researchLog.scrollHeight;
1214
+ }
1215
+
1216
+ // Back to history button handlers
1217
+ document.getElementById('back-to-history').addEventListener('click', () => {
1218
+ const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
1219
+ historyNav.click();
1220
+ });
1221
+
1222
+ document.getElementById('back-to-history-from-details').addEventListener('click', () => {
1223
+ const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
1224
+ historyNav.click();
1225
+ });
1226
+
1227
+ // Helper functions
1228
+ function capitalizeFirstLetter(string) {
1229
+ return string.charAt(0).toUpperCase() + string.slice(1);
1230
+ }
1231
+
1232
+ function formatDate(date) {
1233
+ // Ensure we're handling the date properly
1234
+ if (!(date instanceof Date) || isNaN(date)) {
1235
+ console.warn('Invalid date provided to formatDate:', date);
1236
+ return 'Invalid date';
1237
+ }
1238
+
1239
+ // Get current year to compare with date year
1240
+ const currentYear = new Date().getFullYear();
1241
+ const dateYear = date.getFullYear();
1242
+
1243
+ // Get month name, day, and time
1244
+ const month = date.toLocaleString('en-US', { month: 'short' });
1245
+ const day = date.getDate();
1246
+ const hours = date.getHours().toString().padStart(2, '0');
1247
+ const minutes = date.getMinutes().toString().padStart(2, '0');
1248
+
1249
+ // Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
1250
+ if (dateYear === currentYear) {
1251
+ return `${month} ${day}, ${hours}:${minutes}`;
1252
+ } else {
1253
+ return `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
1254
+ }
1255
+ }
1256
+
1257
+ // Function to update progress UI from current research
1258
+ function updateProgressFromCurrentResearch() {
1259
+ if (!isResearchInProgress || !currentResearchId) return;
1260
+
1261
+ // Fetch current status
1262
+ fetch(getApiUrl(`/api/research/${currentResearchId}`))
1263
+ .then(response => response.json())
1264
+ .then(data => {
1265
+ document.getElementById('current-query').textContent = data.query || '';
1266
+ document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
1267
+ document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
1268
+
1269
+ // Connect to socket for this research
1270
+ window.connectToResearchSocket(currentResearchId);
1271
+ })
1272
+ .catch(error => {
1273
+ console.error('Error fetching research status:', error);
1274
+ });
1275
+ }
1276
+
1277
+ // Function to update the sidebar navigation based on research status
1278
+ function updateNavigationBasedOnResearchStatus() {
1279
+ console.log("Updating navigation based on research status");
1280
+ console.log("isResearchInProgress:", isResearchInProgress);
1281
+ console.log("currentResearchId:", currentResearchId);
1282
+
1283
+ // Get nav items for each update to ensure we have fresh references
1284
+ const navItems = document.querySelectorAll('.sidebar-nav li');
1285
+ const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
1286
+ // Get all pages
1287
+ const pages = document.querySelectorAll('.page');
1288
+
1289
+ const newResearchNav = Array.from(navItems).find(item =>
1290
+ item.getAttribute('data-page') === 'new-research' ||
1291
+ (item.getAttribute('data-original-page') === 'new-research' &&
1292
+ item.getAttribute('data-page') === 'research-progress')
1293
+ );
1294
+
1295
+ if (newResearchNav) {
1296
+ if (isResearchInProgress) {
1297
+ console.log("Research is in progress, updating navigation");
1298
+ // Change text to "Research in Progress"
1299
+ newResearchNav.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Research in Progress';
1300
+
1301
+ // Also update the listener to navigate to progress page
1302
+ if (newResearchNav.getAttribute('data-page') !== 'research-progress') {
1303
+ newResearchNav.setAttribute('data-original-page', 'new-research');
1304
+ newResearchNav.setAttribute('data-page', 'research-progress');
1305
+ }
1306
+
1307
+ // If on new-research page, redirect to research-progress
1308
+ if (document.getElementById('new-research').classList.contains('active')) {
1309
+ pages.forEach(page => page.classList.remove('active'));
1310
+ document.getElementById('research-progress').classList.add('active');
1311
+
1312
+ // Update the research progress page
1313
+ updateProgressFromCurrentResearch();
1314
+ }
1315
+ } else {
1316
+ console.log("Research is not in progress, resetting navigation");
1317
+ // Reset to "New Research" if there's no active research
1318
+ newResearchNav.innerHTML = '<i class="fas fa-search"></i> New Research';
1319
+
1320
+ // Reset the listener
1321
+ if (newResearchNav.hasAttribute('data-original-page')) {
1322
+ newResearchNav.setAttribute('data-page', newResearchNav.getAttribute('data-original-page'));
1323
+ newResearchNav.removeAttribute('data-original-page');
1324
+ }
1325
+
1326
+ // If the terminate button is visible, hide it
1327
+ const terminateBtn = document.getElementById('terminate-research-btn');
1328
+ if (terminateBtn) {
1329
+ terminateBtn.style.display = 'none';
1330
+ terminateBtn.disabled = false;
1331
+ terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1332
+ }
1333
+ }
1334
+
1335
+ // Make sure the navigation highlights the correct item
1336
+ navItems.forEach(item => {
1337
+ if (item === newResearchNav) {
1338
+ if (isResearchInProgress) {
1339
+ if (document.getElementById('research-progress').classList.contains('active')) {
1340
+ item.classList.add('active');
1341
+ }
1342
+ } else if (document.getElementById('new-research').classList.contains('active')) {
1343
+ item.classList.add('active');
1344
+ }
1345
+ }
1346
+ });
1347
+ }
1348
+
1349
+ // Connect to socket for updates
1350
+ if (currentResearchId) {
1351
+ window.connectToResearchSocket(currentResearchId);
1352
+ }
1353
+ }
1354
+
1355
+ // Function to update the research progress page from nav click
1356
+ function updateProgressPage() {
1357
+ if (!isResearchInProgress || !currentResearchId) {
1358
+ return;
1359
+ }
1360
+
1361
+ // Update the progress page
1362
+ fetch(getApiUrl(`/api/research/${currentResearchId}`))
1363
+ .then(response => response.json())
1364
+ .then(data => {
1365
+ // Update the query display
1366
+ document.getElementById('current-query').textContent = data.query;
1367
+
1368
+ // Update the progress bar
1369
+ updateProgressUI(data.progress || 0, data.status);
1370
+
1371
+ // Connect to socket for live updates
1372
+ window.connectToResearchSocket(currentResearchId);
1373
+
1374
+ // Check if we need to show the terminate button
1375
+ if (data.status === 'in_progress') {
1376
+ const terminateBtn = document.getElementById('terminate-research-btn');
1377
+ if (terminateBtn) {
1378
+ terminateBtn.style.display = 'inline-flex';
1379
+ terminateBtn.disabled = false;
1380
+ }
1381
+ }
1382
+ })
1383
+ .catch(error => {
1384
+ console.error('Error fetching research status:', error);
1385
+ });
1386
+ }
1387
+
1388
+ // Function to update a specific history item without reloading the whole list
1389
+ function updateHistoryItemStatus(researchId, status, statusText) {
1390
+ const historyList = document.getElementById('history-list');
1391
+
1392
+ // Look for the item in the active research banner
1393
+ const activeBanner = historyList.querySelector(`.active-research-banner[data-research-id="${researchId}"]`);
1394
+ if (activeBanner) {
1395
+ const statusEl = activeBanner.querySelector('.history-item-status');
1396
+ if (statusEl) {
1397
+ statusEl.textContent = statusText || capitalizeFirstLetter(status);
1398
+ statusEl.className = 'history-item-status';
1399
+ statusEl.classList.add(`status-${status}`);
1400
+ }
1401
+
1402
+ // Update buttons
1403
+ const terminateBtn = activeBanner.querySelector('.terminate-btn');
1404
+ if (terminateBtn) {
1405
+ terminateBtn.style.display = 'none';
1406
+ }
1407
+
1408
+ const viewProgressBtn = activeBanner.querySelector('.view-progress-btn');
1409
+ if (viewProgressBtn) {
1410
+ if (status === 'suspended') {
1411
+ viewProgressBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
1412
+ } else if (status === 'failed') {
1413
+ viewProgressBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
1414
+ }
1415
+ viewProgressBtn.disabled = true;
1416
+ }
1417
+
1418
+ return;
1419
+ }
1420
+
1421
+ // Look for the item in the regular list
1422
+ const historyItem = historyList.querySelector(`.history-item[data-research-id="${researchId}"]`);
1423
+ if (historyItem) {
1424
+ const statusEl = historyItem.querySelector('.history-item-status');
1425
+ if (statusEl) {
1426
+ statusEl.textContent = statusText || capitalizeFirstLetter(status);
1427
+ statusEl.className = 'history-item-status';
1428
+ statusEl.classList.add(`status-${status}`);
1429
+ }
1430
+
1431
+ // Update view button
1432
+ const viewBtn = historyItem.querySelector('.view-btn');
1433
+ if (viewBtn) {
1434
+ if (status === 'suspended') {
1435
+ viewBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
1436
+ viewBtn.disabled = true;
1437
+ } else if (status === 'failed') {
1438
+ viewBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
1439
+ viewBtn.disabled = true;
1440
+ }
1441
+ }
1442
+ }
1443
+ }
1444
+
1445
+ // PDF Generation Functions
1446
+ function generatePdf() {
1447
+ const resultsContent = document.getElementById('results-content');
1448
+ const query = document.getElementById('result-query').textContent;
1449
+ const date = document.getElementById('result-date').textContent;
1450
+ const mode = document.getElementById('result-mode').textContent;
1451
+
1452
+ // Show loading indicator
1453
+ const loadingIndicator = document.createElement('div');
1454
+ loadingIndicator.className = 'loading-spinner centered';
1455
+ loadingIndicator.innerHTML = '<div class="spinner"></div><p style="margin-top: 10px;">Generating PDF...</p>';
1456
+ resultsContent.parentNode.insertBefore(loadingIndicator, resultsContent);
1457
+ resultsContent.style.display = 'none';
1458
+
1459
+ // Create a clone of the content for PDF generation
1460
+ const contentClone = resultsContent.cloneNode(true);
1461
+ contentClone.style.display = 'block';
1462
+ contentClone.style.position = 'absolute';
1463
+ contentClone.style.left = '-9999px';
1464
+ contentClone.style.width = '800px';
1465
+
1466
+ // Apply PDF-specific styling for better readability
1467
+ contentClone.style.background = '#ffffff';
1468
+ contentClone.style.color = '#333333';
1469
+ contentClone.style.padding = '20px';
1470
+
1471
+ // Improve visibility by adjusting styles specifically for PDF
1472
+ const applyPdfStyles = (element) => {
1473
+ // Set all text to dark color for better readability on white background
1474
+ element.querySelectorAll('*').forEach(el => {
1475
+ // Skip elements that already have inline color styles
1476
+ if (!el.style.color) {
1477
+ el.style.color = '#333333';
1478
+ }
1479
+
1480
+ // Fix background colors
1481
+ if (el.style.backgroundColor &&
1482
+ (el.style.backgroundColor.includes('var(--bg') ||
1483
+ el.style.backgroundColor.includes('#121212') ||
1484
+ el.style.backgroundColor.includes('#1e1e2d') ||
1485
+ el.style.backgroundColor.includes('#2a2a3a'))) {
1486
+ el.style.backgroundColor = '#f8f8f8';
1487
+ }
1488
+
1489
+ // Handle code blocks specifically
1490
+ if (el.tagName === 'PRE' || el.tagName === 'CODE') {
1491
+ el.style.backgroundColor = '#f5f5f5';
1492
+ el.style.border = '1px solid #e0e0e0';
1493
+ el.style.color = '#333333';
1494
+ }
1495
+
1496
+ // Make links visible
1497
+ if (el.tagName === 'A') {
1498
+ el.style.color = '#0066cc';
1499
+ el.style.textDecoration = 'underline';
1500
+ }
1501
+ });
1502
+
1503
+ // Fix specific syntax highlighting elements for PDF
1504
+ element.querySelectorAll('.hljs').forEach(hljs => {
1505
+ hljs.style.backgroundColor = '#f8f8f8';
1506
+ hljs.style.color = '#333333';
1507
+
1508
+ // Fix common syntax highlighting colors for PDF
1509
+ hljs.querySelectorAll('.hljs-keyword').forEach(el => el.style.color = '#0000cc');
1510
+ hljs.querySelectorAll('.hljs-string').forEach(el => el.style.color = '#008800');
1511
+ hljs.querySelectorAll('.hljs-number').forEach(el => el.style.color = '#aa0000');
1512
+ hljs.querySelectorAll('.hljs-comment').forEach(el => el.style.color = '#888888');
1513
+ hljs.querySelectorAll('.hljs-function').forEach(el => el.style.color = '#880000');
1514
+ });
1515
+ };
1516
+
1517
+ document.body.appendChild(contentClone);
1518
+
1519
+ // Apply PDF-specific styles
1520
+ applyPdfStyles(contentClone);
1521
+
1522
+ // Add title and metadata to the PDF content
1523
+ const headerDiv = document.createElement('div');
1524
+ headerDiv.innerHTML = `
1525
+ <h1 style="color: #6e4ff6; font-size: 24px; margin-bottom: 10px;">${query}</h1>
1526
+ <div style="margin-bottom: 20px; font-size: 14px; color: #666;">
1527
+ <p><strong>Generated:</strong> ${date}</p>
1528
+ <p><strong>Mode:</strong> ${mode}</p>
1529
+ <p><strong>Source:</strong> Deep Research Lab</p>
1530
+ </div>
1531
+ <hr style="margin-bottom: 20px; border: 1px solid #eee;">
1532
+ `;
1533
+ contentClone.insertBefore(headerDiv, contentClone.firstChild);
1534
+
1535
+ setTimeout(() => {
1536
+ try {
1537
+ // Use window.jspdf which is from the UMD bundle
1538
+ const { jsPDF } = window.jspdf;
1539
+ const pdf = new jsPDF('p', 'pt', 'a4');
1540
+ const pdfWidth = pdf.internal.pageSize.getWidth();
1541
+ const pdfHeight = pdf.internal.pageSize.getHeight();
1542
+ const margin = 40;
1543
+ const contentWidth = pdfWidth - 2 * margin;
1544
+
1545
+ // Create a more efficient PDF generation approach that keeps text selectable
1546
+ const generateTextBasedPDF = async () => {
1547
+ try {
1548
+ // Get all text elements and handle them differently than images and special content
1549
+ const elements = Array.from(contentClone.children);
1550
+ let currentY = margin;
1551
+ let pageNum = 1;
1552
+
1553
+ // Function to add a page with header
1554
+ const addPageWithHeader = (pageNum) => {
1555
+ if (pageNum > 1) {
1556
+ pdf.addPage();
1557
+ }
1558
+ pdf.setFontSize(8);
1559
+ pdf.setTextColor(100, 100, 100);
1560
+ pdf.text(`Deep Research - ${query} - Page ${pageNum}`, margin, pdfHeight - 20);
1561
+ };
1562
+
1563
+ addPageWithHeader(pageNum);
1564
+
1565
+ // Process each element
1566
+ for (const element of elements) {
1567
+ // Simple text content - handled directly by jsPDF
1568
+ if ((element.tagName === 'P' || element.tagName === 'DIV') &&
1569
+ !element.querySelector('img, canvas, svg') &&
1570
+ element.children.length === 0) {
1571
+
1572
+ pdf.setFontSize(11);
1573
+ pdf.setTextColor(0, 0, 0);
1574
+
1575
+ const text = element.textContent.trim();
1576
+ if (!text) continue; // Skip empty text
1577
+
1578
+ const textLines = pdf.splitTextToSize(text, contentWidth);
1579
+
1580
+ // Check if we need a new page
1581
+ if (currentY + (textLines.length * 14) > pdfHeight - margin) {
1582
+ pageNum++;
1583
+ addPageWithHeader(pageNum);
1584
+ currentY = margin;
1585
+ }
1586
+
1587
+ pdf.text(textLines, margin, currentY + 12);
1588
+ currentY += (textLines.length * 14) + 10;
1589
+ }
1590
+ // Handle headings
1591
+ else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
1592
+ const fontSize = {
1593
+ 'H1': 24,
1594
+ 'H2': 20,
1595
+ 'H3': 16,
1596
+ 'H4': 14,
1597
+ 'H5': 12,
1598
+ 'H6': 11
1599
+ }[element.tagName];
1600
+
1601
+ // Add heading text as native PDF text
1602
+ pdf.setFontSize(fontSize);
1603
+
1604
+ // Use a different color for headings to match styling
1605
+ if (element.tagName === 'H1') {
1606
+ pdf.setTextColor(110, 79, 246); // Purple for main headers
1607
+ } else if (element.tagName === 'H2') {
1608
+ pdf.setTextColor(70, 90, 150); // Darker blue for H2
1609
+ } else {
1610
+ pdf.setTextColor(0, 0, 0); // Black for other headings
1611
+ }
1612
+
1613
+ const text = element.textContent.trim();
1614
+ if (!text) continue; // Skip empty headings
1615
+
1616
+ const textLines = pdf.splitTextToSize(text, contentWidth);
1617
+
1618
+ // Check if we need a new page
1619
+ if (currentY + (textLines.length * (fontSize + 4)) > pdfHeight - margin) {
1620
+ pageNum++;
1621
+ addPageWithHeader(pageNum);
1622
+ currentY = margin + 40; // Reset Y position after header
1623
+ }
1624
+
1625
+ pdf.text(textLines, margin, currentY + fontSize);
1626
+ currentY += (textLines.length * (fontSize + 4)) + 10;
1627
+
1628
+ // Add a subtle underline for H1 and H2
1629
+ if (element.tagName === 'H1' || element.tagName === 'H2') {
1630
+ pdf.setDrawColor(110, 79, 246, 0.5);
1631
+ pdf.setLineWidth(0.5);
1632
+ pdf.line(
1633
+ margin,
1634
+ currentY - 5,
1635
+ margin + Math.min(contentWidth, pdf.getTextWidth(text) * 1.2),
1636
+ currentY - 5
1637
+ );
1638
+ currentY += 5; // Add a bit more space after underlined headings
1639
+ }
1640
+ }
1641
+ // Handle lists
1642
+ else if (element.tagName === 'UL' || element.tagName === 'OL') {
1643
+ pdf.setFontSize(11);
1644
+ pdf.setTextColor(0, 0, 0);
1645
+
1646
+ const listItems = element.querySelectorAll('li');
1647
+ let itemNumber = 1;
1648
+
1649
+ for (const item of listItems) {
1650
+ const prefix = element.tagName === 'UL' ? '• ' : `${itemNumber}. `;
1651
+ const text = item.textContent.trim();
1652
+
1653
+ if (!text) continue; // Skip empty list items
1654
+
1655
+ // Split text to fit width, accounting for bullet/number indent
1656
+ const textLines = pdf.splitTextToSize(text, contentWidth - 15);
1657
+
1658
+ // Check if we need a new page
1659
+ if (currentY + (textLines.length * 14) > pdfHeight - margin) {
1660
+ pageNum++;
1661
+ addPageWithHeader(pageNum);
1662
+ currentY = margin;
1663
+ }
1664
+
1665
+ // Add the bullet/number
1666
+ pdf.text(prefix, margin, currentY + 12);
1667
+
1668
+ // Add the text with indent
1669
+ pdf.text(textLines, margin + 15, currentY + 12);
1670
+ currentY += (textLines.length * 14) + 5;
1671
+
1672
+ if (element.tagName === 'OL') itemNumber++;
1673
+ }
1674
+
1675
+ currentY += 5; // Extra space after list
1676
+ }
1677
+ // Handle code blocks as text
1678
+ else if (element.tagName === 'PRE' || element.querySelector('pre')) {
1679
+ const codeElement = element.tagName === 'PRE' ? element : element.querySelector('pre');
1680
+ const codeText = codeElement.textContent.trim();
1681
+
1682
+ if (!codeText) continue; // Skip empty code blocks
1683
+
1684
+ // Use monospace font for code
1685
+ pdf.setFont("courier", "normal");
1686
+ pdf.setFontSize(9); // Smaller font for code
1687
+
1688
+ // Calculate code block size
1689
+ const codeLines = codeText.split('\n');
1690
+ const lineHeight = 10; // Smaller line height for code
1691
+ const codeBlockHeight = (codeLines.length * lineHeight) + 20; // Add padding
1692
+
1693
+ // Add a background for the code block
1694
+ if (currentY + codeBlockHeight > pdfHeight - margin) {
1695
+ pageNum++;
1696
+ addPageWithHeader(pageNum);
1697
+ currentY = margin;
1698
+ }
1699
+
1700
+ // Draw code block background
1701
+ pdf.setFillColor(245, 245, 245); // Light gray background
1702
+ pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'F');
1703
+
1704
+ // Draw a border
1705
+ pdf.setDrawColor(220, 220, 220);
1706
+ pdf.setLineWidth(0.5);
1707
+ pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'S');
1708
+
1709
+ // Add the code text
1710
+ pdf.setTextColor(0, 0, 0);
1711
+ currentY += 10; // Add padding at top
1712
+
1713
+ codeLines.forEach(line => {
1714
+ // Handle indentation by preserving leading spaces
1715
+ const spacePadding = line.match(/^(\s*)/)[0].length;
1716
+ const visibleLine = line.trimLeft();
1717
+
1718
+ // Calculate width of space character
1719
+ const spaceWidth = pdf.getStringUnitWidth(' ') * 9 / pdf.internal.scaleFactor;
1720
+
1721
+ pdf.text(visibleLine, margin + (spacePadding * spaceWidth), currentY);
1722
+ currentY += lineHeight;
1723
+ });
1724
+
1725
+ currentY += 10; // Add padding at bottom
1726
+
1727
+ // Reset to normal font
1728
+ pdf.setFont("helvetica", "normal");
1729
+ pdf.setFontSize(11);
1730
+ }
1731
+ // Handle tables as text
1732
+ else if (element.tagName === 'TABLE' || element.querySelector('table')) {
1733
+ const tableElement = element.tagName === 'TABLE' ? element : element.querySelector('table');
1734
+
1735
+ if (!tableElement) continue;
1736
+
1737
+ // Get table rows
1738
+ const rows = Array.from(tableElement.querySelectorAll('tr'));
1739
+ if (rows.length === 0) continue;
1740
+
1741
+ // Calculate column widths
1742
+ const headerCells = Array.from(rows[0].querySelectorAll('th, td'));
1743
+ const numColumns = headerCells.length;
1744
+
1745
+ if (numColumns === 0) continue;
1746
+
1747
+ // Default column width distribution (equal)
1748
+ const colWidth = contentWidth / numColumns;
1749
+
1750
+ // Start drawing table
1751
+ let tableY = currentY + 10;
1752
+
1753
+ // Check if we need a new page
1754
+ if (tableY + (rows.length * 20) > pdfHeight - margin) {
1755
+ pageNum++;
1756
+ addPageWithHeader(pageNum);
1757
+ tableY = margin + 10;
1758
+ currentY = margin;
1759
+ }
1760
+
1761
+ // Draw table header
1762
+ pdf.setFillColor(240, 240, 240);
1763
+ pdf.rect(margin, tableY, contentWidth, 20, 'F');
1764
+
1765
+ pdf.setFont("helvetica", "bold");
1766
+ pdf.setFontSize(10);
1767
+ pdf.setTextColor(0, 0, 0);
1768
+
1769
+ headerCells.forEach((cell, index) => {
1770
+ const text = cell.textContent.trim();
1771
+ const x = margin + (index * colWidth) + 5;
1772
+ pdf.text(text, x, tableY + 13);
1773
+ });
1774
+
1775
+ // Draw horizontal line after header
1776
+ pdf.setDrawColor(200, 200, 200);
1777
+ pdf.setLineWidth(0.5);
1778
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
1779
+
1780
+ tableY += 20;
1781
+
1782
+ // Draw table rows
1783
+ pdf.setFont("helvetica", "normal");
1784
+ for (let i = 1; i < rows.length; i++) {
1785
+ // Check if we need a new page
1786
+ if (tableY + 20 > pdfHeight - margin) {
1787
+ // Draw bottom border for last row on current page
1788
+ pdf.line(margin, tableY, margin + contentWidth, tableY);
1789
+
1790
+ // Add new page
1791
+ pageNum++;
1792
+ addPageWithHeader(pageNum);
1793
+ tableY = margin + 10;
1794
+
1795
+ // Redraw header on new page
1796
+ pdf.setFillColor(240, 240, 240);
1797
+ pdf.rect(margin, tableY, contentWidth, 20, 'F');
1798
+
1799
+ pdf.setFont("helvetica", "bold");
1800
+ headerCells.forEach((cell, index) => {
1801
+ const text = cell.textContent.trim();
1802
+ const x = margin + (index * colWidth) + 5;
1803
+ pdf.text(text, x, tableY + 13);
1804
+ });
1805
+
1806
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
1807
+ tableY += 20;
1808
+ pdf.setFont("helvetica", "normal");
1809
+ }
1810
+
1811
+ // Get cells for this row
1812
+ const cells = Array.from(rows[i].querySelectorAll('td, th'));
1813
+
1814
+ // Alternate row background for better readability
1815
+ if (i % 2 === 0) {
1816
+ pdf.setFillColor(250, 250, 250);
1817
+ pdf.rect(margin, tableY, contentWidth, 20, 'F');
1818
+ }
1819
+
1820
+ // Add cell content
1821
+ cells.forEach((cell, index) => {
1822
+ const text = cell.textContent.trim();
1823
+ const x = margin + (index * colWidth) + 5;
1824
+ pdf.text(text, x, tableY + 13);
1825
+ });
1826
+
1827
+ // Draw horizontal line after row
1828
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
1829
+ tableY += 20;
1830
+ }
1831
+
1832
+ // Draw vertical lines for columns
1833
+ for (let i = 0; i <= numColumns; i++) {
1834
+ const x = margin + (i * colWidth);
1835
+ pdf.line(x, currentY + 10, x, tableY);
1836
+ }
1837
+
1838
+ currentY = tableY + 10;
1839
+ }
1840
+ // Images still need to be handled as images
1841
+ else if (element.tagName === 'IMG' || element.querySelector('img')) {
1842
+ const imgElement = element.tagName === 'IMG' ? element : element.querySelector('img');
1843
+
1844
+ if (!imgElement || !imgElement.src) continue;
1845
+
1846
+ try {
1847
+ // Create a new image to get dimensions
1848
+ const img = new Image();
1849
+ img.src = imgElement.src;
1850
+
1851
+ // Calculate dimensions
1852
+ const imgWidth = contentWidth;
1853
+ const imgHeight = img.height * (contentWidth / img.width);
1854
+
1855
+ // Check if we need a new page
1856
+ if (currentY + imgHeight > pdfHeight - margin) {
1857
+ pageNum++;
1858
+ addPageWithHeader(pageNum);
1859
+ currentY = margin;
1860
+ }
1861
+
1862
+ // Add image to PDF
1863
+ pdf.addImage(img.src, 'JPEG', margin, currentY, imgWidth, imgHeight);
1864
+ currentY += imgHeight + 10;
1865
+ } catch (imgError) {
1866
+ console.error('Error adding image:', imgError);
1867
+ pdf.text("[Image could not be rendered]", margin, currentY + 12);
1868
+ currentY += 20;
1869
+ }
1870
+ }
1871
+ // Other complex elements still use html2canvas as fallback
1872
+ else {
1873
+ try {
1874
+ const canvas = await html2canvas(element, {
1875
+ scale: 2,
1876
+ useCORS: true,
1877
+ logging: false,
1878
+ backgroundColor: '#FFFFFF'
1879
+ });
1880
+
1881
+ const imgData = canvas.toDataURL('image/png');
1882
+ const imgWidth = contentWidth;
1883
+ const imgHeight = (canvas.height * contentWidth) / canvas.width;
1884
+
1885
+ if (currentY + imgHeight > pdfHeight - margin) {
1886
+ pageNum++;
1887
+ addPageWithHeader(pageNum);
1888
+ currentY = margin;
1889
+ }
1890
+
1891
+ pdf.addImage(imgData, 'PNG', margin, currentY, imgWidth, imgHeight);
1892
+ currentY += imgHeight + 10;
1893
+ } catch (canvasError) {
1894
+ console.error('Error rendering complex element:', canvasError);
1895
+ pdf.text("[Complex content could not be rendered]", margin, currentY + 12);
1896
+ currentY += 20;
1897
+ }
1898
+ }
1899
+ }
1900
+
1901
+ // Download the PDF
1902
+ const filename = `${query.replace(/[^a-z0-9]/gi, '_').substring(0, 30).toLowerCase()}_research.pdf`;
1903
+ pdf.save(filename);
1904
+
1905
+ // Clean up
1906
+ document.body.removeChild(contentClone);
1907
+ resultsContent.style.display = 'block';
1908
+ loadingIndicator.remove();
1909
+ } catch (error) {
1910
+ console.error('Error generating PDF:', error);
1911
+ alert('An error occurred while generating the PDF. Please try again.');
1912
+ document.body.removeChild(contentClone);
1913
+ resultsContent.style.display = 'block';
1914
+ loadingIndicator.remove();
1915
+ }
1916
+ };
1917
+
1918
+ generateTextBasedPDF();
1919
+ } catch (error) {
1920
+ console.error('Error initializing PDF generation:', error);
1921
+ alert('An error occurred while preparing the PDF. Please try again.');
1922
+ document.body.removeChild(contentClone);
1923
+ resultsContent.style.display = 'block';
1924
+ loadingIndicator.remove();
1925
+ }
1926
+ }, 100);
1927
+ }
1928
+
1929
+ // Function to generate PDF from a specific research ID
1930
+ async function generatePdfFromResearch(researchId) {
1931
+ try {
1932
+ // Load research details
1933
+ const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
1934
+ const details = await detailsResponse.json();
1935
+
1936
+ // Load the report content
1937
+ const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
1938
+ const reportData = await reportResponse.json();
1939
+
1940
+ if (reportData.status === 'success') {
1941
+ // Create a temporary container to render the content
1942
+ const tempContainer = document.createElement('div');
1943
+ tempContainer.className = 'results-content pdf-optimized';
1944
+ tempContainer.style.display = 'none';
1945
+ document.body.appendChild(tempContainer);
1946
+
1947
+ // Render markdown with optimized styles for PDF
1948
+ const renderedContent = marked.parse(reportData.content);
1949
+ tempContainer.innerHTML = renderedContent;
1950
+
1951
+ // Apply syntax highlighting
1952
+ tempContainer.querySelectorAll('pre code').forEach((block) => {
1953
+ hljs.highlightElement(block);
1954
+ });
1955
+
1956
+ // Format date
1957
+ let dateText = formatDate(new Date(details.completed_at || details.created_at));
1958
+ if (details.duration_seconds) {
1959
+ let durationText = '';
1960
+ const duration = parseInt(details.duration_seconds);
1961
+
1962
+ if (duration < 60) {
1963
+ durationText = `${duration}s`;
1964
+ } else if (duration < 3600) {
1965
+ durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
1966
+ } else {
1967
+ durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
1968
+ }
1969
+
1970
+ dateText += ` (Duration: ${durationText})`;
1971
+ }
1972
+
1973
+ // Set up data for PDF generation
1974
+ document.getElementById('result-query').textContent = details.query;
1975
+ document.getElementById('result-date').textContent = dateText;
1976
+ document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
1977
+
1978
+ // Replace the current content with our temporary content
1979
+ const resultsContent = document.getElementById('results-content');
1980
+ const originalContent = resultsContent.innerHTML;
1981
+ resultsContent.innerHTML = tempContainer.innerHTML;
1982
+
1983
+ // Generate the PDF
1984
+ generatePdf();
1985
+
1986
+ // Restore original content if we're not on the results page
1987
+ setTimeout(() => {
1988
+ if (!document.getElementById('research-results').classList.contains('active')) {
1989
+ resultsContent.innerHTML = originalContent;
1990
+ }
1991
+ document.body.removeChild(tempContainer);
1992
+ }, 500);
1993
+
1994
+ } else {
1995
+ alert('Error loading report. Could not generate PDF.');
1996
+ }
1997
+ } catch (error) {
1998
+ console.error('Error generating PDF:', error);
1999
+ alert('An error occurred while generating the PDF. Please try again.');
2000
+ }
2001
+ }
2002
+
2003
+ // Initialize the terminate button event listener
2004
+ const terminateBtn = document.getElementById('terminate-research-btn');
2005
+ if (terminateBtn) {
2006
+ terminateBtn.addEventListener('click', () => {
2007
+ if (confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
2008
+ if (currentResearchId) {
2009
+ terminateResearch(currentResearchId);
2010
+ }
2011
+ }
2012
+ });
2013
+ }
2014
+
2015
+ // Initialize PDF download button
2016
+ const downloadPdfBtn = document.getElementById('download-pdf-btn');
2017
+ if (downloadPdfBtn) {
2018
+ downloadPdfBtn.addEventListener('click', generatePdf);
2019
+ }
2020
+
2021
+ // Function to set up the research form
2022
+ function setupResearchForm() {
2023
+ const researchForm = document.getElementById('research-form');
2024
+ const notificationToggle = document.getElementById('notification-toggle');
2025
+
2026
+ // Set notification state from toggle
2027
+ if (notificationToggle) {
2028
+ notificationsEnabled = notificationToggle.checked;
2029
+
2030
+ // Listen for changes to the toggle
2031
+ notificationToggle.addEventListener('change', function() {
2032
+ notificationsEnabled = this.checked;
2033
+ // Store preference in localStorage for persistence
2034
+ localStorage.setItem('notificationsEnabled', notificationsEnabled);
2035
+ });
2036
+
2037
+ // Load saved preference from localStorage
2038
+ const savedPref = localStorage.getItem('notificationsEnabled');
2039
+ if (savedPref !== null) {
2040
+ notificationsEnabled = savedPref === 'true';
2041
+ notificationToggle.checked = notificationsEnabled;
2042
+ }
2043
+ }
2044
+
2045
+ // ... existing form setup ...
2046
+ }
2047
+
2048
+ // Add event listener for view results button
2049
+ const viewResultsBtn = document.getElementById('view-results-btn');
2050
+ if (viewResultsBtn) {
2051
+ viewResultsBtn.addEventListener('click', () => {
2052
+ console.log('View results button clicked');
2053
+ if (currentResearchId) {
2054
+ loadResearch(currentResearchId);
2055
+ } else {
2056
+ console.error('No research ID available');
2057
+ }
2058
+ });
2059
+ }
2060
+
2061
+ // Add listener for research_completed custom event
2062
+ document.addEventListener('research_completed', (event) => {
2063
+ console.log('Research completed event received:', event.detail);
2064
+ const data = event.detail;
2065
+
2066
+ // Mark research as no longer in progress
2067
+ isResearchInProgress = false;
2068
+
2069
+ // Hide terminate button
2070
+ const terminateBtn = document.getElementById('terminate-research-btn');
2071
+ if (terminateBtn) {
2072
+ terminateBtn.style.display = 'none';
2073
+ }
2074
+
2075
+ // Update navigation
2076
+ updateNavigationBasedOnResearchStatus();
2077
+ });
2078
+ });