local-deep-research 0.1.1__py3-none-any.whl → 0.1.13__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.
@@ -20,19 +20,25 @@ document.addEventListener('DOMContentLoaded', () => {
20
20
  // Add function to cleanup research resources globally
21
21
  window.cleanupResearchResources = function() {
22
22
  console.log('Cleaning up research resources');
23
+
24
+ // Disconnect any active sockets
25
+ disconnectAllSockets();
26
+
27
+ // Remove any active research data
28
+ if (window.currentResearchId) {
29
+ console.log(`Cleaning up research ID: ${window.currentResearchId}`);
30
+ window.currentResearchId = null;
31
+ }
32
+
33
+ // Reset any active timers
23
34
  if (pollingInterval) {
24
35
  clearInterval(pollingInterval);
25
36
  pollingInterval = null;
37
+ console.log('Cleared polling interval during cleanup');
26
38
  }
27
- isResearchInProgress = false;
28
- currentResearchId = null;
29
- window.currentResearchId = null;
30
39
 
31
- // Hide the terminate button if it exists
32
- const terminateBtn = document.getElementById('terminate-research-btn');
33
- if (terminateBtn) {
34
- terminateBtn.style.display = 'none';
35
- }
40
+ // Reset research state flags
41
+ isResearchInProgress = false;
36
42
  };
37
43
 
38
44
  // Initialize notification sounds
@@ -198,137 +204,342 @@ document.addEventListener('DOMContentLoaded', () => {
198
204
  }
199
205
  };
200
206
 
201
- // Helper function to connect to research updates
202
- window.connectToResearchSocket = function(researchId) {
207
+ // Function to connect to socket for a research
208
+ window.connectToResearchSocket = async function(researchId) {
209
+ if (!researchId) {
210
+ console.error('No research ID provided for socket connection');
211
+ return;
212
+ }
213
+
203
214
  try {
204
- // Check if the research ID is valid
205
- if (!researchId) {
206
- console.error('Invalid research ID for socket connection');
207
- return false;
208
- }
215
+ // Check if research is terminated/suspended before connecting
216
+ const response = await fetch(getApiUrl(`/api/research/${researchId}`));
217
+ const data = await response.json();
209
218
 
210
- // First disconnect any existing socket to avoid duplicates
211
- window.disconnectSocket();
219
+ // Don't connect to socket for terminated or suspended research
220
+ if (data.status === 'suspended' || data.status === 'failed') {
221
+ console.log(`Not connecting socket for ${data.status} research ${researchId}`);
222
+
223
+ // Make sure UI reflects the suspended state
224
+ updateTerminationUIState('suspended', `Research was ${data.status}`);
225
+ return;
226
+ }
227
+ // Don't connect to completed research
228
+ else if (data.status === 'completed') {
229
+ console.log(`Not connecting socket for completed research ${researchId}`);
230
+ return;
231
+ }
212
232
 
213
- // Initialize socket if needed
214
- socket = initializeSocket();
233
+ console.log(`Connecting to socket for research ${researchId} (status: ${data.status})`);
215
234
 
235
+ // Initialize socket if it doesn't exist
216
236
  if (!socket) {
217
- console.error('Failed to initialize socket for research updates');
218
- return false;
237
+ initializeSocket();
219
238
  }
220
239
 
221
- // Subscribe to research updates
222
- socket.emit('subscribe_to_research', { research_id: researchId });
223
-
224
- // Set up event listener for research progress
225
- const progressEventName = `research_progress_${researchId}`;
240
+ // Subscribe to the research channel
241
+ if (socket && socket.connected) {
242
+ socket.emit('subscribe_to_research', { research_id: researchId });
243
+ console.log(`Subscribed to research ${researchId}`);
244
+ } else {
245
+ console.warn('Socket not connected, waiting for connection...');
246
+ // Wait for socket to connect
247
+ const maxAttempts = 5;
248
+ let attempts = 0;
249
+
250
+ const socketConnectInterval = setInterval(() => {
251
+ attempts++;
252
+ if (socket && socket.connected) {
253
+ socket.emit('subscribe_to_research', { research_id: researchId });
254
+ console.log(`Subscribed to research ${researchId} after ${attempts} attempts`);
255
+ clearInterval(socketConnectInterval);
256
+ } else if (attempts >= maxAttempts) {
257
+ console.error(`Failed to connect to socket after ${maxAttempts} attempts`);
258
+ clearInterval(socketConnectInterval);
259
+ addConsoleLog('Failed to connect to real-time updates', 'error');
260
+ }
261
+ }, 1000);
262
+ }
263
+ } catch (error) {
264
+ console.error(`Error connecting to socket for research ${researchId}:`, error);
265
+ }
266
+ };
267
+
268
+ // Format the research status for display
269
+ function formatStatus(status) {
270
+ if (!status) return 'Unknown';
271
+
272
+ // Handle in_progress specially
273
+ if (status === 'in_progress') return 'In Progress';
274
+
275
+ // Capitalize first letter for other statuses
276
+ return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
277
+ }
278
+
279
+ // Format the research mode for display
280
+ function formatMode(mode) {
281
+ if (!mode) return 'Unknown';
282
+
283
+ return mode === 'detailed' ? 'Detailed Report' : 'Quick Summary';
284
+ }
285
+
286
+ // Format a date for display
287
+ function formatDate(date, duration = null) {
288
+ // Handle null/undefined gracefully
289
+ if (!date) return 'Unknown';
226
290
 
227
- // Remove existing listeners to prevent duplicates
291
+ // Check if we have a date string instead of a Date object
292
+ if (typeof date === 'string') {
228
293
  try {
229
- socket.off(progressEventName);
294
+ // Handle ISO string with microseconds (which causes problems)
295
+ if (date.includes('.') && date.includes('T')) {
296
+ // Extract only up to milliseconds (3 digits after dot) or remove microseconds entirely
297
+ const parts = date.split('.');
298
+ if (parts.length > 1) {
299
+ // If there's a Z or + or - after microseconds, preserve it
300
+ let timezone = '';
301
+ const microsecondPart = parts[1];
302
+ const tzIndex = microsecondPart.search(/[Z+-]/);
303
+
304
+ if (tzIndex !== -1) {
305
+ timezone = microsecondPart.substring(tzIndex);
306
+ }
307
+
308
+ // Use only milliseconds (first 3 digits after dot) or none if format issues
309
+ const milliseconds = microsecondPart.substring(0, Math.min(3, tzIndex !== -1 ? tzIndex : microsecondPart.length));
310
+
311
+ // Reconstruct with controlled precision
312
+ const cleanedDateStr = parts[0] + (milliseconds.length > 0 ? '.' + milliseconds : '') + timezone;
313
+ date = new Date(cleanedDateStr);
314
+ } else {
315
+ date = new Date(date);
316
+ }
317
+ } else {
318
+ date = new Date(date);
319
+ }
230
320
  } catch (e) {
231
- console.warn(`Error removing existing listeners for ${progressEventName}`, e);
321
+ console.warn('Error parsing date string:', e);
322
+ return 'Invalid date'; // Return error message if we can't parse
323
+ }
324
+ }
325
+
326
+ // Ensure we're handling the date properly
327
+ if (!(date instanceof Date) || isNaN(date.getTime())) {
328
+ console.warn('Invalid date provided to formatDate:', date);
329
+ return 'Invalid date';
330
+ }
331
+
332
+ // Get current year to compare with date year
333
+ const currentYear = new Date().getFullYear();
334
+ const dateYear = date.getFullYear();
335
+
336
+ // Get month name, day, and time
337
+ const month = date.toLocaleString('en-US', { month: 'short' });
338
+ const day = date.getDate();
339
+ const hours = date.getHours().toString().padStart(2, '0');
340
+ const minutes = date.getMinutes().toString().padStart(2, '0');
341
+
342
+ // Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
343
+ let formattedDate;
344
+ if (dateYear === currentYear) {
345
+ formattedDate = `${month} ${day}, ${hours}:${minutes}`;
346
+ } else {
347
+ formattedDate = `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
348
+ }
349
+
350
+ // Add duration if provided
351
+ if (duration) {
352
+ let durationText = '';
353
+ const durationSec = typeof duration === 'number' ? duration : parseInt(duration);
354
+
355
+ if (durationSec < 60) {
356
+ durationText = `${durationSec}s`;
357
+ } else if (durationSec < 3600) {
358
+ durationText = `${Math.floor(durationSec / 60)}m ${durationSec % 60}s`;
359
+ } else {
360
+ durationText = `${Math.floor(durationSec / 3600)}h ${Math.floor((durationSec % 3600) / 60)}m`;
361
+ }
362
+
363
+ formattedDate += ` (Duration: ${durationText})`;
364
+ }
365
+
366
+ return formattedDate;
367
+ }
368
+
369
+ // Update the socket event handler to fix termination handling
370
+ window.handleResearchProgressEvent = function(data) {
371
+ console.log('Research progress update:', data);
372
+
373
+ // Extract research ID from the event
374
+ const eventResearchId = getActiveResearchId();
375
+
376
+ // Track processed messages to prevent duplicates
377
+ window.processedMessages = window.processedMessages || new Set();
378
+
379
+ // Add to console log if there's a message
380
+ if (data.message) {
381
+ let logType = 'info';
382
+
383
+ // Create a unique identifier for this message (message + timestamp if available)
384
+ const messageId = data.message + (data.log_entry?.time || '');
385
+
386
+ // Check if we've already processed this message
387
+ if (!window.processedMessages.has(messageId)) {
388
+ window.processedMessages.add(messageId);
389
+
390
+ // Determine log type based on status or message content
391
+ if (data.status === 'failed' || data.status === 'suspended' || data.status === 'terminating') {
392
+ logType = 'error';
393
+ } else if (isMilestoneLog(data.message, data.log_entry?.metadata)) {
394
+ logType = 'milestone';
395
+ }
396
+
397
+ // Store meaningful messages to avoid overwriting with generic messages
398
+ if (data.message && data.message !== 'Processing research...') {
399
+ window.lastMeaningfulStatusMessage = data.message;
400
+ }
401
+
402
+ // Extract metadata for search engine information
403
+ const metadata = data.log_entry?.metadata || null;
404
+
405
+ // Pass metadata to addConsoleLog for potential search engine info
406
+ // Pass the research ID to respect the current viewing context
407
+ addConsoleLog(data.message, logType, metadata, eventResearchId);
232
408
  }
409
+ }
410
+
411
+ // Add error messages to log
412
+ if (data.error && !window.processedMessages.has('error:' + data.error)) {
413
+ window.processedMessages.add('error:' + data.error);
414
+ addConsoleLog(data.error, 'error', null, eventResearchId);
233
415
 
234
- // Add new listener
235
- socket.on(progressEventName, (data) => {
236
- console.log('Received research progress update:', data);
416
+ // Store error as last meaningful message
417
+ window.lastMeaningfulStatusMessage = data.error;
418
+ }
419
+
420
+ // Update progress UI if progress is provided
421
+ if (data.progress !== undefined) {
422
+ const displayMessage = data.message || window.lastMeaningfulStatusMessage || 'Processing research...';
423
+ updateProgressUI(data.progress, data.status, displayMessage);
424
+ }
425
+
426
+ // Update detail log if log_entry is provided
427
+ if (data.log_entry) {
428
+ updateDetailLogEntry(data.log_entry);
429
+ }
430
+
431
+ // Handle status changes
432
+ if (data.status) {
433
+ // Special handling for terminating status and handling already terminated research
434
+ if (data.status === 'terminating') {
435
+ // Immediately mark as suspended and update UI
436
+ isResearchInProgress = false;
237
437
 
238
- // Handle different message types
239
- if (data.status === 'in_progress') {
240
- // Update progress
241
- updateProgressUI(data.progress, data.status, data.message);
242
- } else if (data.status === 'completed') {
243
- // Research completed
244
- console.log('Socket received research complete notification');
245
-
246
- // Clear polling interval
247
- if (pollingInterval) {
248
- console.log('Clearing polling interval from socket event');
249
- clearInterval(pollingInterval);
250
- pollingInterval = null;
251
- }
252
-
253
- // Load the results
254
- loadResearch(researchId);
255
-
256
- // Play notification sound
257
- console.log('Playing success notification sound from socket event');
438
+ // Update UI state
439
+ updateTerminationUIState('suspending', data.message || 'Terminating research...');
440
+ }
441
+ // Handle suspended research specifically
442
+ else if (data.status === 'suspended') {
443
+ console.log('Research was suspended, updating UI directly');
444
+
445
+ const researchId = getActiveResearchId();
446
+ if (!researchId) return;
447
+
448
+ // Mark research as not in progress
449
+ isResearchInProgress = false;
450
+
451
+ // Clear polling interval
452
+ if (pollingInterval) {
453
+ console.log('Clearing polling interval due to suspension');
454
+ clearInterval(pollingInterval);
455
+ pollingInterval = null;
456
+ }
457
+
458
+ // Play error notification sound
459
+ playNotificationSound('error');
460
+
461
+ // Update UI for suspended research
462
+ updateTerminationUIState('suspended', data.message || 'Research was suspended');
463
+
464
+ // Add console log
465
+ addConsoleLog('Research suspended', 'error');
466
+
467
+ // Reset lastMeaningfulStatusMessage for next research
468
+ window.lastMeaningfulStatusMessage = '';
469
+
470
+ // Reset current research ID
471
+ currentResearchId = null;
472
+ window.currentResearchId = null;
473
+
474
+ // Update navigation
475
+ updateNavigationBasedOnResearchStatus();
476
+
477
+ // Refresh history if on history page
478
+ if (document.getElementById('history').classList.contains('active')) {
479
+ loadResearchHistory();
480
+ }
481
+ }
482
+ // Handle completion states
483
+ else if (data.status === 'completed' || data.status === 'failed') {
484
+ const researchId = getActiveResearchId();
485
+ if (!researchId) return;
486
+
487
+ // Mark research as not in progress
488
+ isResearchInProgress = false;
489
+
490
+ // Clear polling interval
491
+ if (pollingInterval) {
492
+ console.log('Clearing polling interval from socket event');
493
+ clearInterval(pollingInterval);
494
+ pollingInterval = null;
495
+ }
496
+
497
+ if (data.status === 'completed') {
498
+ // Success sound and notification
258
499
  playNotificationSound('success');
259
500
 
260
- // Clean up resources
261
- window.cleanupResearchResources();
501
+ // Store the completed research ID for navigation
502
+ const completedResearchId = researchId;
262
503
 
263
- // Update navigation and research status display
264
- updateNavigationBasedOnResearchStatus();
504
+ // Reset current research ID
505
+ currentResearchId = null;
506
+ window.currentResearchId = null;
265
507
 
266
- // Update history item status if on history page
267
- updateHistoryItemStatus(researchId, 'completed');
508
+ // Reset lastMeaningfulStatusMessage for next research
509
+ window.lastMeaningfulStatusMessage = '';
268
510
 
269
- // Dispatch event for any other components that need to know about completion
270
- document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
271
- } else if (data.status === 'failed' || data.status === 'suspended') {
272
- // Research failed or was suspended
273
- console.log(`Socket received research final state: ${data.status}`);
274
-
275
- // Clear polling interval
276
- if (pollingInterval) {
277
- console.log('Clearing polling interval from socket event');
278
- clearInterval(pollingInterval);
279
- pollingInterval = null;
280
- }
281
-
282
- // Get the error message from the socket response
283
- const errorText = data.error || data.message || (data.status === 'failed' ? 'Research failed' : 'Research was suspended');
284
- console.log(`Error message from socket: ${errorText}`);
285
-
286
- // Show error message
287
- console.log(`Showing error message for status: ${data.status} from socket event`);
288
- const errorMessage = document.getElementById('error-message');
289
- if (errorMessage) {
290
- errorMessage.style.display = 'block';
291
- errorMessage.textContent = errorText;
292
- }
293
-
294
- // Update UI with status
295
- updateProgressUI(
296
- 0,
297
- data.status,
298
- errorText
299
- );
511
+ // Update navigation state
512
+ updateNavigationBasedOnResearchStatus();
300
513
 
301
- // Update history item status if on history page
302
- updateHistoryItemStatus(researchId, data.status, errorText);
514
+ // Navigate to results page with a slight delay to ensure all updates are processed
515
+ setTimeout(() => {
516
+ // Load the research results
517
+ loadResearch(completedResearchId);
518
+ }, 800);
519
+ } else {
520
+ // Error sound and notification
521
+ playNotificationSound('error');
303
522
 
304
- // Update navigation
305
- updateNavigationBasedOnResearchStatus();
523
+ // Use the updateTerminationUIState function for consistency
524
+ updateTerminationUIState('suspended', data.error || `Research was ${data.status}`);
306
525
 
307
- if (data.status === 'failed') {
308
- console.log('Playing error notification sound from socket event');
309
- playNotificationSound('error');
310
- }
526
+ // Reset lastMeaningfulStatusMessage for next research
527
+ window.lastMeaningfulStatusMessage = '';
311
528
 
312
- // Clean up resources
313
- window.cleanupResearchResources();
529
+ // Reset current research ID
530
+ currentResearchId = null;
531
+ window.currentResearchId = null;
314
532
 
315
- // Update navigation and research status display
533
+ // Update navigation - important for correctly showing/hiding various elements
534
+ // based on the current research state
316
535
  updateNavigationBasedOnResearchStatus();
317
-
318
- // Dispatch event for any other components that need to know about completion
319
- document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
320
536
  }
321
537
 
322
- // Update the detailed log if on details page
323
- if (data.log_entry && document.getElementById('research-log')) {
324
- updateDetailLogEntry(data.log_entry);
538
+ // Refresh the history list to show the completed research
539
+ if (document.getElementById('history').classList.contains('active')) {
540
+ loadResearchHistory();
325
541
  }
326
- });
327
-
328
- return true;
329
- } catch (e) {
330
- console.error('Error connecting to research socket:', e);
331
- return false;
542
+ }
332
543
  }
333
544
  };
334
545
 
@@ -412,225 +623,320 @@ document.addEventListener('DOMContentLoaded', () => {
412
623
 
413
624
  // Function to start research
414
625
  async function startResearch(query, mode) {
415
- // Reset potentially stale state
416
- if (isResearchInProgress) {
417
- // Force a fresh check of active research status
418
- try {
419
- const response = await fetch(getApiUrl('/api/history'));
420
- const history = await response.json();
421
-
422
- // Find any in-progress research
423
- const activeResearch = history.find(item => item.status === 'in_progress');
424
-
425
- // If no active research is found, reset the state
426
- if (!activeResearch) {
427
- console.log('Resetting stale research state - no active research found in history');
428
- window.cleanupResearchResources();
429
- } else {
430
- alert('Another research is already in progress. Please wait for it to complete or check its status in the history tab.');
431
- return;
432
- }
433
- } catch (error) {
434
- console.error('Error checking for active research:', error);
435
- }
626
+ // First validate that we have a query
627
+ if (!query || query.trim() === '') {
628
+ alert('Please enter a query');
629
+ return;
436
630
  }
437
631
 
438
- // Get the start button
439
- const startResearchBtn = document.getElementById('start-research-btn');
440
- if (!startResearchBtn) return;
441
-
442
- // Disable the start button while we attempt to start the research
443
- startResearchBtn.disabled = true;
444
- startResearchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
445
-
446
632
  try {
447
- // Set the favicon based on research mode
448
- setFavicon(mode);
633
+ // Update button state
634
+ const startBtn = document.getElementById('start-research-btn');
635
+ if (startBtn) {
636
+ startBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
637
+ startBtn.disabled = true;
638
+ }
639
+
640
+ // Clear any previous research
641
+ resetResearchState();
642
+
643
+ // Set favicon to loading
644
+ setFavicon('loading');
449
645
 
646
+ // Get the current query element
647
+ const currentQueryEl = document.getElementById('current-query');
648
+ if (currentQueryEl) {
649
+ currentQueryEl.textContent = query;
650
+ }
651
+
652
+ // Create payload
653
+ const payload = {
654
+ query: query,
655
+ mode: mode || 'quick'
656
+ };
657
+
658
+ // Call the API
450
659
  const response = await fetch(getApiUrl('/api/start_research'), {
451
660
  method: 'POST',
452
661
  headers: {
453
662
  'Content-Type': 'application/json'
454
663
  },
455
- body: JSON.stringify({
456
- query: query,
457
- mode: mode
458
- })
664
+ body: JSON.stringify(payload)
459
665
  });
460
666
 
461
- const data = await response.json();
667
+ // Parse the response
668
+ const result = await response.json();
462
669
 
463
- if (data.status === 'success') {
670
+ if (result.status === 'success') {
671
+ // Update the current research ID
672
+ currentResearchId = result.research_id;
673
+ window.currentResearchId = result.research_id;
674
+
675
+ console.log(`Started research with ID: ${currentResearchId}`);
676
+
677
+ // Mark as in progress
464
678
  isResearchInProgress = true;
465
- currentResearchId = data.research_id;
466
679
 
467
- // Also update the window object
468
- window.currentResearchId = data.research_id;
680
+ // Hide the try again button if visible
681
+ const tryAgainBtn = document.getElementById('try-again-btn');
682
+ if (tryAgainBtn) {
683
+ tryAgainBtn.style.display = 'none';
684
+ }
469
685
 
470
- // Update the navigation to show Research in Progress
686
+ // Reset progress UI
687
+ updateProgressUI(0, 'in_progress', `Researching: ${query}`);
688
+
689
+ // Store query in case we need to display it again
690
+ window.currentResearchQuery = query;
691
+
692
+ // Update navigation
471
693
  updateNavigationBasedOnResearchStatus();
472
694
 
473
- // Update progress page
474
- document.getElementById('current-query').textContent = query;
475
- document.getElementById('progress-fill').style.width = '0%';
476
- document.getElementById('progress-percentage').textContent = '0%';
477
- document.getElementById('progress-status').textContent = 'Initializing research process...';
478
-
479
- // Navigate to progress page
695
+ // Navigate to the progress page
480
696
  switchPage('research-progress');
481
697
 
482
- // Connect to socket for this research
483
- window.connectToResearchSocket(data.research_id);
698
+ // Connect to the socket for this research
699
+ window.connectToResearchSocket(currentResearchId);
484
700
 
485
701
  // Start polling for status
486
- pollResearchStatus(data.research_id);
487
-
488
- // Show the terminate button
489
- const terminateBtn = document.getElementById('terminate-research-btn');
490
- if (terminateBtn) {
491
- terminateBtn.style.display = 'inline-flex';
492
- terminateBtn.disabled = false;
493
- }
702
+ pollResearchStatus(currentResearchId);
494
703
  } else {
495
- // Reset favicon to default on error
496
- createDynamicFavicon('');
704
+ // Handle error
705
+ const errorMessage = result.message || 'Failed to start research';
706
+ console.error('Research start error:', errorMessage);
707
+
708
+ // Add error to log
709
+ addConsoleLog(`Error: ${errorMessage}`, 'error');
710
+
711
+ alert(errorMessage);
712
+
713
+ // Reset the favicon
714
+ setFavicon('default');
497
715
 
498
- alert('Error starting research: ' + (data.message || 'Unknown error'));
499
- startResearchBtn.disabled = false;
500
- startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
716
+ // Reset button state
717
+ if (startBtn) {
718
+ startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
719
+ startBtn.disabled = false;
720
+ }
501
721
  }
502
722
  } catch (error) {
503
- // Reset favicon to default on error
504
- createDynamicFavicon('⚡');
723
+ console.error('Error starting research:', error);
724
+
725
+ // Add error to log
726
+ addConsoleLog(`Error: ${error.message}`, 'error');
727
+
728
+ // Reset the favicon
729
+ setFavicon('default');
730
+
731
+ // Reset button state
732
+ const startBtn = document.getElementById('start-research-btn');
733
+ if (startBtn) {
734
+ startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
735
+ startBtn.disabled = false;
736
+ }
505
737
 
506
- console.error('Error:', error);
507
738
  alert('An error occurred while starting the research. Please try again.');
508
- startResearchBtn.disabled = false;
509
- startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
510
739
  }
511
740
  }
512
741
 
513
- // Function to poll research status (as a backup to socket.io)
742
+ // Function to clear any existing polling interval
743
+ function clearPollingInterval() {
744
+ if (pollingInterval) {
745
+ console.log('Clearing existing polling interval');
746
+ clearInterval(pollingInterval);
747
+ pollingInterval = null;
748
+ }
749
+ }
750
+
751
+ // Update the polling function to prevent "Processing research..." from overwriting actual status messages
514
752
  function pollResearchStatus(researchId) {
515
- console.log(`Polling research status for ID: ${researchId}`);
516
- fetch(getApiUrl(`/api/research/${researchId}`))
517
- .then(response => {
518
- if (!response.ok) {
519
- throw new Error(`Server returned ${response.status}`);
520
- }
521
- return response.json();
522
- })
523
- .then(data => {
524
- console.log('Research status response:', data);
525
-
526
- // Get the latest progress and log data
527
- const progress = data.progress || 0;
528
- const status = data.status || 'in_progress';
529
-
530
- // Get the latest message from the log if available
531
- let latestMessage = "Processing research...";
532
- if (data.log && Array.isArray(data.log) && data.log.length > 0) {
533
- const latestLog = data.log[data.log.length - 1];
534
- if (latestLog && latestLog.message) {
535
- latestMessage = latestLog.message;
536
- }
537
- }
538
-
539
- // Update the UI with the current progress - always
540
- updateProgressUI(progress, status, latestMessage);
541
-
542
- // If research is complete, show the completion buttons
543
- if (status === 'completed' || status === 'failed' || status === 'suspended') {
544
- console.log(`Research is in final state: ${status}`);
545
- // Clear the polling interval
546
- if (pollingInterval) {
547
- console.log('Clearing polling interval');
548
- clearInterval(pollingInterval);
549
- pollingInterval = null;
753
+ if (!researchId) {
754
+ console.error('No research ID provided for polling');
755
+ return;
756
+ }
757
+
758
+ // Reset message deduplication when starting a new poll
759
+ window.processedMessages = window.processedMessages || new Set();
760
+
761
+ // Don't set "Loading research..." here, as we'll set the actual query from the response
762
+ // Only set loading if currentQuery is empty
763
+ const currentQueryEl = document.getElementById('current-query');
764
+ if (currentQueryEl && (!currentQueryEl.textContent || currentQueryEl.textContent === 'Loading research...')) {
765
+ currentQueryEl.textContent = window.currentResearchQuery || 'Loading research...';
766
+ }
767
+
768
+ console.log(`Starting polling for research ${researchId}`);
769
+
770
+ // Store the last real message to avoid overwriting with generic messages
771
+ window.lastMeaningfulStatusMessage = window.lastMeaningfulStatusMessage || '';
772
+
773
+ // Ensure we have a socket connection
774
+ if (typeof window.connectToResearchSocket === 'function') {
775
+ window.connectToResearchSocket(researchId);
776
+ }
777
+
778
+ // Set polling interval for updates
779
+ if (pollingInterval) {
780
+ clearInterval(pollingInterval);
781
+ }
782
+
783
+ // Make an immediate request
784
+ checkResearchStatus(researchId);
785
+
786
+ // Then set up polling
787
+ pollingInterval = setInterval(() => {
788
+ checkResearchStatus(researchId);
789
+ }, 2000); // Poll every 2 seconds
790
+
791
+ // Function to check research status
792
+ function checkResearchStatus(researchId) {
793
+ fetch(getApiUrl(`/api/research/${researchId}/details`))
794
+ .then(response => response.json())
795
+ .then(data => {
796
+ // Update the current query with the actual query from the research
797
+ const currentQueryEl = document.getElementById('current-query');
798
+ if (currentQueryEl && data.query) {
799
+ currentQueryEl.textContent = data.query;
800
+ // Store the query in case we need it later
801
+ window.currentResearchQuery = data.query;
550
802
  }
551
803
 
552
- // Update UI for completion
553
- if (status === 'completed') {
554
- console.log('Research completed, loading results automatically');
555
- // Hide the terminate button
556
- const terminateBtn = document.getElementById('terminate-research-btn');
557
- if (terminateBtn) {
558
- terminateBtn.style.display = 'none';
559
- }
560
-
561
- // Update history item status
562
- updateHistoryItemStatus(researchId, 'completed');
804
+ // Process status update
805
+ if (data && data.status !== 'error') {
806
+ // Update UI with progress
807
+ const progress = data.progress || 0;
808
+ const status = data.status || 'in_progress';
563
809
 
564
- // Auto-load the results instead of showing a button
565
- loadResearch(researchId);
566
- } else if (status === 'failed' || status === 'suspended') {
567
- console.log(`Showing error message for status: ${status}`);
810
+ // Get most recent message
811
+ let message = '';
812
+ let foundNewMessage = false;
813
+ let latestMetadata = null;
568
814
 
569
- // Get error message from metadata if available or use latest message
570
- let errorText = latestMessage;
571
- if (data.metadata && typeof data.metadata === 'string') {
572
- try {
573
- const metadataObj = JSON.parse(data.metadata);
574
- if (metadataObj.error) {
575
- errorText = metadataObj.error;
815
+ if (data.log && data.log.length > 0) {
816
+ // Get the latest unique log entry
817
+ for (let i = data.log.length - 1; i >= 0; i--) {
818
+ const latestLog = data.log[i];
819
+ const messageId = latestLog.message + (latestLog.time || '');
820
+
821
+ if (!window.processedMessages.has(messageId)) {
822
+ window.processedMessages.add(messageId);
823
+ message = latestLog.message || '';
824
+ latestMetadata = latestLog.metadata || null;
825
+
826
+ // Only update the lastMeaningfulStatusMessage if we have a real message
827
+ if (message && message !== 'Processing research...') {
828
+ window.lastMeaningfulStatusMessage = message;
829
+ foundNewMessage = true;
830
+ }
831
+
832
+ // Add to console logs
833
+ if (message) {
834
+ let logType = 'info';
835
+ if (isMilestoneLog(message, latestLog.metadata)) {
836
+ logType = 'milestone';
837
+ } else if (latestLog.type === 'error' || (latestLog.metadata && latestLog.metadata.phase === 'error')) {
838
+ logType = 'error';
839
+ } else if (latestLog.type) {
840
+ logType = latestLog.type; // Use the type from the database if available
841
+ }
842
+ addConsoleLog(message, logType, latestMetadata);
843
+ }
844
+
845
+ break; // Only process one message per poll
576
846
  }
577
- } catch (e) {
578
- console.log('Error parsing metadata:', e);
579
847
  }
580
848
  }
581
849
 
582
- // Show error message in UI
583
- const errorMessage = document.getElementById('error-message');
584
- if (errorMessage) {
585
- errorMessage.style.display = 'block';
586
- errorMessage.textContent = errorText;
587
- console.log('Set error message to:', errorText);
588
- } else {
589
- console.error('error-message element not found');
590
- }
850
+ // Use a meaningful message if available; otherwise, keep the last good one
851
+ const displayMessage = foundNewMessage ? message :
852
+ (window.lastMeaningfulStatusMessage || 'Processing research...');
591
853
 
592
- // Update progress display with error
593
- updateProgressUI(0, status, errorText);
854
+ // Update progress UI
855
+ updateProgressUI(progress, status, displayMessage);
594
856
 
595
- // Update history item status
596
- updateHistoryItemStatus(researchId, status, errorText);
597
- }
598
-
599
- // Play notification sound based on status
600
- if (status === 'completed') {
601
- console.log('Playing success notification sound');
602
- playNotificationSound('success');
603
- } else if (status === 'failed') {
604
- console.log('Playing error notification sound');
605
- playNotificationSound('error');
606
- }
607
-
608
- // Update the navigation
609
- console.log('Updating navigation based on research status');
610
- updateNavigationBasedOnResearchStatus();
611
-
612
- // Force the UI to update with a manual trigger
613
- document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
614
-
615
- return;
616
- }
617
-
618
- // Continue polling if still in progress
619
- if (status === 'in_progress') {
620
- console.log('Research is still in progress, continuing polling');
621
- if (!pollingInterval) {
622
- console.log('Setting up polling interval');
623
- pollingInterval = setInterval(() => {
624
- pollResearchStatus(researchId);
625
- }, 10000);
857
+ // Update the UI based on research status
858
+ if (status === 'completed' || status === 'failed' || status === 'suspended') {
859
+ // Clear polling interval
860
+ if (pollingInterval) {
861
+ console.log('Clearing polling interval due to status change');
862
+ clearInterval(pollingInterval);
863
+ pollingInterval = null;
864
+ }
865
+
866
+ // Handle completion or failure
867
+ if (status === 'completed') {
868
+ addConsoleLog('Research completed successfully', 'milestone');
869
+ playNotificationSound('success');
870
+
871
+ // Store the completed research ID for navigation
872
+ const completedResearchId = researchId;
873
+
874
+ // Reset current research ID
875
+ currentResearchId = null;
876
+ window.currentResearchId = null;
877
+
878
+ // Update navigation state
879
+ isResearchInProgress = false;
880
+ updateNavigationBasedOnResearchStatus();
881
+
882
+ // Reset lastMeaningfulStatusMessage for next research
883
+ window.lastMeaningfulStatusMessage = '';
884
+
885
+ // Navigate to results page with a slight delay to ensure all updates are processed
886
+ setTimeout(() => {
887
+ // Load the research results
888
+ loadResearch(completedResearchId);
889
+ }, 800);
890
+ } else {
891
+ addConsoleLog(`Research ${status}`, 'error');
892
+ playNotificationSound('error');
893
+
894
+ // Show error message and Try Again button
895
+ const errorMessage = document.getElementById('error-message');
896
+ const tryAgainBtn = document.getElementById('try-again-btn');
897
+
898
+ if (errorMessage) {
899
+ errorMessage.textContent = data.error || `Research was ${status}`;
900
+ errorMessage.style.display = 'block';
901
+ }
902
+
903
+ if (tryAgainBtn) {
904
+ tryAgainBtn.style.display = 'block';
905
+ }
906
+
907
+ // Reset lastMeaningfulStatusMessage for next research
908
+ window.lastMeaningfulStatusMessage = '';
909
+
910
+ // Update navigation
911
+ isResearchInProgress = false;
912
+ currentResearchId = null;
913
+ window.currentResearchId = null;
914
+ updateNavigationBasedOnResearchStatus();
915
+ }
916
+ } else {
917
+ // Research is still in progress
918
+ isResearchInProgress = true;
919
+ currentResearchId = researchId;
920
+ window.currentResearchId = researchId;
921
+ }
626
922
  }
627
- }
628
- })
629
- .catch(error => {
630
- console.error('Error polling research status:', error);
631
- });
923
+ })
924
+ .catch(error => {
925
+ console.error('Error polling research status:', error);
926
+ // Don't clear the interval on error - just keep trying
927
+ });
928
+ }
632
929
  }
633
930
 
931
+ // Function to reset the start research button to its default state
932
+ function resetStartResearchButton() {
933
+ const startBtn = document.getElementById('start-research-btn');
934
+ if (startBtn) {
935
+ startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
936
+ startBtn.disabled = false;
937
+ }
938
+ }
939
+
634
940
  // Main initialization function
635
941
  function initializeApp() {
636
942
  console.log('Initializing application...');
@@ -638,14 +944,33 @@ document.addEventListener('DOMContentLoaded', () => {
638
944
  // Initialize the sounds
639
945
  initializeSounds();
640
946
 
947
+ // Initialize socket connection
948
+ initializeSocket();
949
+
641
950
  // Create a dynamic favicon with the lightning emoji by default
642
951
  createDynamicFavicon('⚡');
643
952
 
953
+ // Add try again button handler
954
+ const tryAgainBtn = document.getElementById('try-again-btn');
955
+ if (tryAgainBtn) {
956
+ tryAgainBtn.addEventListener('click', function() {
957
+ // Switch back to the new research page
958
+ switchPage('new-research');
959
+
960
+ // Reset the research state
961
+ resetResearchState();
962
+
963
+ // Reset the Start Research button
964
+ resetStartResearchButton();
965
+ });
966
+ }
967
+
644
968
  // Get navigation elements
645
969
  const navItems = document.querySelectorAll('.sidebar-nav li');
646
970
  const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
647
971
  const pages = document.querySelectorAll('.page');
648
972
  const mobileTabBar = document.querySelector('.mobile-tab-bar');
973
+ const logo = document.getElementById('logo-link');
649
974
 
650
975
  // Handle responsive navigation based on screen size
651
976
  function handleResponsiveNavigation() {
@@ -667,6 +992,14 @@ document.addEventListener('DOMContentLoaded', () => {
667
992
  // Add resize listener for responsive design
668
993
  window.addEventListener('resize', handleResponsiveNavigation);
669
994
 
995
+ // Handle logo click
996
+ if (logo) {
997
+ logo.addEventListener('click', () => {
998
+ switchPage('new-research');
999
+ resetStartResearchButton();
1000
+ });
1001
+ }
1002
+
670
1003
  // Setup navigation click handlers
671
1004
  navItems.forEach(item => {
672
1005
  if (!item.classList.contains('external-link')) {
@@ -674,6 +1007,10 @@ document.addEventListener('DOMContentLoaded', () => {
674
1007
  const pageId = this.dataset.page;
675
1008
  if (pageId) {
676
1009
  switchPage(pageId);
1010
+ // Reset Start Research button when returning to the form
1011
+ if (pageId === 'new-research') {
1012
+ resetStartResearchButton();
1013
+ }
677
1014
  }
678
1015
  });
679
1016
  }
@@ -685,6 +1022,10 @@ document.addEventListener('DOMContentLoaded', () => {
685
1022
  const pageId = this.dataset.page;
686
1023
  if (pageId) {
687
1024
  switchPage(pageId);
1025
+ // Reset Start Research button when returning to the form
1026
+ if (pageId === 'new-research') {
1027
+ resetStartResearchButton();
1028
+ }
688
1029
  }
689
1030
  });
690
1031
  }
@@ -738,44 +1079,41 @@ document.addEventListener('DOMContentLoaded', () => {
738
1079
 
739
1080
  // Function to switch between pages
740
1081
  function switchPage(pageId) {
741
- // Get elements directly from the DOM
1082
+ // First hide all pages
742
1083
  const pages = document.querySelectorAll('.page');
743
- const navItems = document.querySelectorAll('.sidebar-nav li');
744
- const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
1084
+ pages.forEach(page => page.classList.remove('active'));
745
1085
 
746
- // Remove active class from all pages
747
- pages.forEach(page => {
748
- page.classList.remove('active');
749
- });
750
-
751
- // Add active class to target page
752
- const targetPage = document.getElementById(pageId);
753
- if (targetPage) {
754
- targetPage.classList.add('active');
1086
+ // Then activate the selected page
1087
+ const selectedPage = document.getElementById(pageId);
1088
+ if (selectedPage) {
1089
+ selectedPage.classList.add('active');
755
1090
  }
756
1091
 
757
- // Update sidebar navigation active states
758
- navItems.forEach(item => {
759
- if (item.getAttribute('data-page') === pageId) {
760
- item.classList.add('active');
761
- } else {
762
- item.classList.remove('active');
763
- }
764
- });
1092
+ // Update the URL hash
1093
+ window.location.hash = '#' + pageId;
765
1094
 
766
- // Update mobile tab bar active states
767
- mobileNavItems.forEach(item => {
768
- if (item.getAttribute('data-page') === pageId) {
769
- item.classList.add('active');
770
- } else {
771
- item.classList.remove('active');
772
- }
773
- });
1095
+ // Update the navigation UI to highlight the active page
1096
+ updateNavigationUI(pageId);
1097
+
1098
+ // Clear console logs when switching to pages that don't show research details
1099
+ if (pageId === 'new-research' || pageId === 'history') {
1100
+ clearConsoleLogs();
1101
+ // Reset the viewing research ID when navigating to non-research pages
1102
+ viewingResearchId = null;
1103
+ }
774
1104
 
775
1105
  // Special handling for history page
776
1106
  if (pageId === 'history') {
777
1107
  loadResearchHistory();
778
1108
  }
1109
+
1110
+ // Reset scroll position for the newly activated page
1111
+ window.scrollTo(0, 0);
1112
+
1113
+ console.log(`Switched to page: ${pageId}`);
1114
+
1115
+ // Update the log panel visibility
1116
+ updateLogPanelVisibility(pageId);
779
1117
  }
780
1118
 
781
1119
  // Track termination status
@@ -794,15 +1132,41 @@ document.addEventListener('DOMContentLoaded', () => {
794
1132
  return `/research${path}`;
795
1133
  }
796
1134
 
797
- // Function to terminate research - exposed to window object
1135
+ // Function to properly disconnect all socket connections
1136
+ function disconnectAllSockets() {
1137
+ if (socket) {
1138
+ try {
1139
+ console.log('Disconnecting all socket connections');
1140
+
1141
+ // Get the active research ID
1142
+ const researchId = getActiveResearchId();
1143
+
1144
+ // If there's an active research, unsubscribe first
1145
+ if (researchId) {
1146
+ console.log(`Unsubscribing from research ${researchId}`);
1147
+ socket.emit('unsubscribe_from_research', { research_id: researchId });
1148
+ }
1149
+
1150
+ // Also attempt to disconnect the socket
1151
+ socket.disconnect();
1152
+ socket = null;
1153
+
1154
+ console.log('Socket disconnected successfully');
1155
+ } catch (error) {
1156
+ console.error('Error disconnecting socket:', error);
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ // Update the terminateResearch function to handle termination more gracefully
798
1162
  async function terminateResearch(researchId) {
799
1163
  if (!researchId) {
800
1164
  console.error('No research ID provided for termination');
801
1165
  return;
802
1166
  }
803
-
1167
+
804
1168
  // Prevent multiple termination requests
805
- if (isTerminating) {
1169
+ if (document.getElementById('terminate-research-btn')?.disabled) {
806
1170
  console.log('Termination already in progress');
807
1171
  return;
808
1172
  }
@@ -812,97 +1176,219 @@ document.addEventListener('DOMContentLoaded', () => {
812
1176
  return;
813
1177
  }
814
1178
 
1179
+ console.log(`Attempting to terminate research: ${researchId}`);
1180
+
815
1181
  try {
816
- // Set terminating flag
817
- isTerminating = true;
818
-
819
- // Update UI to show we're processing
1182
+ // Get the terminate button
820
1183
  const terminateBtn = document.getElementById('terminate-research-btn');
821
1184
  if (terminateBtn) {
1185
+ // Disable the button to prevent multiple clicks
822
1186
  terminateBtn.disabled = true;
823
1187
  terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
824
1188
  }
825
-
826
- // Find all terminate buttons in history items and disable them
827
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
828
- allTerminateBtns.forEach(btn => {
829
- if (btn !== terminateBtn) {
830
- btn.disabled = true;
831
- btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
832
- }
833
- });
834
1189
 
835
- // Call the terminate API
1190
+ // Update UI to show terminating state immediately
1191
+ updateTerminationUIState('suspending', 'Terminating research...');
1192
+
1193
+ // Add a log entry
1194
+ addConsoleLog('Terminating research...', 'error');
1195
+
1196
+ // Immediately mark the research as terminated in our app state
1197
+ // so we don't reconnect to it
1198
+ isResearchInProgress = false;
1199
+
1200
+ // Disconnect all sockets to ensure we stop receiving updates
1201
+ disconnectAllSockets();
1202
+
1203
+ // Clear the polling interval immediately
1204
+ if (pollingInterval) {
1205
+ clearInterval(pollingInterval);
1206
+ pollingInterval = null;
1207
+ console.log('Cleared polling interval during termination');
1208
+ }
1209
+
1210
+ // Call the API to terminate
836
1211
  const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
837
- method: 'POST'
1212
+ method: 'POST',
1213
+ headers: {
1214
+ 'Content-Type': 'application/json'
1215
+ }
838
1216
  });
839
1217
 
840
1218
  const data = await response.json();
841
1219
 
842
1220
  if (data.status === 'success') {
843
- // The UI will be updated via socket events or polling
844
- console.log('Termination request sent successfully');
1221
+ console.log(`Research ${researchId} termination requested successfully`);
845
1222
 
846
- // If we're on the history page, update the status of this item
847
- if (document.getElementById('history').classList.contains('active')) {
848
- updateHistoryItemStatus(researchId, 'terminating', 'Terminating...');
849
- }
1223
+ // Add termination log
1224
+ addConsoleLog('Research termination requested. Please wait...', 'error');
850
1225
 
851
- // Set a timeout to reset UI if socket doesn't respond
852
- setTimeout(() => {
853
- if (isTerminating) {
854
- console.log('Termination timeout - resetting UI');
855
- isTerminating = false;
856
- resetProgressAnimations();
1226
+ // Immediately update UI for better responsiveness
1227
+ updateTerminationUIState('suspended', 'Research was terminated');
1228
+
1229
+ // Start polling for the suspended status with a more reliable approach
1230
+ let checkAttempts = 0;
1231
+ const maxAttempts = 3; // Reduced from 5
1232
+ const checkInterval = setInterval(async () => {
1233
+ checkAttempts++;
1234
+ console.log(`Checking termination status (attempt ${checkAttempts}/${maxAttempts})...`);
1235
+
1236
+ try {
1237
+ const statusResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
1238
+ const statusData = await statusResponse.json();
1239
+
1240
+ // Check for actual termination status in the response
1241
+ if (statusData.status === 'suspended' || statusData.status === 'failed') {
1242
+ console.log(`Research is now ${statusData.status}, updating UI`);
1243
+ clearInterval(checkInterval);
1244
+
1245
+ // Reset research state
1246
+ currentResearchId = null;
1247
+ window.currentResearchId = null;
1248
+
1249
+ // Disconnect any remaining sockets again, just to be sure
1250
+ disconnectAllSockets();
1251
+
1252
+ // Update navigation
1253
+ updateNavigationBasedOnResearchStatus();
1254
+
1255
+ return;
1256
+ }
857
1257
 
858
- // Update the navigation
859
- updateNavigationBasedOnResearchStatus();
1258
+ // If we reach the maximum attempts but status isn't updated yet
1259
+ if (checkAttempts >= maxAttempts) {
1260
+ console.log('Max termination check attempts reached, forcing UI update');
1261
+ clearInterval(checkInterval);
1262
+
1263
+ // Force update to suspended state even if backend hasn't caught up yet
1264
+ currentResearchId = null;
1265
+ window.currentResearchId = null;
1266
+
1267
+ // Disconnect any remaining sockets
1268
+ disconnectAllSockets();
1269
+
1270
+ // Update database status directly with a second termination request
1271
+ try {
1272
+ console.log('Sending second termination request to ensure completion');
1273
+ await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
1274
+ method: 'POST',
1275
+ headers: { 'Content-Type': 'application/json' }
1276
+ });
1277
+ } catch (secondError) {
1278
+ console.error('Error sending second termination request:', secondError);
1279
+ }
1280
+
1281
+ updateNavigationBasedOnResearchStatus();
1282
+ }
1283
+ } catch (checkError) {
1284
+ console.error(`Error checking termination status: ${checkError}`);
1285
+ if (checkAttempts >= maxAttempts) {
1286
+ clearInterval(checkInterval);
1287
+ updateTerminationUIState('error', 'Error checking termination status');
1288
+ }
860
1289
  }
861
- }, 10000); // 10 second timeout
862
- } else {
863
- console.error('Termination request failed:', data.message);
864
- alert(`Failed to terminate research: ${data.message}`);
1290
+ }, 300); // Check faster for more responsive feedback
865
1291
 
866
- // Reset the terminating flag
867
- isTerminating = false;
1292
+ } else {
1293
+ console.error(`Error terminating research: ${data.message}`);
1294
+ addConsoleLog(`Error terminating research: ${data.message}`, 'error');
868
1295
 
869
- // Reset the button
1296
+ // Re-enable the button
870
1297
  if (terminateBtn) {
871
1298
  terminateBtn.disabled = false;
872
1299
  terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
873
1300
  }
874
-
875
- // Reset history button states
876
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
877
- allTerminateBtns.forEach(btn => {
878
- if (btn !== terminateBtn) {
879
- btn.disabled = false;
880
- btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
881
- }
882
- });
883
1301
  }
884
1302
  } catch (error) {
885
- console.error('Error terminating research:', error);
886
- alert('An error occurred while trying to terminate the research.');
1303
+ console.error(`Error in terminate request: ${error}`);
1304
+ addConsoleLog(`Error in terminate request: ${error}`, 'error');
887
1305
 
888
- // Reset the terminating flag
889
- isTerminating = false;
890
-
891
- // Reset the button
1306
+ // Re-enable the button
892
1307
  const terminateBtn = document.getElementById('terminate-research-btn');
893
1308
  if (terminateBtn) {
894
1309
  terminateBtn.disabled = false;
895
1310
  terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
896
1311
  }
897
-
898
- // Reset history button states
899
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
900
- allTerminateBtns.forEach(btn => {
901
- if (btn !== terminateBtn) {
902
- btn.disabled = false;
903
- btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
1312
+ }
1313
+ }
1314
+
1315
+ // Helper function to update UI elements during termination
1316
+ function updateTerminationUIState(state, message) {
1317
+ const terminateBtn = document.getElementById('terminate-research-btn');
1318
+ const errorMessage = document.getElementById('error-message');
1319
+ const tryAgainBtn = document.getElementById('try-again-btn');
1320
+ const progressStatus = document.getElementById('progress-status');
1321
+ const progressBar = document.getElementById('progress-bar');
1322
+
1323
+ switch (state) {
1324
+ case 'suspending':
1325
+ if (progressStatus) {
1326
+ progressStatus.textContent = 'Terminating research...';
1327
+ progressStatus.className = 'progress-status status-terminating fade-in';
904
1328
  }
905
- });
1329
+ if (progressBar) {
1330
+ progressBar.classList.remove('suspended-status');
1331
+ }
1332
+ if (terminateBtn) {
1333
+ terminateBtn.disabled = true;
1334
+ terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
1335
+ }
1336
+ if (errorMessage) {
1337
+ errorMessage.style.display = 'none';
1338
+ }
1339
+ if (tryAgainBtn) {
1340
+ tryAgainBtn.style.display = 'none';
1341
+ }
1342
+ break;
1343
+
1344
+ case 'suspended':
1345
+ if (progressStatus) {
1346
+ progressStatus.innerHTML = '<i class="fas fa-exclamation-triangle termination-icon"></i> ' + (message || 'Research was suspended');
1347
+ progressStatus.className = 'progress-status status-failed fade-in';
1348
+ }
1349
+ if (progressBar) {
1350
+ progressBar.classList.add('suspended-status');
1351
+ }
1352
+ if (terminateBtn) {
1353
+ terminateBtn.style.display = 'none';
1354
+ }
1355
+ if (errorMessage) {
1356
+ // Hide the error message box completely
1357
+ errorMessage.style.display = 'none';
1358
+ }
1359
+ if (tryAgainBtn) {
1360
+ tryAgainBtn.style.display = 'block';
1361
+ // Update try again button to be more attractive
1362
+ tryAgainBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Try Again';
1363
+ tryAgainBtn.className = 'btn btn-primary fade-in';
1364
+ }
1365
+
1366
+ // Update page title to show suspension
1367
+ document.title = '⚠️ Research Suspended - Local Deep Research';
1368
+
1369
+ break;
1370
+
1371
+ case 'error':
1372
+ if (progressStatus) {
1373
+ progressStatus.innerHTML = '<i class="fas fa-exclamation-circle termination-icon"></i>' + (message || 'Error terminating research');
1374
+ progressStatus.className = 'progress-status status-failed fade-in';
1375
+ }
1376
+ if (progressBar) {
1377
+ progressBar.classList.remove('suspended-status');
1378
+ }
1379
+ if (terminateBtn) {
1380
+ terminateBtn.disabled = false;
1381
+ terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1382
+ }
1383
+ if (errorMessage) {
1384
+ errorMessage.innerHTML = '<i class="fas fa-exclamation-circle termination-icon"></i>' + (message || 'Error terminating research');
1385
+ errorMessage.style.display = 'block';
1386
+ errorMessage.className = 'error-message fade-in';
1387
+ }
1388
+ if (tryAgainBtn) {
1389
+ tryAgainBtn.style.display = 'none';
1390
+ }
1391
+ break;
906
1392
  }
907
1393
  }
908
1394
 
@@ -1211,6 +1697,32 @@ document.addEventListener('DOMContentLoaded', () => {
1211
1697
 
1212
1698
  // Function to navigate to research progress
1213
1699
  function navigateToResearchProgress(research) {
1700
+ // Set the current viewing research ID
1701
+ viewingResearchId = research.id;
1702
+
1703
+ // First check if the research is already terminated/suspended
1704
+ if (research.status === 'suspended' || research.status === 'failed') {
1705
+ // Switch to the progress page with terminated state
1706
+ switchPage('research-progress');
1707
+
1708
+ // Show the query
1709
+ document.getElementById('current-query').textContent = research.query || '';
1710
+
1711
+ // Update UI for terminated state
1712
+ updateTerminationUIState('suspended', `Research was ${research.status}`);
1713
+
1714
+ // Update progress percentage
1715
+ const progress = research.progress || 0;
1716
+ document.getElementById('progress-fill').style.width = `${progress}%`;
1717
+ document.getElementById('progress-percentage').textContent = `${progress}%`;
1718
+
1719
+ // Load logs for this research
1720
+ loadLogsForResearch(research.id);
1721
+
1722
+ // Don't connect to socket or start polling for terminated research
1723
+ return;
1724
+ }
1725
+
1214
1726
  document.getElementById('current-query').textContent = research.query;
1215
1727
  document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
1216
1728
  document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
@@ -1218,6 +1730,9 @@ document.addEventListener('DOMContentLoaded', () => {
1218
1730
  // Navigate to progress page
1219
1731
  switchPage('research-progress');
1220
1732
 
1733
+ // Load logs for this research
1734
+ loadLogsForResearch(research.id);
1735
+
1221
1736
  // Connect to socket for this research
1222
1737
  window.connectToResearchSocket(research.id);
1223
1738
 
@@ -1244,165 +1759,204 @@ document.addEventListener('DOMContentLoaded', () => {
1244
1759
  }
1245
1760
  }
1246
1761
 
1247
- // Function to load research
1762
+ // Update the loadResearch function to handle terminated/suspended research better
1248
1763
  async function loadResearch(researchId) {
1249
1764
  try {
1250
- const response = await fetch(getApiUrl(`/api/research/${researchId}`));
1251
- if (!response.ok) {
1252
- throw new Error('Failed to load research');
1253
- }
1254
-
1255
- const data = await response.json();
1256
-
1257
- if (data.status === 'completed' && data.report_path) {
1258
- // Set the favicon based on the research mode
1259
- setFavicon(data.mode || 'quick');
1260
-
1261
- // Get report content
1262
- const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
1263
- if (!reportResponse.ok) {
1264
- throw new Error('Failed to load report');
1265
- }
1266
-
1267
- const reportData = await reportResponse.json();
1268
-
1269
- if (reportData.status === 'success') {
1270
- // Update UI with report content
1271
- document.getElementById('result-query').textContent = data.query || 'Unknown query';
1272
- document.getElementById('result-mode').textContent = data.mode === 'detailed' ? 'Detailed Report' : 'Quick Summary';
1273
-
1274
- // Format date
1275
- const reportDate = data.completed_at ? formatDate(new Date(data.completed_at)) : 'Unknown';
1276
- document.getElementById('result-date').textContent = reportDate;
1277
-
1278
- // Render markdown content
1279
- const resultsContent = document.getElementById('results-content');
1280
- if (resultsContent) {
1281
- resultsContent.innerHTML = '';
1282
- resultsContent.classList.add('markdown-body');
1765
+ console.log(`Loading research results for research ID: ${researchId}`);
1766
+
1767
+ // Set the current viewing research ID
1768
+ viewingResearchId = researchId;
1769
+
1770
+ // Get research data first to check status
1771
+ fetch(getApiUrl(`/api/research/${researchId}`))
1772
+ .then(response => response.json())
1773
+ .then(researchData => {
1774
+ // Check if research was terminated or failed
1775
+ if (researchData.status === 'suspended' || researchData.status === 'failed') {
1776
+ console.log(`Research ${researchId} was ${researchData.status}, not loading results`);
1283
1777
 
1284
- // Enable syntax highlighting
1285
- const renderedContent = marked.parse(reportData.content);
1286
- resultsContent.innerHTML = renderedContent;
1778
+ // Switch to research progress page if not already there
1779
+ const progressPage = document.getElementById('research-progress');
1780
+ if (!progressPage.classList.contains('active')) {
1781
+ switchPage('research-progress');
1782
+ }
1287
1783
 
1288
- // Apply syntax highlighting
1289
- document.querySelectorAll('pre code').forEach((block) => {
1290
- hljs.highlightElement(block);
1291
- });
1784
+ // Show error message and Try Again button
1785
+ const errorMessage = document.getElementById('error-message');
1786
+ const tryAgainBtn = document.getElementById('try-again-btn');
1787
+
1788
+ if (errorMessage) {
1789
+ errorMessage.textContent = researchData.error || `Research was ${researchData.status}`;
1790
+ errorMessage.style.display = 'block';
1791
+ }
1792
+
1793
+ if (tryAgainBtn) {
1794
+ tryAgainBtn.style.display = 'block';
1795
+ }
1796
+
1797
+ // Update UI elements for this research
1798
+ document.getElementById('current-query').textContent = researchData.query || 'Unknown query';
1799
+
1800
+ // Update progress bar
1801
+ const progressFill = document.getElementById('progress-fill');
1802
+ const progressPercentage = document.getElementById('progress-percentage');
1803
+ if (progressFill) progressFill.style.width = '0%';
1804
+ if (progressPercentage) progressPercentage.textContent = '0%';
1805
+
1806
+ // Load logs for this research
1807
+ loadLogsForResearch(researchId);
1808
+
1809
+ return; // Exit early, no need to load report for terminated research
1292
1810
  }
1293
1811
 
1294
- // Switch to results page
1295
- switchPage('research-results');
1296
- } else {
1297
- throw new Error(reportData.message || 'Failed to load report content');
1298
- }
1299
- } else if (data.status === 'in_progress') {
1300
- // Set favicon based on the research mode
1301
- setFavicon(data.mode || 'quick');
1302
-
1303
- // Redirect to progress page
1304
- navigateToResearchProgress(data);
1305
- } else {
1306
- // Show research details for failed or suspended research
1307
- loadResearchDetails(researchId);
1308
- }
1812
+ // Normal flow for completed research
1813
+ fetch(getApiUrl(`/api/report/${researchId}`))
1814
+ .then(response => response.json())
1815
+ .then(data => {
1816
+ if (data.status === 'error') {
1817
+ throw new Error('Research report not found');
1818
+ }
1819
+
1820
+ if (!data.content) {
1821
+ console.error('No report content found in research data');
1822
+ throw new Error('Report content is empty');
1823
+ }
1824
+
1825
+ // Set the report content and metadata
1826
+ document.getElementById('result-query').textContent = researchData.query || 'Unknown Query';
1827
+ document.getElementById('result-date').textContent = formatDate(
1828
+ researchData.completed_at,
1829
+ researchData.duration_seconds
1830
+ );
1831
+ document.getElementById('result-mode').textContent = formatMode(researchData.mode);
1832
+
1833
+ // Update duration if available (for backward compatibility with existing UI elements)
1834
+ if (researchData.created_at && researchData.completed_at && !researchData.duration_seconds) {
1835
+ // Calculate duration if it's not provided by the API
1836
+ const startDate = new Date(researchData.created_at);
1837
+ const endDate = new Date(researchData.completed_at);
1838
+ const durationSec = Math.floor((endDate - startDate) / 1000);
1839
+
1840
+ // Update the date display with calculated duration
1841
+ document.getElementById('result-date').textContent = formatDate(
1842
+ researchData.completed_at,
1843
+ durationSec
1844
+ );
1845
+
1846
+ // Also update any UI elements that might rely on updateResearchDuration
1847
+ const metadata = {
1848
+ started_at: researchData.created_at,
1849
+ completed_at: researchData.completed_at
1850
+ };
1851
+ updateResearchDuration(metadata);
1852
+ }
1853
+
1854
+ // Render the content
1855
+ const resultsContent = document.getElementById('results-content');
1856
+ resultsContent.innerHTML = ''; // Clear any previous content
1857
+
1858
+ // Convert markdown to HTML
1859
+ const htmlContent = marked.parse(data.content);
1860
+ resultsContent.innerHTML = htmlContent;
1861
+
1862
+ // Apply code highlighting
1863
+ document.querySelectorAll('pre code').forEach((block) => {
1864
+ hljs.highlightBlock(block);
1865
+ });
1866
+
1867
+ // Load logs for this research
1868
+ loadLogsForResearch(researchId);
1869
+
1870
+ // Switch to the results page
1871
+ switchPage('research-results');
1872
+ })
1873
+ .catch(error => {
1874
+ console.error(`Error loading research: ${error}`);
1875
+ });
1876
+ })
1877
+ .catch(error => {
1878
+ console.error(`Error checking research status: ${error}`);
1879
+ });
1309
1880
  } catch (error) {
1310
- console.error('Error loading research:', error);
1311
- alert('Error loading research: ' + error.message);
1312
- // Reset favicon to default on error
1313
- createDynamicFavicon('⚡');
1881
+ console.error(`Error loading research: ${error}`);
1314
1882
  }
1315
1883
  }
1316
1884
 
1317
1885
  // Function to load research details
1318
1886
  async function loadResearchDetails(researchId) {
1319
1887
  try {
1320
- // Navigate to details page
1321
- switchPage('research-details');
1888
+ // Show loading indicators
1889
+ document.getElementById('research-log').innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1322
1890
 
1323
- const researchLog = document.getElementById('research-log');
1324
- researchLog.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1891
+ // Set the current viewing research ID
1892
+ viewingResearchId = researchId;
1325
1893
 
1894
+ // Fetch the research data
1326
1895
  const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
1327
- if (!response.ok) {
1328
- throw new Error('Failed to load research details');
1329
- }
1330
-
1331
1896
  const data = await response.json();
1332
1897
 
1333
- // Set the favicon based on the research mode
1334
- setFavicon(data.mode || 'quick');
1335
-
1336
- // Update UI with research details
1337
- document.getElementById('detail-query').textContent = data.query || 'Unknown query';
1338
- document.getElementById('detail-status').textContent = capitalizeFirstLetter(data.status || 'unknown');
1339
- document.getElementById('detail-mode').textContent = data.mode === 'detailed' ? 'Detailed Report' : 'Quick Summary';
1340
-
1341
- // Update progress indicator
1342
- const progress = data.progress || 0;
1343
- document.getElementById('detail-progress-fill').style.width = `${progress}%`;
1344
- document.getElementById('detail-progress-percentage').textContent = `${progress}%`;
1345
-
1346
- // Add status classes
1347
- document.getElementById('detail-status').className = 'metadata-value status-' + (data.status || 'unknown');
1348
-
1349
- // Clear existing log entries
1350
- researchLog.innerHTML = '';
1351
-
1352
- // Render log entries
1353
- if (data.log && Array.isArray(data.log)) {
1354
- renderLogEntries(data.log);
1898
+ if (data.status === 'success') {
1899
+ // Update the metadata display
1900
+ document.getElementById('detail-query').textContent = data.query || 'Unknown query';
1901
+ document.getElementById('detail-status').textContent = formatStatus(data.status);
1902
+ document.getElementById('detail-mode').textContent = formatMode(data.mode);
1903
+
1904
+ // Update progress percentage
1905
+ const progressFill = document.getElementById('detail-progress-fill');
1906
+ const progressPercentage = document.getElementById('detail-progress-percentage');
1907
+
1908
+ if (progressFill && progressPercentage) {
1909
+ const progress = data.progress || 0;
1910
+ progressFill.style.width = `${progress}%`;
1911
+ progressPercentage.textContent = `${progress}%`;
1912
+ }
1913
+
1914
+ // Update duration if available
1915
+ const metadata = {
1916
+ created_at: data.created_at,
1917
+ completed_at: data.completed_at
1918
+ };
1919
+ updateResearchDuration(metadata);
1920
+
1921
+ // Render the log entries from the response directly
1922
+ if (data.log && Array.isArray(data.log)) {
1923
+ renderResearchLog(data.log, researchId);
1924
+ } else {
1925
+ // Fallback to the dedicated logs endpoint if log data is missing
1926
+ try {
1927
+ const logResponse = await fetch(getApiUrl(`/api/research/${researchId}/logs`));
1928
+ const logData = await logResponse.json();
1929
+
1930
+ if (logData.status === 'success' && logData.logs && Array.isArray(logData.logs)) {
1931
+ renderResearchLog(logData.logs, researchId);
1932
+ } else {
1933
+ document.getElementById('research-log').innerHTML = '<div class="empty-state">No log entries available</div>';
1934
+ }
1935
+ } catch (logError) {
1936
+ console.error('Error fetching logs:', logError);
1937
+ document.getElementById('research-log').innerHTML = '<div class="empty-state">Failed to load log entries</div>';
1938
+ }
1939
+ }
1940
+
1941
+ // Update detail actions based on research status
1942
+ updateDetailActions(data);
1943
+
1944
+ // Load logs for the console panel as well
1945
+ loadLogsForResearch(researchId);
1946
+
1947
+ // Switch to the details page
1948
+ switchPage('research-details');
1355
1949
  } else {
1356
- researchLog.innerHTML = '<div class="empty-message">No log entries available</div>';
1357
- }
1358
-
1359
- // Update actions based on status
1360
- const actionsContainer = document.getElementById('detail-actions');
1361
- actionsContainer.innerHTML = '';
1362
-
1363
- if (data.status === 'completed' && data.report_path) {
1364
- // Add button to view results
1365
- const viewResultsBtn = document.createElement('button');
1366
- viewResultsBtn.className = 'btn btn-primary';
1367
- viewResultsBtn.innerHTML = '<i class="fas fa-eye"></i> View Results';
1368
- viewResultsBtn.addEventListener('click', () => loadResearch(researchId));
1369
- actionsContainer.appendChild(viewResultsBtn);
1370
- } else if (data.status === 'in_progress') {
1371
- // Add button to view progress
1372
- const viewProgressBtn = document.createElement('button');
1373
- viewProgressBtn.className = 'btn btn-primary';
1374
- viewProgressBtn.innerHTML = '<i class="fas fa-spinner"></i> View Progress';
1375
- viewProgressBtn.addEventListener('click', () => navigateToResearchProgress(data));
1376
- actionsContainer.appendChild(viewProgressBtn);
1377
-
1378
- // Add button to terminate research
1379
- const terminateBtn = document.createElement('button');
1380
- terminateBtn.className = 'btn btn-outline terminate-btn';
1381
- terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1382
- terminateBtn.addEventListener('click', () => terminateResearch(researchId));
1383
- actionsContainer.appendChild(terminateBtn);
1950
+ alert('Failed to load research details: ' + (data.message || 'Unknown error'));
1384
1951
  }
1385
-
1386
- // Add delete button for all research items
1387
- const deleteBtn = document.createElement('button');
1388
- deleteBtn.className = 'btn btn-outline delete-btn';
1389
- deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete Research';
1390
- deleteBtn.addEventListener('click', () => {
1391
- if (confirm('Are you sure you want to delete this research? This action cannot be undone.')) {
1392
- deleteResearch(researchId);
1393
- }
1394
- });
1395
- actionsContainer.appendChild(deleteBtn);
1396
1952
  } catch (error) {
1397
1953
  console.error('Error loading research details:', error);
1398
- document.getElementById('research-log').innerHTML = '<div class="error-message">Error loading research details. Please try again later.</div>';
1399
- // Reset favicon to default on error
1400
- createDynamicFavicon('⚡');
1954
+ alert('An error occurred while loading the research details');
1401
1955
  }
1402
1956
  }
1403
1957
 
1404
1958
  // Function to render log entries
1405
- function renderLogEntries(logEntries) {
1959
+ function renderResearchLog(logEntries, researchId) {
1406
1960
  const researchLog = document.getElementById('research-log');
1407
1961
  researchLog.innerHTML = '';
1408
1962
 
@@ -1480,8 +2034,18 @@ document.addEventListener('DOMContentLoaded', () => {
1480
2034
  }
1481
2035
 
1482
2036
  // Connect to socket for updates if this is an in-progress research
1483
- if (research && research.status === 'in_progress') {
1484
- window.connectToResearchSocket(researchId);
2037
+ // Check for research ID and whether it's in progress by checking the database status
2038
+ if (researchId) {
2039
+ // Check research status in the database to determine if we should connect to socket
2040
+ fetch(getApiUrl(`/api/research/${researchId}`))
2041
+ .then(response => response.json())
2042
+ .then(data => {
2043
+ if (data && data.status === 'in_progress') {
2044
+ console.log(`Connecting to socket for research ${researchId} from log view`);
2045
+ window.connectToResearchSocket(researchId);
2046
+ }
2047
+ })
2048
+ .catch(err => console.error(`Error checking research status for socket connection: ${err}`));
1485
2049
  }
1486
2050
  }
1487
2051
 
@@ -1540,31 +2104,6 @@ document.addEventListener('DOMContentLoaded', () => {
1540
2104
  return string.charAt(0).toUpperCase() + string.slice(1);
1541
2105
  }
1542
2106
 
1543
- function formatDate(date) {
1544
- // Ensure we're handling the date properly
1545
- if (!(date instanceof Date) || isNaN(date)) {
1546
- console.warn('Invalid date provided to formatDate:', date);
1547
- return 'Invalid date';
1548
- }
1549
-
1550
- // Get current year to compare with date year
1551
- const currentYear = new Date().getFullYear();
1552
- const dateYear = date.getFullYear();
1553
-
1554
- // Get month name, day, and time
1555
- const month = date.toLocaleString('en-US', { month: 'short' });
1556
- const day = date.getDate();
1557
- const hours = date.getHours().toString().padStart(2, '0');
1558
- const minutes = date.getMinutes().toString().padStart(2, '0');
1559
-
1560
- // Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
1561
- if (dateYear === currentYear) {
1562
- return `${month} ${day}, ${hours}:${minutes}`;
1563
- } else {
1564
- return `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
1565
- }
1566
- }
1567
-
1568
2107
  // Function to update progress UI from current research
1569
2108
  function updateProgressFromCurrentResearch() {
1570
2109
  if (!isResearchInProgress || !currentResearchId) return;
@@ -1587,148 +2126,99 @@ document.addEventListener('DOMContentLoaded', () => {
1587
2126
 
1588
2127
  // Function to update the sidebar navigation based on research status
1589
2128
  function updateNavigationBasedOnResearchStatus() {
1590
- console.log("Updating navigation based on research status");
1591
- console.log("isResearchInProgress:", isResearchInProgress);
1592
- console.log("currentResearchId:", currentResearchId);
2129
+ const isResearchPage = document.getElementById('research-progress').classList.contains('active');
2130
+ const isAnyResearchInProgress = window.currentResearchId !== null && isResearchInProgress;
1593
2131
 
1594
- // Get nav items for each update to ensure we have fresh references
1595
- const navItems = document.querySelectorAll('.sidebar-nav li');
1596
- const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
1597
- // Get all pages
1598
- const pages = document.querySelectorAll('.page');
2132
+ // Get the sidebar nav items and mobile tab items
2133
+ const sidebarNewResearchItem = document.querySelector('.sidebar-nav li[data-page="new-research"]');
2134
+ const sidebarHistoryItem = document.querySelector('.sidebar-nav li[data-page="history"]');
2135
+ const mobileNewResearchItem = document.querySelector('.mobile-tab-bar li[data-page="new-research"]');
2136
+ const mobileHistoryItem = document.querySelector('.mobile-tab-bar li[data-page="history"]');
1599
2137
 
1600
- const newResearchNav = Array.from(navItems).find(item =>
1601
- item.getAttribute('data-page') === 'new-research' ||
1602
- (item.getAttribute('data-original-page') === 'new-research' &&
1603
- item.getAttribute('data-page') === 'research-progress')
1604
- );
2138
+ // Control elements
2139
+ const terminateBtn = document.getElementById('terminate-research-btn');
2140
+ const errorMessage = document.getElementById('error-message');
2141
+ const tryAgainBtn = document.getElementById('try-again-btn');
1605
2142
 
1606
- if (newResearchNav) {
1607
- if (isResearchInProgress) {
1608
- console.log("Research is in progress, updating navigation");
1609
- // Change text to "Research in Progress"
1610
- newResearchNav.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Research in Progress';
1611
-
1612
- // Also update the listener to navigate to progress page
1613
- if (newResearchNav.getAttribute('data-page') !== 'research-progress') {
1614
- newResearchNav.setAttribute('data-original-page', 'new-research');
1615
- newResearchNav.setAttribute('data-page', 'research-progress');
1616
- }
1617
-
1618
- // If on new-research page, redirect to research-progress
1619
- if (document.getElementById('new-research').classList.contains('active')) {
1620
- pages.forEach(page => page.classList.remove('active'));
1621
- document.getElementById('research-progress').classList.add('active');
1622
-
1623
- // Update the research progress page
1624
- updateProgressFromCurrentResearch();
1625
- }
1626
- } else {
1627
- console.log("Research is not in progress, resetting navigation");
1628
- // Reset to "New Research" if there's no active research
1629
- newResearchNav.innerHTML = '<i class="fas fa-search"></i> New Research';
1630
-
1631
- // Reset the listener
1632
- if (newResearchNav.hasAttribute('data-original-page')) {
1633
- newResearchNav.setAttribute('data-page', newResearchNav.getAttribute('data-original-page'));
1634
- newResearchNav.removeAttribute('data-original-page');
1635
- }
1636
-
1637
- // Reset progress UI elements
1638
- const progressFill = document.getElementById('progress-fill');
1639
- const progressPercentage = document.getElementById('progress-percentage');
1640
- const progressStatus = document.getElementById('progress-status');
1641
-
1642
- if (progressFill) progressFill.style.width = '0%';
1643
- if (progressPercentage) progressPercentage.textContent = '0%';
1644
- if (progressStatus) {
1645
- const errorMessage = document.getElementById('error-message');
1646
- if (errorMessage && errorMessage.style.display === 'block') {
1647
- // If there's an error message displayed, show a more informative status
1648
- progressStatus.textContent = 'Research process encountered an error. Please check the error message above and try again.';
1649
- progressStatus.classList.add('status-failed');
1650
- } else {
1651
- progressStatus.textContent = 'Start a new research query when you\'re ready...';
1652
- progressStatus.classList.remove('status-failed');
1653
- }
1654
- }
1655
-
1656
- // If the terminate button is visible, hide it
1657
- const terminateBtn = document.getElementById('terminate-research-btn');
1658
- if (terminateBtn) {
1659
- terminateBtn.style.display = 'none';
1660
- terminateBtn.disabled = false;
1661
- terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1662
- }
1663
-
1664
- // Also hide error message if visible
1665
- const errorMessage = document.getElementById('error-message');
1666
- if (errorMessage) {
1667
- errorMessage.style.display = 'none';
1668
- errorMessage.textContent = '';
1669
- }
1670
-
1671
- // Reset research form submit button
1672
- const startResearchBtn = document.getElementById('start-research-btn');
1673
- if (startResearchBtn) {
1674
- startResearchBtn.disabled = false;
1675
- startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
1676
- }
2143
+ // Log panel should only be visible on research-progress and research-results pages
2144
+ const logPanel = document.querySelector('.collapsible-log-panel');
2145
+
2146
+ // If research is in progress
2147
+ if (isAnyResearchInProgress) {
2148
+ // Disable new research and history navigation while research is in progress
2149
+ if (sidebarNewResearchItem) sidebarNewResearchItem.classList.add('disabled');
2150
+ if (sidebarHistoryItem) sidebarHistoryItem.classList.add('disabled');
2151
+ if (mobileNewResearchItem) mobileNewResearchItem.classList.add('disabled');
2152
+ if (mobileHistoryItem) mobileHistoryItem.classList.add('disabled');
2153
+
2154
+ // If user is not already on the research progress page, switch to it
2155
+ if (!isResearchPage) {
2156
+ switchPage('research-progress');
1677
2157
  }
1678
2158
 
1679
- // Make sure the navigation highlights the correct item
1680
- navItems.forEach(item => {
1681
- if (item === newResearchNav) {
1682
- if (isResearchInProgress) {
1683
- if (document.getElementById('research-progress').classList.contains('active')) {
1684
- item.classList.add('active');
1685
- }
1686
- } else if (document.getElementById('new-research').classList.contains('active')) {
1687
- item.classList.add('active');
1688
- }
1689
- }
1690
- });
1691
- }
1692
-
1693
- // Connect to socket for updates only if research is in progress
1694
- if (isResearchInProgress && currentResearchId) {
1695
- window.connectToResearchSocket(currentResearchId);
2159
+ // Show terminate button and hide error message and try again button
2160
+ if (terminateBtn) terminateBtn.style.display = 'block';
2161
+ if (errorMessage) errorMessage.style.display = 'none';
2162
+ if (tryAgainBtn) tryAgainBtn.style.display = 'none';
1696
2163
  } else {
1697
- // Disconnect socket if no active research
1698
- window.disconnectSocket();
1699
-
1700
- // Also reset the current research ID
1701
- currentResearchId = null;
1702
- window.currentResearchId = null;
2164
+ // Enable navigation when no research is in progress
2165
+ if (sidebarNewResearchItem) sidebarNewResearchItem.classList.remove('disabled');
2166
+ if (sidebarHistoryItem) sidebarHistoryItem.classList.remove('disabled');
2167
+ if (mobileNewResearchItem) mobileNewResearchItem.classList.remove('disabled');
2168
+ if (mobileHistoryItem) mobileHistoryItem.classList.remove('disabled');
2169
+
2170
+ // Hide terminate button when no research is in progress
2171
+ if (terminateBtn) terminateBtn.style.display = 'none';
1703
2172
  }
2173
+
2174
+ console.log('Updated navigation based on research status. In progress:', isAnyResearchInProgress);
1704
2175
  }
1705
2176
 
1706
2177
  // Function to update the research progress page from nav click
1707
2178
  function updateProgressPage() {
1708
- if (!isResearchInProgress || !currentResearchId) {
2179
+ if (!currentResearchId && !window.currentResearchId) {
1709
2180
  return;
1710
2181
  }
1711
2182
 
2183
+ const researchId = currentResearchId || window.currentResearchId;
2184
+
1712
2185
  // Update the progress page
1713
- fetch(getApiUrl(`/api/research/${currentResearchId}`))
2186
+ fetch(getApiUrl(`/api/research/${researchId}`))
1714
2187
  .then(response => response.json())
1715
2188
  .then(data => {
1716
2189
  // Update the query display
1717
- document.getElementById('current-query').textContent = data.query;
1718
-
1719
- // Update the progress bar
1720
- updateProgressUI(data.progress || 0, data.status);
1721
-
1722
- // Connect to socket for live updates
1723
- window.connectToResearchSocket(currentResearchId);
2190
+ document.getElementById('current-query').textContent = data.query;
1724
2191
 
1725
- // Check if we need to show the terminate button
2192
+ // Check status before updating UI
1726
2193
  if (data.status === 'in_progress') {
2194
+ // Update the progress bar
2195
+ updateProgressUI(data.progress || 0, data.status);
2196
+
2197
+ // Check if we need to show the terminate button
1727
2198
  const terminateBtn = document.getElementById('terminate-research-btn');
1728
2199
  if (terminateBtn) {
1729
2200
  terminateBtn.style.display = 'inline-flex';
1730
2201
  terminateBtn.disabled = false;
1731
2202
  }
2203
+
2204
+ // Only connect to socket for in-progress research
2205
+ window.connectToResearchSocket(researchId);
2206
+ // Only start polling for in-progress research
2207
+ if (!pollingInterval) {
2208
+ pollResearchStatus(researchId);
2209
+ }
2210
+ }
2211
+ else if (data.status === 'suspended' || data.status === 'failed') {
2212
+ // Update UI for terminated research
2213
+ updateTerminationUIState('suspended', data.error || `Research was ${data.status}`);
2214
+ // Update progress bar
2215
+ document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
2216
+ document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
2217
+ }
2218
+ else {
2219
+ // Just update progress for completed research
2220
+ document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
2221
+ document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
1732
2222
  }
1733
2223
  })
1734
2224
  .catch(error => {
@@ -2321,22 +2811,11 @@ document.addEventListener('DOMContentLoaded', () => {
2321
2811
  hljs.highlightElement(block);
2322
2812
  });
2323
2813
 
2324
- // Format date
2325
- let dateText = formatDate(new Date(details.completed_at || details.created_at));
2326
- if (details.duration_seconds) {
2327
- let durationText = '';
2328
- const duration = parseInt(details.duration_seconds);
2329
-
2330
- if (duration < 60) {
2331
- durationText = `${duration}s`;
2332
- } else if (duration < 3600) {
2333
- durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
2334
- } else {
2335
- durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
2336
- }
2337
-
2338
- dateText += ` (Duration: ${durationText})`;
2339
- }
2814
+ // Format date with duration
2815
+ let dateText = formatDate(
2816
+ new Date(details.completed_at || details.created_at),
2817
+ details.duration_seconds
2818
+ );
2340
2819
 
2341
2820
  // Set up data for PDF generation
2342
2821
  document.getElementById('result-query').textContent = details.query;
@@ -2371,11 +2850,12 @@ document.addEventListener('DOMContentLoaded', () => {
2371
2850
  // Initialize the terminate button event listener
2372
2851
  const terminateBtn = document.getElementById('terminate-research-btn');
2373
2852
  if (terminateBtn) {
2374
- terminateBtn.addEventListener('click', () => {
2375
- if (confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
2376
- if (currentResearchId) {
2377
- terminateResearch(currentResearchId);
2378
- }
2853
+ terminateBtn.addEventListener('click', function() {
2854
+ if (currentResearchId) {
2855
+ terminateResearch(currentResearchId);
2856
+ } else {
2857
+ console.error('No active research ID found for termination');
2858
+ alert('No active research ID found. Please try again.');
2379
2859
  }
2380
2860
  });
2381
2861
  }
@@ -2518,4 +2998,766 @@ document.addEventListener('DOMContentLoaded', () => {
2518
2998
  const emoji = mode === 'detailed' ? '🔬' : '⚡';
2519
2999
  return createDynamicFavicon(emoji);
2520
3000
  }
3001
+
3002
+ // Global log tracking variables
3003
+ let consoleLogEntries = [];
3004
+ let logCount = 0;
3005
+ let lastLogMessage = null; // Track the last log message to prevent duplicates
3006
+ let lastLogTimestamp = null; // Track the last log timestamp
3007
+ let viewingResearchId = null; // Track which research ID is currently being viewed
3008
+
3009
+ // Function to filter console logs by type
3010
+ function filterConsoleLogs(filterType = 'all') {
3011
+ console.log(`----- FILTER LOGS START (${filterType}) -----`);
3012
+ console.log(`Filtering logs by type: ${filterType}`);
3013
+
3014
+ // Make sure the filter type is lowercase for consistency
3015
+ filterType = filterType.toLowerCase();
3016
+
3017
+ // Get all log entries from the DOM
3018
+ const logEntries = document.querySelectorAll('.console-log-entry');
3019
+ console.log(`Found ${logEntries.length} log entries to filter`);
3020
+
3021
+ // If no entries found, exit early
3022
+ if (logEntries.length === 0) {
3023
+ console.log('No log entries found to filter');
3024
+ console.log(`----- FILTER LOGS END (${filterType}) -----`);
3025
+ return;
3026
+ }
3027
+
3028
+ let visibleCount = 0;
3029
+
3030
+ // Apply filters
3031
+ logEntries.forEach(entry => {
3032
+ // Use the data attribute directly - this comes directly from the database
3033
+ const logType = entry.dataset.logType;
3034
+
3035
+ // Determine visibility based on filter type
3036
+ let shouldShow = false;
3037
+
3038
+ switch (filterType) {
3039
+ case 'all':
3040
+ shouldShow = true;
3041
+ break;
3042
+ case 'info':
3043
+ shouldShow = logType === 'info';
3044
+ break;
3045
+ case 'milestone':
3046
+ case 'milestones': // Handle plural form too
3047
+ shouldShow = logType === 'milestone';
3048
+ break;
3049
+ case 'error':
3050
+ case 'errors': // Handle plural form too
3051
+ shouldShow = logType === 'error';
3052
+ break;
3053
+ default:
3054
+ shouldShow = true; // Default to showing everything
3055
+ console.warn(`Unknown filter type: ${filterType}, showing all logs`);
3056
+ }
3057
+
3058
+ // Set display style based on filter result
3059
+ entry.style.display = shouldShow ? '' : 'none';
3060
+
3061
+ if (shouldShow) {
3062
+ visibleCount++;
3063
+ }
3064
+ });
3065
+
3066
+ console.log(`Filtering complete. Showing ${visibleCount} of ${logEntries.length} logs with filter: ${filterType}`);
3067
+
3068
+ // Show 'no logs' message if all logs are filtered out
3069
+ const consoleContainer = document.getElementById('console-log-container');
3070
+ if (consoleContainer && logEntries.length > 0) {
3071
+ // Remove any existing empty message
3072
+ const existingEmptyMessage = consoleContainer.querySelector('.empty-log-message');
3073
+ if (existingEmptyMessage) {
3074
+ existingEmptyMessage.remove();
3075
+ }
3076
+
3077
+ // Add empty message if needed
3078
+ if (visibleCount === 0) {
3079
+ console.log(`Adding 'no logs' message for filter: ${filterType}`);
3080
+ const newEmptyMessage = document.createElement('div');
3081
+ newEmptyMessage.className = 'empty-log-message';
3082
+ newEmptyMessage.textContent = `No ${filterType} logs to display.`;
3083
+ consoleContainer.appendChild(newEmptyMessage);
3084
+ }
3085
+ }
3086
+
3087
+ console.log(`----- FILTER LOGS END (${filterType}) -----`);
3088
+ }
3089
+
3090
+ // Function to initialize the log panel
3091
+ function initializeLogPanel() {
3092
+ console.log('Initializing log panel');
3093
+
3094
+ // Get DOM elements
3095
+ const logPanelToggle = document.getElementById('log-panel-toggle');
3096
+ const logPanelContent = document.getElementById('log-panel-content');
3097
+ const filterButtons = document.querySelectorAll('.filter-buttons .small-btn');
3098
+
3099
+ // Check if elements exist
3100
+ if (!logPanelToggle || !logPanelContent) {
3101
+ console.error('Log panel elements not found');
3102
+ return;
3103
+ }
3104
+
3105
+ console.log('Log panel elements found:', {
3106
+ toggle: logPanelToggle ? 'Found' : 'Missing',
3107
+ content: logPanelContent ? 'Found' : 'Missing',
3108
+ filterButtons: filterButtons.length > 0 ? `Found ${filterButtons.length} buttons` : 'Missing'
3109
+ });
3110
+
3111
+ // Set up toggle click handler
3112
+ logPanelToggle.addEventListener('click', function() {
3113
+ console.log('Log panel toggle clicked');
3114
+
3115
+ // Toggle collapsed state
3116
+ logPanelContent.classList.toggle('collapsed');
3117
+ logPanelToggle.classList.toggle('collapsed');
3118
+
3119
+ // Update toggle icon
3120
+ const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
3121
+ if (toggleIcon) {
3122
+ if (logPanelToggle.classList.contains('collapsed')) {
3123
+ toggleIcon.className = 'fas fa-chevron-right toggle-icon';
3124
+ } else {
3125
+ toggleIcon.className = 'fas fa-chevron-down toggle-icon';
3126
+
3127
+ // Scroll log container to bottom
3128
+ const consoleContainer = document.getElementById('console-log-container');
3129
+ if (consoleContainer) {
3130
+ setTimeout(() => {
3131
+ consoleContainer.scrollTop = consoleContainer.scrollHeight;
3132
+ }, 10);
3133
+ }
3134
+ }
3135
+ }
3136
+
3137
+ console.log('Log panel is now ' + (logPanelContent.classList.contains('collapsed') ? 'collapsed' : 'expanded'));
3138
+ });
3139
+
3140
+ // Initial state - collapsed
3141
+ logPanelContent.classList.add('collapsed');
3142
+ logPanelToggle.classList.add('collapsed');
3143
+ const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
3144
+ if (toggleIcon) {
3145
+ toggleIcon.className = 'fas fa-chevron-right toggle-icon';
3146
+ }
3147
+
3148
+ // Initial filtering - use a longer delay to ensure DOM is ready
3149
+ setTimeout(() => {
3150
+ console.log('Applying initial log filter: all');
3151
+ filterConsoleLogs('all');
3152
+ }, 300);
3153
+
3154
+ console.log('Log panel initialization completed');
3155
+ }
3156
+
3157
+ // Function to clear all console logs
3158
+ function clearConsoleLogs() {
3159
+ const consoleContainer = document.getElementById('console-log-container');
3160
+ if (!consoleContainer) return;
3161
+
3162
+ console.log('Clearing console logs');
3163
+
3164
+ // Reset the processed messages set to allow new logs after clearing
3165
+ window.processedMessages = new Set();
3166
+
3167
+ // Clear the container
3168
+ consoleContainer.innerHTML = '<div class="empty-log-message">No logs yet. Research logs will appear here as they occur.</div>';
3169
+
3170
+ // Reset the log entries array
3171
+ consoleLogEntries = [];
3172
+
3173
+ // Reset the log count
3174
+ logCount = 0;
3175
+ updateLogIndicator();
3176
+
3177
+ // Reset last message tracking
3178
+ lastLogMessage = null;
3179
+ lastLogTimestamp = null;
3180
+
3181
+ // DO NOT reset viewingResearchId here, as clearing logs doesn't mean
3182
+ // we're changing which research we're viewing
3183
+
3184
+ console.log('Console logs cleared');
3185
+ }
3186
+
3187
+ // Function to determine if a log message is a milestone
3188
+ function isMilestoneLog(message, metadata) {
3189
+ if (!message) return false;
3190
+
3191
+ // Critical milestones - main research flow points
3192
+ const criticalMilestones = [
3193
+ // Research start/end
3194
+ /^Research (started|complete)/i,
3195
+ /^Starting research/i,
3196
+
3197
+ // Iteration markers
3198
+ /^Starting iteration \d+/i,
3199
+ /^Iteration \d+ complete/i,
3200
+
3201
+ // Final completion
3202
+ /^Research completed successfully/i,
3203
+ /^Report generation complete/i,
3204
+ /^Writing research report to file/i
3205
+ ];
3206
+
3207
+ // Check for critical milestones first
3208
+ const isCriticalMilestone = criticalMilestones.some(pattern => pattern.test(message));
3209
+ if (isCriticalMilestone) return true;
3210
+
3211
+ // Check metadata phase for critical phases only
3212
+ const isCriticalPhase = metadata && (
3213
+ metadata.phase === 'init' ||
3214
+ metadata.phase === 'iteration_start' ||
3215
+ metadata.phase === 'iteration_complete' ||
3216
+ metadata.phase === 'complete' ||
3217
+ metadata.phase === 'report_generation' ||
3218
+ metadata.phase === 'report_complete'
3219
+ );
3220
+
3221
+ return isCriticalPhase;
3222
+ }
3223
+
3224
+ // Initialize the log panel when the application starts
3225
+ function initializeAppWithLogs() {
3226
+ // Original initializeApp function
3227
+ const originalInitializeApp = window.initializeApp || initializeApp;
3228
+ window.initializeApp = function() {
3229
+ // Call the original initialization
3230
+ originalInitializeApp();
3231
+
3232
+ // Ensure global log variables are initialized
3233
+ window.consoleLogEntries = [];
3234
+ window.logCount = 0;
3235
+ window.lastLogMessage = null;
3236
+ window.lastLogTimestamp = null;
3237
+
3238
+ // Initialize the log panel with a delay to ensure DOM is ready
3239
+ setTimeout(() => {
3240
+ initializeLogPanel();
3241
+
3242
+ // Add an initial welcome log entry
3243
+ addConsoleLog('Research system initialized and ready', 'milestone');
3244
+
3245
+ console.log('Log panel initialization completed');
3246
+ }, 100);
3247
+ };
3248
+ }
3249
+
3250
+ // Call the initialization function immediately
3251
+ initializeAppWithLogs();
3252
+
3253
+ // Function to get active research ID
3254
+ function getActiveResearchId() {
3255
+ return window.currentResearchId || currentResearchId;
3256
+ }
3257
+
3258
+ // Function to update research duration on results display
3259
+ function updateResearchDuration(metadata) {
3260
+ if (!metadata || !metadata.started_at || !metadata.completed_at) return;
3261
+
3262
+ try {
3263
+ // Parse ISO dates
3264
+ const startDate = new Date(metadata.started_at);
3265
+ const endDate = new Date(metadata.completed_at);
3266
+
3267
+ // Calculate duration in seconds
3268
+ const durationMs = endDate - startDate;
3269
+ const durationSec = Math.floor(durationMs / 1000);
3270
+
3271
+ // Format as minutes and seconds
3272
+ const minutes = Math.floor(durationSec / 60);
3273
+ const seconds = durationSec % 60;
3274
+ const formattedDuration = `${minutes}m ${seconds}s`;
3275
+
3276
+ // Add to results metadata
3277
+ const resultsMetadata = document.querySelector('.results-metadata');
3278
+ if (resultsMetadata) {
3279
+ // Check if duration element already exists
3280
+ let durationEl = resultsMetadata.querySelector('.metadata-item.duration');
3281
+
3282
+ if (!durationEl) {
3283
+ // Create new duration element
3284
+ durationEl = document.createElement('div');
3285
+ durationEl.className = 'metadata-item duration';
3286
+
3287
+ const labelEl = document.createElement('span');
3288
+ labelEl.className = 'metadata-label';
3289
+ labelEl.textContent = 'Duration:';
3290
+
3291
+ const valueEl = document.createElement('span');
3292
+ valueEl.className = 'metadata-value';
3293
+ valueEl.id = 'result-duration';
3294
+
3295
+ durationEl.appendChild(labelEl);
3296
+ durationEl.appendChild(valueEl);
3297
+
3298
+ // Insert after "Generated" metadata
3299
+ const generatedEl = resultsMetadata.querySelector('.metadata-item:nth-child(2)');
3300
+ if (generatedEl) {
3301
+ resultsMetadata.insertBefore(durationEl, generatedEl.nextSibling);
3302
+ } else {
3303
+ resultsMetadata.appendChild(durationEl);
3304
+ }
3305
+ }
3306
+
3307
+ // Update duration value
3308
+ const durationValueEl = resultsMetadata.querySelector('#result-duration');
3309
+ if (durationValueEl) {
3310
+ durationValueEl.textContent = formattedDuration;
3311
+ }
3312
+ }
3313
+
3314
+ // Also add to research details metadata
3315
+ const detailsMetadata = document.querySelector('.research-metadata');
3316
+ if (detailsMetadata) {
3317
+ // Check if duration element already exists
3318
+ let durationEl = null;
3319
+
3320
+ // Use querySelectorAll and iterate to find the element with "Duration" label
3321
+ const metadataItems = detailsMetadata.querySelectorAll('.metadata-item');
3322
+ for (let i = 0; i < metadataItems.length; i++) {
3323
+ const labelEl = metadataItems[i].querySelector('.metadata-label');
3324
+ if (labelEl && labelEl.textContent.includes('Duration')) {
3325
+ durationEl = metadataItems[i];
3326
+ break;
3327
+ }
3328
+ }
3329
+
3330
+ if (!durationEl) {
3331
+ // Create new duration element
3332
+ durationEl = document.createElement('div');
3333
+ durationEl.className = 'metadata-item';
3334
+
3335
+ const labelEl = document.createElement('span');
3336
+ labelEl.className = 'metadata-label';
3337
+ labelEl.textContent = 'Duration:';
3338
+
3339
+ const valueEl = document.createElement('span');
3340
+ valueEl.className = 'metadata-value';
3341
+ valueEl.id = 'detail-duration';
3342
+
3343
+ durationEl.appendChild(labelEl);
3344
+ durationEl.appendChild(valueEl);
3345
+
3346
+ // Insert after mode metadata
3347
+ const modeEl = detailsMetadata.querySelector('.metadata-item:nth-child(3)');
3348
+ if (modeEl) {
3349
+ detailsMetadata.insertBefore(durationEl, modeEl.nextSibling);
3350
+ } else {
3351
+ detailsMetadata.appendChild(durationEl);
3352
+ }
3353
+ }
3354
+
3355
+ // Update duration value
3356
+ const durationValueEl = durationEl.querySelector('.metadata-value');
3357
+ if (durationValueEl) {
3358
+ durationValueEl.textContent = formattedDuration;
3359
+ }
3360
+ }
3361
+
3362
+ console.log(`Research duration: ${formattedDuration}`);
3363
+ } catch (error) {
3364
+ console.error('Error calculating research duration:', error);
3365
+ }
3366
+ }
3367
+
3368
+ // Function to add a log entry to the console with reduced deduplication time window
3369
+ function addConsoleLog(message, type = 'info', metadata = null, forResearchId = null) {
3370
+ // Skip if identical to the last message to prevent duplication
3371
+ const currentTime = new Date().getTime();
3372
+ if (message === lastLogMessage && lastLogTimestamp && currentTime - lastLogTimestamp < 300) {
3373
+ // Skip duplicate message if it's within 300ms of the last one (reduced from 2000ms)
3374
+ return null;
3375
+ }
3376
+
3377
+ // Skip if we're viewing a specific research and this log is for a different research
3378
+ if (viewingResearchId && forResearchId && viewingResearchId !== forResearchId) {
3379
+ console.log(`Skipping log for research ${forResearchId} because viewing ${viewingResearchId}`);
3380
+ return null;
3381
+ }
3382
+
3383
+ // Update tracking variables
3384
+ lastLogMessage = message;
3385
+ lastLogTimestamp = currentTime;
3386
+
3387
+ // Use the direct log addition function
3388
+ return addConsoleLogDirect(message, type, metadata);
3389
+ }
3390
+
3391
+ // Function to update the log indicator count
3392
+ function updateLogIndicator() {
3393
+ const indicator = document.getElementById('log-indicator');
3394
+ if (indicator) {
3395
+ indicator.textContent = logCount;
3396
+ }
3397
+ }
3398
+
3399
+ // New function to update log panel visibility based on current page
3400
+ function updateLogPanelVisibility(pageId) {
3401
+ console.log('Updating log panel visibility for page:', pageId);
3402
+
3403
+ // Only show log panel on research progress and results pages
3404
+ if (pageId === 'research-progress' || pageId === 'research-results') {
3405
+ console.log('Showing log panel');
3406
+ // Let CSS handle the display
3407
+ } else {
3408
+ console.log('Hiding log panel');
3409
+ // Let CSS handle the display
3410
+ }
3411
+ }
3412
+
3413
+ // Function to update the navigation UI based on active page
3414
+ function updateNavigationUI(pageId) {
3415
+ // Update sidebar navigation
3416
+ const sidebarNavItems = document.querySelectorAll('.sidebar-nav li');
3417
+ sidebarNavItems.forEach(item => {
3418
+ if (item.getAttribute('data-page') === pageId) {
3419
+ item.classList.add('active');
3420
+ } else {
3421
+ item.classList.remove('active');
3422
+ }
3423
+ });
3424
+
3425
+ // Update mobile tab bar
3426
+ const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
3427
+ mobileNavItems.forEach(item => {
3428
+ if (item.getAttribute('data-page') === pageId) {
3429
+ item.classList.add('active');
3430
+ } else {
3431
+ item.classList.remove('active');
3432
+ }
3433
+ });
3434
+ }
3435
+
3436
+ // Function to check research status before polling
3437
+ async function checkResearchStatusBeforePolling(researchId) {
3438
+ try {
3439
+ // Get the current status from the server
3440
+ const response = await fetch(getApiUrl(`/api/research/${researchId}`));
3441
+ const data = await response.json();
3442
+
3443
+ // If research is in_progress, start polling normally
3444
+ if (data.status === 'in_progress') {
3445
+ console.log(`Research ${researchId} is in progress, starting polling`);
3446
+ pollResearchStatus(researchId);
3447
+ window.connectToResearchSocket(researchId);
3448
+ return true;
3449
+ }
3450
+ // If terminated or complete, update UI but don't start polling
3451
+ else if (data.status === 'suspended' || data.status === 'failed') {
3452
+ console.log(`Research ${researchId} is ${data.status}, not starting polling`);
3453
+ updateTerminationUIState('suspended', `Research was ${data.status}`);
3454
+ return false;
3455
+ }
3456
+ // If completed, don't start polling
3457
+ else if (data.status === 'completed') {
3458
+ console.log(`Research ${researchId} is completed, not starting polling`);
3459
+ return false;
3460
+ }
3461
+ } catch (error) {
3462
+ console.error(`Error checking research status: ${error}`);
3463
+ return false;
3464
+ }
3465
+
3466
+ return false;
3467
+ }
3468
+
3469
+ // Function to reset research state before starting a new research
3470
+ function resetResearchState() {
3471
+ // Clean up any existing research resources
3472
+ window.cleanupResearchResources();
3473
+
3474
+ // Clear any previous polling intervals
3475
+ clearPollingInterval();
3476
+
3477
+ // Reset research flags
3478
+ isResearchInProgress = false;
3479
+ currentResearchId = null;
3480
+ window.currentResearchId = null;
3481
+
3482
+ // Clear console logs
3483
+ clearConsoleLogs();
3484
+
3485
+ // Reset any UI elements
3486
+ const errorMessage = document.getElementById('error-message');
3487
+ if (errorMessage) {
3488
+ errorMessage.style.display = 'none';
3489
+ errorMessage.textContent = '';
3490
+ }
3491
+
3492
+ // Hide the try again button if visible
3493
+ const tryAgainBtn = document.getElementById('try-again-btn');
3494
+ if (tryAgainBtn) {
3495
+ tryAgainBtn.style.display = 'none';
3496
+ }
3497
+
3498
+ // Reset progress bar
3499
+ const progressFill = document.getElementById('progress-fill');
3500
+ if (progressFill) {
3501
+ progressFill.style.width = '0%';
3502
+ }
3503
+
3504
+ // Reset progress percentage
3505
+ const progressPercentage = document.getElementById('progress-percentage');
3506
+ if (progressPercentage) {
3507
+ progressPercentage.textContent = '0%';
3508
+ }
3509
+
3510
+ // Reset progress status
3511
+ const progressStatus = document.getElementById('progress-status');
3512
+ if (progressStatus) {
3513
+ progressStatus.textContent = 'Initializing research process...';
3514
+ progressStatus.className = 'progress-status';
3515
+ }
3516
+
3517
+ // Reset the Start Research button
3518
+ resetStartResearchButton();
3519
+
3520
+ // Add initial log entry
3521
+ addConsoleLog('Preparing to start new research', 'info');
3522
+ }
3523
+
3524
+ // Function to load research logs into the console log panel
3525
+ function loadLogsForResearch(researchId) {
3526
+ console.log(`Loading logs for research ${researchId}`);
3527
+
3528
+ // Clear existing logs
3529
+ clearConsoleLogs();
3530
+
3531
+ // Set the current viewing research ID
3532
+ viewingResearchId = researchId;
3533
+
3534
+ // Fetch logs from the server for this research
3535
+ fetch(getApiUrl(`/api/research/${researchId}/logs`))
3536
+ .then(response => response.json())
3537
+ .then(data => {
3538
+ if (data.status === 'success' && Array.isArray(data.logs) && data.logs.length > 0) {
3539
+ console.log(`Loaded ${data.logs.length} logs for research ${researchId}`);
3540
+
3541
+ // Sort logs by timestamp if needed
3542
+ data.logs.sort((a, b) => {
3543
+ const timeA = new Date(a.time);
3544
+ const timeB = new Date(b.time);
3545
+ return timeA - timeB;
3546
+ });
3547
+
3548
+ // Add logs to the console
3549
+ data.logs.forEach(log => {
3550
+ let logType = 'info';
3551
+
3552
+ // Use the type directly from the database if available
3553
+ if (log.type) {
3554
+ logType = log.type;
3555
+ } else if (isMilestoneLog(log.message, log.metadata)) {
3556
+ logType = 'milestone';
3557
+ } else if (log.metadata && log.metadata.phase === 'error') {
3558
+ logType = 'error';
3559
+ }
3560
+
3561
+ // Add to console without triggering the duplicate detection
3562
+ // by directly creating the log entry
3563
+ addConsoleLogDirect(log.message, logType, log.metadata, log.time);
3564
+ });
3565
+
3566
+ // Apply initial filter (all)
3567
+ filterConsoleLogs('all');
3568
+
3569
+ // Make sure the "All" button is selected
3570
+ const allButton = document.querySelector('.filter-buttons .small-btn');
3571
+ if (allButton) {
3572
+ // Remove selected class from all buttons
3573
+ document.querySelectorAll('.filter-buttons .small-btn').forEach(btn => {
3574
+ btn.classList.remove('selected');
3575
+ });
3576
+ // Add selected class to the All button
3577
+ allButton.classList.add('selected');
3578
+ }
3579
+ } else {
3580
+ console.log(`No logs found for research ${researchId}`);
3581
+ // Add a message indicating no logs are available
3582
+ const consoleContainer = document.getElementById('console-log-container');
3583
+ if (consoleContainer) {
3584
+ consoleContainer.innerHTML = '<div class="empty-log-message">No logs available for this research.</div>';
3585
+ }
3586
+ }
3587
+ })
3588
+ .catch(error => {
3589
+ console.error(`Error loading logs for research ${researchId}:`, error);
3590
+ // Show error message in console log
3591
+ const consoleContainer = document.getElementById('console-log-container');
3592
+ if (consoleContainer) {
3593
+ consoleContainer.innerHTML = '<div class="empty-log-message error-message">Failed to load logs. Please try again.</div>';
3594
+ }
3595
+ });
3596
+ }
3597
+
3598
+ // New direct log addition function that bypasses duplicate detection
3599
+ function addConsoleLogDirect(message, type = 'info', metadata = null, timestamp = null) {
3600
+ // Get DOM elements
3601
+ const consoleContainer = document.getElementById('console-log-container');
3602
+ const template = document.getElementById('console-log-entry-template');
3603
+
3604
+ if (!consoleContainer || !template) {
3605
+ console.error('Console log container or template not found');
3606
+ return null;
3607
+ }
3608
+
3609
+ // Clear the empty message if it's the first log
3610
+ const emptyMessage = consoleContainer.querySelector('.empty-log-message');
3611
+ if (emptyMessage) {
3612
+ consoleContainer.removeChild(emptyMessage);
3613
+ }
3614
+
3615
+ // Create a new log entry from the template
3616
+ const clone = document.importNode(template.content, true);
3617
+ const logEntry = clone.querySelector('.console-log-entry');
3618
+
3619
+ // Make sure type is valid and standardized
3620
+ let validType = 'info';
3621
+ if (['info', 'milestone', 'error'].includes(type.toLowerCase())) {
3622
+ validType = type.toLowerCase();
3623
+ }
3624
+
3625
+ // Add appropriate class based on type
3626
+ logEntry.classList.add(`log-${validType}`);
3627
+
3628
+ // IMPORTANT: Store the log type directly as a data attribute
3629
+ logEntry.dataset.logType = validType;
3630
+
3631
+ // Set the timestamp
3632
+ const actualTimestamp = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
3633
+ const timestampEl = logEntry.querySelector('.log-timestamp');
3634
+ if (timestampEl) timestampEl.textContent = actualTimestamp;
3635
+
3636
+ // Set the badge text based on type
3637
+ const badgeEl = logEntry.querySelector('.log-badge');
3638
+ if (badgeEl) {
3639
+ badgeEl.textContent = validType.toUpperCase();
3640
+ }
3641
+
3642
+ // Process message to add search engine info if it's a search query
3643
+ let displayMessage = message;
3644
+
3645
+ // Check if this is a search query message
3646
+ if (message && typeof message === 'string' && message.startsWith('Searching for:')) {
3647
+ // Determine search engine - add SearXNG by default since that's what we see in logs
3648
+ let searchEngine = 'SearXNG';
3649
+
3650
+ // Check metadata if available for any search engine info
3651
+ if (metadata) {
3652
+ if (metadata.engine) searchEngine = metadata.engine;
3653
+ else if (metadata.search_engine) searchEngine = metadata.search_engine;
3654
+ else if (metadata.source) searchEngine = metadata.source;
3655
+ else if (metadata.phase && metadata.phase.includes('searxng')) searchEngine = 'SearXNG';
3656
+ else if (metadata.phase && metadata.phase.includes('google')) searchEngine = 'Google';
3657
+ else if (metadata.phase && metadata.phase.includes('bing')) searchEngine = 'Bing';
3658
+ else if (metadata.phase && metadata.phase.includes('duckduckgo')) searchEngine = 'DuckDuckGo';
3659
+ }
3660
+
3661
+ // Append search engine info to message
3662
+ displayMessage = `${message} [Engine: ${searchEngine}]`;
3663
+ }
3664
+
3665
+ // Set the message
3666
+ const messageEl = logEntry.querySelector('.log-message');
3667
+ if (messageEl) messageEl.textContent = displayMessage;
3668
+
3669
+ // Add the log entry to the container
3670
+ consoleContainer.appendChild(logEntry);
3671
+
3672
+ // Store the log entry for filtering
3673
+ consoleLogEntries.push({
3674
+ element: logEntry,
3675
+ type: validType,
3676
+ message: displayMessage,
3677
+ timestamp: actualTimestamp,
3678
+ researchId: viewingResearchId
3679
+ });
3680
+
3681
+ // Update log count
3682
+ logCount++;
3683
+ updateLogIndicator();
3684
+
3685
+ // Use setTimeout to ensure DOM updates before scrolling
3686
+ setTimeout(() => {
3687
+ // Scroll to the bottom
3688
+ consoleContainer.scrollTop = consoleContainer.scrollHeight;
3689
+ }, 0);
3690
+
3691
+ return logEntry;
3692
+ }
3693
+
3694
+ // Add a global function to filter logs directly from HTML
3695
+ window.filterLogsByType = function(filterType) {
3696
+ console.log('Direct filterLogsByType called with:', filterType);
3697
+ if (filterType && typeof filterConsoleLogs === 'function') {
3698
+ // Update the button styling for the selected filter
3699
+ const filterButtons = document.querySelectorAll('.filter-buttons .small-btn');
3700
+ if (filterButtons.length > 0) {
3701
+ // Remove 'selected' class from all buttons
3702
+ filterButtons.forEach(btn => {
3703
+ btn.classList.remove('selected');
3704
+ });
3705
+
3706
+ // Add 'selected' class to the clicked button
3707
+ const selectedButton = Array.from(filterButtons).find(
3708
+ btn => btn.textContent.toLowerCase().includes(filterType) ||
3709
+ (filterType === 'all' && btn.textContent.toLowerCase() === 'all')
3710
+ );
3711
+
3712
+ if (selectedButton) {
3713
+ selectedButton.classList.add('selected');
3714
+ }
3715
+ }
3716
+
3717
+ // Apply the filter
3718
+ filterConsoleLogs(filterType);
3719
+ } else {
3720
+ console.error('Unable to filter logs - filterConsoleLogs function not available');
3721
+ }
3722
+ };
3723
+
3724
+ // Function to check if there are any logs available
3725
+ window.checkIfLogsAvailable = function() {
3726
+ const logEntries = document.querySelectorAll('.console-log-entry');
3727
+ if (logEntries.length === 0) {
3728
+ console.log('No logs available to filter');
3729
+ return false;
3730
+ }
3731
+ return true;
3732
+ };
3733
+
3734
+ // Add direct log panel toggle handler as backup
3735
+ const logPanelToggle = document.getElementById('log-panel-toggle');
3736
+ const logPanelContent = document.getElementById('log-panel-content');
3737
+
3738
+ if (logPanelToggle && logPanelContent) {
3739
+ console.log('Adding direct DOM event listener to log panel toggle');
3740
+
3741
+ logPanelToggle.addEventListener('click', function(event) {
3742
+ console.log('Log panel toggle clicked (direct handler)');
3743
+ event.preventDefault();
3744
+ event.stopPropagation();
3745
+
3746
+ // Toggle collapsed state
3747
+ logPanelContent.classList.toggle('collapsed');
3748
+ logPanelToggle.classList.toggle('collapsed');
3749
+
3750
+ // Update toggle icon
3751
+ const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
3752
+ if (toggleIcon) {
3753
+ if (logPanelToggle.classList.contains('collapsed')) {
3754
+ toggleIcon.className = 'fas fa-chevron-right toggle-icon';
3755
+ } else {
3756
+ toggleIcon.className = 'fas fa-chevron-down toggle-icon';
3757
+ }
3758
+ }
3759
+
3760
+ return false;
3761
+ });
3762
+ }
2521
3763
  });