local-deep-research 0.1.0__py3-none-any.whl → 0.1.12__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.
@@ -17,6 +17,30 @@ document.addEventListener('DOMContentLoaded', () => {
17
17
  let errorSound = null;
18
18
  let notificationsEnabled = true;
19
19
 
20
+ // Add function to cleanup research resources globally
21
+ window.cleanupResearchResources = function() {
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
34
+ if (pollingInterval) {
35
+ clearInterval(pollingInterval);
36
+ pollingInterval = null;
37
+ console.log('Cleared polling interval during cleanup');
38
+ }
39
+
40
+ // Reset research state flags
41
+ isResearchInProgress = false;
42
+ };
43
+
20
44
  // Initialize notification sounds
21
45
  function initializeSounds() {
22
46
  successSound = new Audio('/research/static/sounds/success.mp3');
@@ -47,25 +71,46 @@ document.addEventListener('DOMContentLoaded', () => {
47
71
 
48
72
  // Initialize socket only when needed with a timeout for safety
49
73
  function initializeSocket() {
50
- if (socket) return socket; // Return existing socket if already initialized
74
+ if (socket) {
75
+ // If we already have a socket but it's disconnected, reconnect
76
+ if (!socketConnected) {
77
+ try {
78
+ console.log('Socket disconnected, reconnecting...');
79
+ socket.connect();
80
+ } catch (e) {
81
+ console.error('Error reconnecting socket:', e);
82
+ // Create a new socket
83
+ socket = null;
84
+ return initializeSocket();
85
+ }
86
+ }
87
+ return socket;
88
+ }
51
89
 
52
90
  console.log('Initializing socket connection...');
53
91
  // Create new socket connection with optimized settings for threading mode
54
92
  socket = io({
55
93
  path: '/research/socket.io',
56
- transports: ['websocket', 'polling'],
57
- reconnection: true,
58
- reconnectionAttempts: 3,
59
- reconnectionDelay: 1000,
60
- timeout: 5000,
94
+ transports: ['polling', 'websocket'], // Try polling first, then websocket
95
+ reconnection: true,
96
+ reconnectionAttempts: 10,
97
+ reconnectionDelay: 1000,
98
+ timeout: 25000, // Increase timeout further
61
99
  autoConnect: true,
62
- forceNew: true
100
+ forceNew: true,
101
+ upgrade: false // Disable automatic transport upgrade for more stability
63
102
  });
64
103
 
65
104
  // Add event handlers
66
- socket.on('connect', () => {
105
+ socket.on('connect', () => {
67
106
  console.log('Socket connected');
68
107
  socketConnected = true;
108
+
109
+ // If we're reconnecting and have a current research, resubscribe
110
+ if (currentResearchId) {
111
+ console.log(`Reconnected, resubscribing to research ${currentResearchId}`);
112
+ socket.emit('subscribe_to_research', { research_id: currentResearchId });
113
+ }
69
114
  });
70
115
 
71
116
  socket.on('disconnect', () => {
@@ -78,18 +123,52 @@ document.addEventListener('DOMContentLoaded', () => {
78
123
  socketConnected = false;
79
124
  });
80
125
 
126
+ socket.on('error', (error) => {
127
+ console.error('Socket error:', error);
128
+ });
129
+
81
130
  // Set a timeout to detect hanging connections
82
- setTimeout(() => {
131
+ let connectionTimeoutId = setTimeout(() => {
83
132
  if (!socketConnected) {
84
133
  console.log('Socket connection timeout - forcing reconnect');
85
134
  try {
86
- socket.disconnect();
87
- socket.connect();
135
+ if (socket) {
136
+ // First try to disconnect cleanly
137
+ try {
138
+ socket.disconnect();
139
+ } catch (disconnectErr) {
140
+ console.warn('Error disconnecting socket during timeout:', disconnectErr);
141
+ }
142
+
143
+ // Then try to reconnect
144
+ try {
145
+ socket.connect();
146
+ } catch (connectErr) {
147
+ console.warn('Error reconnecting socket during timeout:', connectErr);
148
+ // Create a new socket only if connect fails
149
+ socket = null;
150
+ socket = initializeSocket();
151
+ }
152
+ } else {
153
+ // Socket is already null, just create a new one
154
+ socket = initializeSocket();
155
+ }
88
156
  } catch (e) {
89
157
  console.error('Error during forced reconnect:', e);
158
+ // Force create a new socket
159
+ socket = null;
160
+ socket = initializeSocket();
90
161
  }
91
162
  }
92
- }, 5000);
163
+ }, 15000); // Longer timeout before forcing reconnection
164
+
165
+ // Clean up timeout if socket connects
166
+ socket.on('connect', () => {
167
+ if (connectionTimeoutId) {
168
+ clearTimeout(connectionTimeoutId);
169
+ connectionTimeoutId = null;
170
+ }
171
+ });
93
172
 
94
173
  return socket;
95
174
  }
@@ -99,104 +178,368 @@ document.addEventListener('DOMContentLoaded', () => {
99
178
  try {
100
179
  if (socket) {
101
180
  console.log('Manually disconnecting socket');
102
- socket.removeAllListeners();
103
- socket.disconnect();
181
+ try {
182
+ // First remove all listeners
183
+ socket.removeAllListeners();
184
+ } catch (listenerErr) {
185
+ console.warn('Error removing socket listeners:', listenerErr);
186
+ }
187
+
188
+ try {
189
+ // Then disconnect
190
+ socket.disconnect();
191
+ } catch (disconnectErr) {
192
+ console.warn('Error during socket disconnect:', disconnectErr);
193
+ }
194
+
195
+ // Always set to null to allow garbage collection
104
196
  socket = null;
105
197
  socketConnected = false;
106
198
  }
107
199
  } catch (e) {
108
200
  console.error('Error disconnecting socket:', e);
201
+ // Ensure socket is nullified even if errors occur
202
+ socket = null;
203
+ socketConnected = false;
109
204
  }
110
205
  };
111
206
 
112
- // Helper function to connect to research updates
113
- 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
+
114
214
  try {
115
- // Initialize socket if needed
116
- if (!socket) {
117
- socket = initializeSocket();
118
- }
215
+ // Check if research is terminated/suspended before connecting
216
+ const response = await fetch(getApiUrl(`/api/research/${researchId}`));
217
+ const data = await response.json();
119
218
 
120
- // Subscribe to research updates
121
- socket.emit('subscribe_to_research', { research_id: researchId });
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
+ }
122
232
 
123
- // Set up event listener for research progress
124
- const progressEventName = `research_progress_${researchId}`;
233
+ console.log(`Connecting to socket for research ${researchId} (status: ${data.status})`);
125
234
 
126
- // Remove existing listeners to prevent duplicates
127
- socket.off(progressEventName);
235
+ // Initialize socket if it doesn't exist
236
+ if (!socket) {
237
+ initializeSocket();
238
+ }
128
239
 
129
- // Add new listener
130
- socket.on(progressEventName, (data) => {
131
- console.log('Received research progress update:', data);
132
- updateProgressUI(data.progress, data.status, data.message);
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;
133
249
 
134
- // If research is complete, show the completion buttons
135
- if (data.status === 'completed' || data.status === 'terminated' || data.status === 'failed' || data.status === 'suspended') {
136
- console.log(`Socket received research final state: ${data.status}`);
137
-
138
- // Clear polling interval if it exists
139
- if (pollingInterval) {
140
- console.log('Clearing polling interval from socket event');
141
- clearInterval(pollingInterval);
142
- pollingInterval = null;
143
- }
144
-
145
- // Update navigation state
146
- if (data.status === 'completed') {
147
- isResearchInProgress = false;
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');
148
260
  }
149
-
150
- // Update UI for completion
151
- if (data.status === 'completed') {
152
- console.log('Research completed via socket, loading results automatically');
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';
290
+
291
+ // Check if we have a date string instead of a Date object
292
+ if (typeof date === 'string') {
293
+ try {
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+-]/);
153
303
 
154
- // Hide terminate button
155
- const terminateBtn = document.getElementById('terminate-research-btn');
156
- if (terminateBtn) {
157
- terminateBtn.style.display = 'none';
304
+ if (tzIndex !== -1) {
305
+ timezone = microsecondPart.substring(tzIndex);
158
306
  }
159
307
 
160
- // Auto-load the results
161
- loadResearch(researchId);
162
- } else if (data.status === 'failed' || data.status === 'suspended') {
163
- console.log(`Showing error message for status: ${data.status} from socket event`);
164
- const errorMessage = document.getElementById('error-message');
165
- if (errorMessage) {
166
- errorMessage.style.display = 'block';
167
- errorMessage.textContent = data.status === 'failed' ?
168
- (data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
169
- 'Research was suspended';
170
- } else {
171
- console.error('error-message element not found in socket handler');
172
- }
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);
173
316
  }
317
+ } else {
318
+ date = new Date(date);
319
+ }
320
+ } catch (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);
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);
415
+
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;
437
+
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
499
+ playNotificationSound('success');
500
+
501
+ // Store the completed research ID for navigation
502
+ const completedResearchId = researchId;
174
503
 
504
+ // Reset current research ID
505
+ currentResearchId = null;
506
+ window.currentResearchId = null;
507
+
508
+ // Reset lastMeaningfulStatusMessage for next research
509
+ window.lastMeaningfulStatusMessage = '';
510
+
511
+ // Update navigation state
175
512
  updateNavigationBasedOnResearchStatus();
176
513
 
177
- // Play notification sounds based on status
178
- if (data.status === 'completed') {
179
- console.log('Playing success notification sound from socket event');
180
- playNotificationSound('success');
181
- } else if (data.status === 'failed') {
182
- console.log('Playing error notification sound from socket event');
183
- playNotificationSound('error');
184
- }
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');
522
+
523
+ // Use the updateTerminationUIState function for consistency
524
+ updateTerminationUIState('suspended', data.error || `Research was ${data.status}`);
525
+
526
+ // Reset lastMeaningfulStatusMessage for next research
527
+ window.lastMeaningfulStatusMessage = '';
528
+
529
+ // Reset current research ID
530
+ currentResearchId = null;
531
+ window.currentResearchId = null;
185
532
 
186
- // Force the UI to update with a manual trigger
187
- document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
533
+ // Update navigation - important for correctly showing/hiding various elements
534
+ // based on the current research state
535
+ updateNavigationBasedOnResearchStatus();
188
536
  }
189
537
 
190
- // Update the detailed log if on details page
191
- if (data.log_entry && document.getElementById('research-log')) {
192
- updateDetailLogEntry(data.log_entry);
538
+ // Refresh the history list to show the completed research
539
+ if (document.getElementById('history').classList.contains('active')) {
540
+ loadResearchHistory();
193
541
  }
194
- });
195
-
196
- return true;
197
- } catch (e) {
198
- console.error('Error connecting to research socket:', e);
199
- return false;
542
+ }
200
543
  }
201
544
  };
202
545
 
@@ -210,22 +553,62 @@ document.addEventListener('DOMContentLoaded', () => {
210
553
  const activeResearch = history.find(item => item.status === 'in_progress');
211
554
 
212
555
  if (activeResearch) {
213
- isResearchInProgress = true;
214
- currentResearchId = activeResearch.id;
215
- window.currentResearchId = currentResearchId;
216
-
217
- // Check if we're on the new research page and redirect to progress
218
- const currentPage = document.querySelector('.page.active');
219
-
220
- if (currentPage && currentPage.id === 'new-research') {
221
- // Navigate to progress page
222
- switchPage('research-progress');
556
+ // Verify the research is truly active by checking its details
557
+ try {
558
+ const detailsResponse = await fetch(getApiUrl(`/api/research/${activeResearch.id}`));
559
+ const details = await detailsResponse.json();
560
+
561
+ // If status is not in_progress in the details, it's stale
562
+ if (details.status !== 'in_progress') {
563
+ console.log(`Research ${activeResearch.id} is stale (status: ${details.status}), ignoring`);
564
+ return;
565
+ }
566
+
567
+ // Check when the research was started - if it's been more than 1 hour, it might be stale
568
+ if (details.created_at) {
569
+ const startTime = new Date(details.created_at);
570
+ const currentTime = new Date();
571
+ const hoursSinceStart = (currentTime - startTime) / (1000 * 60 * 60);
572
+
573
+ if (hoursSinceStart > 1) {
574
+ console.log(`Research ${activeResearch.id} has been running for ${hoursSinceStart.toFixed(2)} hours, which is unusually long. Checking for activity...`);
575
+
576
+ // Check if there has been log activity in the last 10 minutes
577
+ let recentActivity = false;
578
+ if (details.log && Array.isArray(details.log) && details.log.length > 0) {
579
+ const lastLogTime = new Date(details.log[details.log.length - 1].time);
580
+ const minutesSinceLastLog = (currentTime - lastLogTime) / (1000 * 60);
581
+
582
+ if (minutesSinceLastLog < 10) {
583
+ recentActivity = true;
584
+ } else {
585
+ console.log(`No recent activity for ${minutesSinceLastLog.toFixed(2)} minutes, treating as stale`);
586
+ return;
587
+ }
588
+ }
589
+ }
590
+ }
223
591
 
224
- // Connect to socket for this research
225
- window.connectToResearchSocket(currentResearchId);
592
+ // If we get here, the research seems to be genuinely active
593
+ isResearchInProgress = true;
594
+ currentResearchId = activeResearch.id;
595
+ window.currentResearchId = currentResearchId;
226
596
 
227
- // Start polling for updates
228
- pollResearchStatus(currentResearchId);
597
+ // Check if we're on the new research page and redirect to progress
598
+ const currentPage = document.querySelector('.page.active');
599
+
600
+ if (currentPage && currentPage.id === 'new-research') {
601
+ // Navigate to progress page
602
+ switchPage('research-progress');
603
+
604
+ // Connect to socket for this research
605
+ window.connectToResearchSocket(currentResearchId);
606
+
607
+ // Start polling for updates
608
+ pollResearchStatus(currentResearchId);
609
+ }
610
+ } catch (detailsError) {
611
+ console.error('Error checking research details:', detailsError);
229
612
  }
230
613
  }
231
614
  } catch (error) {
@@ -240,162 +623,320 @@ document.addEventListener('DOMContentLoaded', () => {
240
623
 
241
624
  // Function to start research
242
625
  async function startResearch(query, mode) {
243
- // Check if research is already in progress
244
- if (isResearchInProgress) {
245
- alert('Another research is already in progress. Please wait for it to complete or check its status in the history tab.');
626
+ // First validate that we have a query
627
+ if (!query || query.trim() === '') {
628
+ alert('Please enter a query');
246
629
  return;
247
630
  }
248
631
 
249
- // Get the start button
250
- const startResearchBtn = document.getElementById('start-research-btn');
251
- if (!startResearchBtn) return;
252
-
253
- // Disable the start button while we attempt to start the research
254
- startResearchBtn.disabled = true;
255
- startResearchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
256
-
257
632
  try {
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');
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
258
659
  const response = await fetch(getApiUrl('/api/start_research'), {
259
660
  method: 'POST',
260
661
  headers: {
261
662
  'Content-Type': 'application/json'
262
663
  },
263
- body: JSON.stringify({
264
- query: query,
265
- mode: mode
266
- })
664
+ body: JSON.stringify(payload)
267
665
  });
268
666
 
269
- const data = await response.json();
667
+ // Parse the response
668
+ const result = await response.json();
270
669
 
271
- 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
272
678
  isResearchInProgress = true;
273
- currentResearchId = data.research_id;
274
679
 
275
- // Also update the window object
276
- 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
+ }
277
685
 
278
- // 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
279
693
  updateNavigationBasedOnResearchStatus();
280
694
 
281
- // Update progress page
282
- document.getElementById('current-query').textContent = query;
283
- document.getElementById('progress-fill').style.width = '0%';
284
- document.getElementById('progress-percentage').textContent = '0%';
285
- document.getElementById('progress-status').textContent = 'Initializing research process...';
286
-
287
- // Navigate to progress page
695
+ // Navigate to the progress page
288
696
  switchPage('research-progress');
289
697
 
290
- // Connect to socket for this research
291
- window.connectToResearchSocket(data.research_id);
698
+ // Connect to the socket for this research
699
+ window.connectToResearchSocket(currentResearchId);
292
700
 
293
701
  // Start polling for status
294
- pollResearchStatus(data.research_id);
702
+ pollResearchStatus(currentResearchId);
703
+ } else {
704
+ // Handle error
705
+ const errorMessage = result.message || 'Failed to start research';
706
+ console.error('Research start error:', errorMessage);
295
707
 
296
- // Show the terminate button
297
- const terminateBtn = document.getElementById('terminate-research-btn');
298
- if (terminateBtn) {
299
- terminateBtn.style.display = 'inline-flex';
300
- terminateBtn.disabled = false;
708
+ // Add error to log
709
+ addConsoleLog(`Error: ${errorMessage}`, 'error');
710
+
711
+ alert(errorMessage);
712
+
713
+ // Reset the favicon
714
+ setFavicon('default');
715
+
716
+ // Reset button state
717
+ if (startBtn) {
718
+ startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
719
+ startBtn.disabled = false;
301
720
  }
302
- } else {
303
- alert('Error starting research: ' + (data.message || 'Unknown error'));
304
- startResearchBtn.disabled = false;
305
- startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
306
721
  }
307
722
  } catch (error) {
308
- console.error('Error:', error);
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
+ }
737
+
309
738
  alert('An error occurred while starting the research. Please try again.');
310
- startResearchBtn.disabled = false;
311
- startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
312
739
  }
313
740
  }
314
741
 
315
- // 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
316
752
  function pollResearchStatus(researchId) {
317
- console.log(`Polling research status for ID: ${researchId}`);
318
- fetch(getApiUrl(`/api/research/${researchId}`))
319
- .then(response => {
320
- if (!response.ok) {
321
- throw new Error(`Server returned ${response.status}`);
322
- }
323
- return response.json();
324
- })
325
- .then(data => {
326
- console.log('Research status response:', data);
327
- // Update the UI with the current progress
328
- updateProgressUI(data.progress, data.status, data.message);
329
-
330
- // If research is complete, show the completion buttons
331
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'suspended') {
332
- console.log(`Research is in final state: ${data.status}`);
333
- // Clear the polling interval
334
- if (pollingInterval) {
335
- console.log('Clearing polling interval');
336
- clearInterval(pollingInterval);
337
- pollingInterval = null;
338
- }
339
-
340
- // Update UI for completion
341
- if (data.status === 'completed') {
342
- console.log('Research completed, loading results automatically');
343
- // Hide the terminate button
344
- const terminateBtn = document.getElementById('terminate-research-btn');
345
- if (terminateBtn) {
346
- terminateBtn.style.display = 'none';
347
- }
348
-
349
- // Auto-load the results instead of showing a button
350
- loadResearch(researchId);
351
- } else if (data.status === 'failed' || data.status === 'suspended') {
352
- console.log(`Showing error message for status: ${data.status}`);
353
- const errorMessage = document.getElementById('error-message');
354
- if (errorMessage) {
355
- errorMessage.style.display = 'block';
356
- errorMessage.textContent = data.status === 'failed' ?
357
- (data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
358
- 'Research was suspended';
359
- } else {
360
- console.error('error-message element not found');
361
- }
362
- }
363
-
364
- // Play notification sound based on status
365
- if (data.status === 'completed') {
366
- console.log('Playing success notification sound');
367
- playNotificationSound('success');
368
- } else if (data.status === 'failed') {
369
- console.log('Playing error notification sound');
370
- playNotificationSound('error');
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;
371
802
  }
372
803
 
373
- // Update the navigation
374
- console.log('Updating navigation based on research status');
375
- updateNavigationBasedOnResearchStatus();
376
-
377
- // Force the UI to update with a manual trigger
378
- document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
379
-
380
- return;
381
- }
382
-
383
- // Continue polling if still in progress
384
- if (data.status === 'in_progress') {
385
- console.log('Research is still in progress, continuing polling');
386
- if (!pollingInterval) {
387
- console.log('Setting up polling interval');
388
- pollingInterval = setInterval(() => {
389
- pollResearchStatus(researchId);
390
- }, 10000);
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';
809
+
810
+ // Get most recent message
811
+ let message = '';
812
+ let foundNewMessage = false;
813
+ let latestMetadata = null;
814
+
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
846
+ }
847
+ }
848
+ }
849
+
850
+ // Use a meaningful message if available; otherwise, keep the last good one
851
+ const displayMessage = foundNewMessage ? message :
852
+ (window.lastMeaningfulStatusMessage || 'Processing research...');
853
+
854
+ // Update progress UI
855
+ updateProgressUI(progress, status, displayMessage);
856
+
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
+ }
391
922
  }
392
- }
393
- })
394
- .catch(error => {
395
- console.error('Error polling research status:', error);
396
- });
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
+ }
397
929
  }
398
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
+
399
940
  // Main initialization function
400
941
  function initializeApp() {
401
942
  console.log('Initializing application...');
@@ -403,11 +944,33 @@ document.addEventListener('DOMContentLoaded', () => {
403
944
  // Initialize the sounds
404
945
  initializeSounds();
405
946
 
947
+ // Initialize socket connection
948
+ initializeSocket();
949
+
950
+ // Create a dynamic favicon with the lightning emoji by default
951
+ createDynamicFavicon('⚡');
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
+
406
968
  // Get navigation elements
407
969
  const navItems = document.querySelectorAll('.sidebar-nav li');
408
970
  const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
409
971
  const pages = document.querySelectorAll('.page');
410
972
  const mobileTabBar = document.querySelector('.mobile-tab-bar');
973
+ const logo = document.getElementById('logo-link');
411
974
 
412
975
  // Handle responsive navigation based on screen size
413
976
  function handleResponsiveNavigation() {
@@ -429,6 +992,14 @@ document.addEventListener('DOMContentLoaded', () => {
429
992
  // Add resize listener for responsive design
430
993
  window.addEventListener('resize', handleResponsiveNavigation);
431
994
 
995
+ // Handle logo click
996
+ if (logo) {
997
+ logo.addEventListener('click', () => {
998
+ switchPage('new-research');
999
+ resetStartResearchButton();
1000
+ });
1001
+ }
1002
+
432
1003
  // Setup navigation click handlers
433
1004
  navItems.forEach(item => {
434
1005
  if (!item.classList.contains('external-link')) {
@@ -436,6 +1007,10 @@ document.addEventListener('DOMContentLoaded', () => {
436
1007
  const pageId = this.dataset.page;
437
1008
  if (pageId) {
438
1009
  switchPage(pageId);
1010
+ // Reset Start Research button when returning to the form
1011
+ if (pageId === 'new-research') {
1012
+ resetStartResearchButton();
1013
+ }
439
1014
  }
440
1015
  });
441
1016
  }
@@ -447,6 +1022,10 @@ document.addEventListener('DOMContentLoaded', () => {
447
1022
  const pageId = this.dataset.page;
448
1023
  if (pageId) {
449
1024
  switchPage(pageId);
1025
+ // Reset Start Research button when returning to the form
1026
+ if (pageId === 'new-research') {
1027
+ resetStartResearchButton();
1028
+ }
450
1029
  }
451
1030
  });
452
1031
  }
@@ -474,6 +1053,10 @@ document.addEventListener('DOMContentLoaded', () => {
474
1053
  option.addEventListener('click', function() {
475
1054
  modeOptions.forEach(opt => opt.classList.remove('active'));
476
1055
  this.classList.add('active');
1056
+
1057
+ // Update favicon based on selected mode
1058
+ const mode = this.dataset.mode;
1059
+ setFavicon(mode);
477
1060
  });
478
1061
  });
479
1062
 
@@ -496,44 +1079,41 @@ document.addEventListener('DOMContentLoaded', () => {
496
1079
 
497
1080
  // Function to switch between pages
498
1081
  function switchPage(pageId) {
499
- // Get elements directly from the DOM
1082
+ // First hide all pages
500
1083
  const pages = document.querySelectorAll('.page');
501
- const navItems = document.querySelectorAll('.sidebar-nav li');
502
- const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
503
-
504
- // Remove active class from all pages
505
- pages.forEach(page => {
506
- page.classList.remove('active');
507
- });
1084
+ pages.forEach(page => page.classList.remove('active'));
508
1085
 
509
- // Add active class to target page
510
- const targetPage = document.getElementById(pageId);
511
- if (targetPage) {
512
- targetPage.classList.add('active');
1086
+ // Then activate the selected page
1087
+ const selectedPage = document.getElementById(pageId);
1088
+ if (selectedPage) {
1089
+ selectedPage.classList.add('active');
513
1090
  }
514
1091
 
515
- // Update sidebar navigation active states
516
- navItems.forEach(item => {
517
- if (item.getAttribute('data-page') === pageId) {
518
- item.classList.add('active');
519
- } else {
520
- item.classList.remove('active');
521
- }
522
- });
1092
+ // Update the URL hash
1093
+ window.location.hash = '#' + pageId;
523
1094
 
524
- // Update mobile tab bar active states
525
- mobileNavItems.forEach(item => {
526
- if (item.getAttribute('data-page') === pageId) {
527
- item.classList.add('active');
528
- } else {
529
- item.classList.remove('active');
530
- }
531
- });
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
+ }
532
1104
 
533
1105
  // Special handling for history page
534
1106
  if (pageId === 'history') {
535
1107
  loadResearchHistory();
536
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);
537
1117
  }
538
1118
 
539
1119
  // Track termination status
@@ -552,15 +1132,41 @@ document.addEventListener('DOMContentLoaded', () => {
552
1132
  return `/research${path}`;
553
1133
  }
554
1134
 
555
- // 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
556
1162
  async function terminateResearch(researchId) {
557
1163
  if (!researchId) {
558
1164
  console.error('No research ID provided for termination');
559
1165
  return;
560
1166
  }
561
-
1167
+
562
1168
  // Prevent multiple termination requests
563
- if (isTerminating) {
1169
+ if (document.getElementById('terminate-research-btn')?.disabled) {
564
1170
  console.log('Termination already in progress');
565
1171
  return;
566
1172
  }
@@ -570,85 +1176,219 @@ document.addEventListener('DOMContentLoaded', () => {
570
1176
  return;
571
1177
  }
572
1178
 
1179
+ console.log(`Attempting to terminate research: ${researchId}`);
1180
+
573
1181
  try {
574
- // Set terminating flag
575
- isTerminating = true;
576
-
577
- // Update UI to show we're processing
1182
+ // Get the terminate button
578
1183
  const terminateBtn = document.getElementById('terminate-research-btn');
579
1184
  if (terminateBtn) {
1185
+ // Disable the button to prevent multiple clicks
580
1186
  terminateBtn.disabled = true;
581
1187
  terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
582
1188
  }
583
-
584
- // Find all terminate buttons in history items and disable them
585
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
586
- allTerminateBtns.forEach(btn => {
587
- if (btn !== terminateBtn) {
588
- btn.disabled = true;
589
- btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
590
- }
591
- });
592
1189
 
593
- // 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
594
1211
  const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
595
- method: 'POST'
1212
+ method: 'POST',
1213
+ headers: {
1214
+ 'Content-Type': 'application/json'
1215
+ }
596
1216
  });
597
1217
 
598
1218
  const data = await response.json();
599
1219
 
600
1220
  if (data.status === 'success') {
601
- // The UI will be updated via socket events or polling
602
- console.log('Termination request sent successfully');
1221
+ console.log(`Research ${researchId} termination requested successfully`);
603
1222
 
604
- // If we're on the history page, update the status of this item
605
- if (document.getElementById('history').classList.contains('active')) {
606
- updateHistoryItemStatus(researchId, 'terminating', 'Terminating...');
607
- }
608
- } else {
609
- console.error('Termination request failed:', data.message);
610
- alert(`Failed to terminate research: ${data.message}`);
1223
+ // Add termination log
1224
+ addConsoleLog('Research termination requested. Please wait...', 'error');
1225
+
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
+ }
1257
+
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
+ }
1289
+ }
1290
+ }, 300); // Check faster for more responsive feedback
611
1291
 
612
- // Reset the terminating flag
613
- isTerminating = false;
1292
+ } else {
1293
+ console.error(`Error terminating research: ${data.message}`);
1294
+ addConsoleLog(`Error terminating research: ${data.message}`, 'error');
614
1295
 
615
- // Reset the button
1296
+ // Re-enable the button
616
1297
  if (terminateBtn) {
617
1298
  terminateBtn.disabled = false;
618
1299
  terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
619
1300
  }
620
-
621
- // Reset history button states
622
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
623
- allTerminateBtns.forEach(btn => {
624
- if (btn !== terminateBtn) {
625
- btn.disabled = false;
626
- btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
627
- }
628
- });
629
- }
630
- } catch (error) {
631
- console.error('Error terminating research:', error);
632
- alert('An error occurred while trying to terminate the research.');
633
-
634
- // Reset the terminating flag
635
- isTerminating = false;
1301
+ }
1302
+ } catch (error) {
1303
+ console.error(`Error in terminate request: ${error}`);
1304
+ addConsoleLog(`Error in terminate request: ${error}`, 'error');
636
1305
 
637
- // Reset the button
1306
+ // Re-enable the button
638
1307
  const terminateBtn = document.getElementById('terminate-research-btn');
639
1308
  if (terminateBtn) {
640
1309
  terminateBtn.disabled = false;
641
1310
  terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
642
1311
  }
643
-
644
- // Reset history button states
645
- const allTerminateBtns = document.querySelectorAll('.terminate-btn');
646
- allTerminateBtns.forEach(btn => {
647
- if (btn !== terminateBtn) {
648
- btn.disabled = false;
649
- btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
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';
650
1328
  }
651
- });
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;
652
1392
  }
653
1393
  }
654
1394
 
@@ -660,6 +1400,8 @@ document.addEventListener('DOMContentLoaded', () => {
660
1400
  const progressFill = document.getElementById('progress-fill');
661
1401
  const progressPercentage = document.getElementById('progress-percentage');
662
1402
  const progressStatus = document.getElementById('progress-status');
1403
+ const errorMessage = document.getElementById('error-message');
1404
+ const tryAgainBtn = document.getElementById('try-again-btn');
663
1405
 
664
1406
  if (progressFill && progressPercentage) {
665
1407
  progressFill.style.width = `${progress}%`;
@@ -683,12 +1425,51 @@ document.addEventListener('DOMContentLoaded', () => {
683
1425
  terminateBtn.style.display = 'inline-flex';
684
1426
  terminateBtn.disabled = false;
685
1427
  terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1428
+
1429
+ // Hide try again button when in progress
1430
+ if (tryAgainBtn) {
1431
+ tryAgainBtn.style.display = 'none';
1432
+ }
686
1433
  } else if (status === 'terminating') {
687
1434
  terminateBtn.style.display = 'inline-flex';
688
1435
  terminateBtn.disabled = true;
689
1436
  terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
690
1437
  } else {
691
1438
  terminateBtn.style.display = 'none';
1439
+
1440
+ // Show try again button for failed or suspended research
1441
+ if (tryAgainBtn && (status === 'failed' || status === 'suspended')) {
1442
+ tryAgainBtn.style.display = 'inline-flex';
1443
+
1444
+ // Get the current query for retry
1445
+ const currentQuery = document.getElementById('current-query');
1446
+ const queryText = currentQuery ? currentQuery.textContent : '';
1447
+
1448
+ // Add click event to try again button to go back to research form with the query preserved
1449
+ tryAgainBtn.onclick = function() {
1450
+ // Switch to the research form
1451
+ switchPage('new-research');
1452
+
1453
+ // Set the query text in the form
1454
+ const queryTextarea = document.getElementById('query');
1455
+ if (queryTextarea && queryText) {
1456
+ queryTextarea.value = queryText;
1457
+ }
1458
+
1459
+ // Clean up any remaining research state
1460
+ window.cleanupResearchResources();
1461
+ };
1462
+ }
1463
+ }
1464
+ }
1465
+
1466
+ // Show error message when there's an error
1467
+ if (errorMessage) {
1468
+ if (status === 'failed' || status === 'suspended') {
1469
+ errorMessage.style.display = 'block';
1470
+ errorMessage.textContent = message || (status === 'failed' ? 'Research failed' : 'Research was suspended');
1471
+ } else {
1472
+ errorMessage.style.display = 'none';
692
1473
  }
693
1474
  }
694
1475
  }
@@ -764,8 +1545,11 @@ document.addEventListener('DOMContentLoaded', () => {
764
1545
  title.textContent = item.query || 'Untitled Research';
765
1546
 
766
1547
  const status = document.createElement('div');
767
- status.className = `history-item-status status-${item.status || 'unknown'}`;
768
- status.textContent = item.status ? (item.status.charAt(0).toUpperCase() + item.status.slice(1)) : 'Unknown';
1548
+ status.className = `history-item-status status-${item.status ? item.status.replace('_', '-') : 'unknown'}`;
1549
+ status.textContent = item.status ?
1550
+ (item.status === 'in_progress' ? 'In Progress' :
1551
+ item.status.charAt(0).toUpperCase() + item.status.slice(1)) :
1552
+ 'Unknown';
769
1553
 
770
1554
  header.appendChild(title);
771
1555
  header.appendChild(status);
@@ -913,6 +1697,32 @@ document.addEventListener('DOMContentLoaded', () => {
913
1697
 
914
1698
  // Function to navigate to research progress
915
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
+
916
1726
  document.getElementById('current-query').textContent = research.query;
917
1727
  document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
918
1728
  document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
@@ -920,6 +1730,9 @@ document.addEventListener('DOMContentLoaded', () => {
920
1730
  // Navigate to progress page
921
1731
  switchPage('research-progress');
922
1732
 
1733
+ // Load logs for this research
1734
+ loadLogsForResearch(research.id);
1735
+
923
1736
  // Connect to socket for this research
924
1737
  window.connectToResearchSocket(research.id);
925
1738
 
@@ -946,150 +1759,204 @@ document.addEventListener('DOMContentLoaded', () => {
946
1759
  }
947
1760
  }
948
1761
 
949
- // Function to load a specific research result
1762
+ // Update the loadResearch function to handle terminated/suspended research better
950
1763
  async function loadResearch(researchId) {
951
- // Navigate to results page
952
- switchPage('research-results');
953
-
954
- const resultsContent = document.getElementById('results-content');
955
- resultsContent.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
956
-
957
1764
  try {
958
- // Load research details
959
- const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
960
- const details = await detailsResponse.json();
961
-
962
- // Display metadata
963
- document.getElementById('result-query').textContent = details.query;
964
-
965
- // Format date with duration if available
966
- let dateText = formatDate(new Date(details.completed_at || details.created_at));
967
- if (details.duration_seconds) {
968
- // Format duration
969
- let durationText = '';
970
- const duration = parseInt(details.duration_seconds);
971
-
972
- if (duration < 60) { // less than a minute
973
- durationText = `${duration}s`;
974
- } else if (duration < 3600) { // less than an hour
975
- durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
976
- } else { // hours
977
- durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
978
- }
979
-
980
- dateText += ` (Duration: ${durationText})`;
981
- }
982
- document.getElementById('result-date').textContent = dateText;
983
-
984
- document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
1765
+ console.log(`Loading research results for research ID: ${researchId}`);
985
1766
 
986
- // Load the report content
987
- const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
988
- const reportData = await reportResponse.json();
1767
+ // Set the current viewing research ID
1768
+ viewingResearchId = researchId;
989
1769
 
990
- if (reportData.status === 'success') {
991
- // Render markdown
992
- const renderedContent = marked.parse(reportData.content);
993
- resultsContent.innerHTML = renderedContent;
994
-
995
- // Apply syntax highlighting
996
- document.querySelectorAll('pre code').forEach((block) => {
997
- hljs.highlightElement(block);
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`);
1777
+
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
+ }
1783
+
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
1810
+ }
1811
+
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}`);
998
1879
  });
999
- } else {
1000
- resultsContent.innerHTML = '<div class="error-message">Error loading report. Please try again later.</div>';
1001
- }
1002
1880
  } catch (error) {
1003
- console.error('Error loading research:', error);
1004
- resultsContent.innerHTML = '<div class="error-message">Error loading research results. Please try again later.</div>';
1881
+ console.error(`Error loading research: ${error}`);
1005
1882
  }
1006
1883
  }
1007
1884
 
1008
- // Function to load research details page
1885
+ // Function to load research details
1009
1886
  async function loadResearchDetails(researchId) {
1010
- // Navigate to details page
1011
- switchPage('research-details');
1012
-
1013
- // Initialize the research log area
1014
- const researchLog = document.getElementById('research-log');
1015
- researchLog.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1016
-
1017
1887
  try {
1018
- // Load research details
1019
- const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
1020
- console.log('Research details API response status:', response.status);
1888
+ // Show loading indicators
1889
+ document.getElementById('research-log').innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1021
1890
 
1022
- if (!response.ok) {
1023
- console.error('API error:', response.status, response.statusText);
1024
- researchLog.innerHTML = `<div class="error-message">Error loading research details. Status: ${response.status}</div>`;
1025
- return;
1026
- }
1891
+ // Set the current viewing research ID
1892
+ viewingResearchId = researchId;
1027
1893
 
1894
+ // Fetch the research data
1895
+ const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
1028
1896
  const data = await response.json();
1029
- console.log('Research details data:', data);
1030
-
1031
- if (data.status !== 'success') {
1032
- researchLog.innerHTML = `<div class="error-message">Error loading research details: ${data.message || 'Unknown error'}</div>`;
1033
- return;
1034
- }
1035
-
1036
- // Display metadata
1037
- document.getElementById('detail-query').textContent = data.query || 'N/A';
1038
- document.getElementById('detail-status').textContent = capitalizeFirstLetter(data.status || 'unknown');
1039
- document.getElementById('detail-status').className = `metadata-value status-${data.status || 'unknown'}`;
1040
- document.getElementById('detail-mode').textContent = (data.mode === 'quick' ? 'Quick Summary' : 'Detailed Report') || 'N/A';
1041
-
1042
- // Update progress bar
1043
- const progress = data.progress || 0;
1044
- document.getElementById('detail-progress-fill').style.width = `${progress}%`;
1045
- document.getElementById('detail-progress-percentage').textContent = `${progress}%`;
1046
-
1047
- // Render log entries
1048
- renderLogEntries(data.log || []);
1049
1897
 
1050
- // Connect to socket for real-time updates
1051
- window.connectToResearchSocket(researchId);
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);
1052
1903
 
1053
- // Add appropriate actions based on research status
1054
- const detailActions = document.getElementById('detail-actions');
1055
- detailActions.innerHTML = '';
1056
-
1057
- if (data.status === 'completed') {
1058
- const viewResultsBtn = document.createElement('button');
1059
- viewResultsBtn.className = 'btn btn-primary';
1060
- viewResultsBtn.innerHTML = '<i class="fas fa-eye"></i> View Results';
1061
- viewResultsBtn.addEventListener('click', () => loadResearch(researchId));
1062
- detailActions.appendChild(viewResultsBtn);
1063
-
1064
- // Add download PDF button
1065
- const downloadPdfBtn = document.createElement('button');
1066
- downloadPdfBtn.className = 'btn btn-outline';
1067
- downloadPdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
1068
- downloadPdfBtn.addEventListener('click', () => generatePdfFromResearch(researchId));
1069
- detailActions.appendChild(downloadPdfBtn);
1070
- } else if (data.status === 'in_progress') {
1071
- const viewProgressBtn = document.createElement('button');
1072
- viewProgressBtn.className = 'btn btn-primary';
1073
- viewProgressBtn.innerHTML = '<i class="fas fa-sync"></i> View Live Progress';
1074
- viewProgressBtn.addEventListener('click', () => {
1075
- document.getElementById('current-query').textContent = data.query || '';
1076
-
1077
- // Navigate to progress page
1078
- switchPage('research-progress');
1079
-
1080
- // Connect to socket
1081
- window.connectToResearchSocket(researchId);
1082
- });
1083
- detailActions.appendChild(viewProgressBtn);
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');
1949
+ } else {
1950
+ alert('Failed to load research details: ' + (data.message || 'Unknown error'));
1084
1951
  }
1085
1952
  } catch (error) {
1086
1953
  console.error('Error loading research details:', error);
1087
- researchLog.innerHTML = `<div class="error-message">Error loading research details: ${error.message}</div>`;
1954
+ alert('An error occurred while loading the research details');
1088
1955
  }
1089
1956
  }
1090
1957
 
1091
1958
  // Function to render log entries
1092
- function renderLogEntries(logEntries) {
1959
+ function renderResearchLog(logEntries, researchId) {
1093
1960
  const researchLog = document.getElementById('research-log');
1094
1961
  researchLog.innerHTML = '';
1095
1962
 
@@ -1167,8 +2034,18 @@ document.addEventListener('DOMContentLoaded', () => {
1167
2034
  }
1168
2035
 
1169
2036
  // Connect to socket for updates if this is an in-progress research
1170
- if (research && research.status === 'in_progress') {
1171
- 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}`));
1172
2049
  }
1173
2050
  }
1174
2051
 
@@ -1213,15 +2090,13 @@ document.addEventListener('DOMContentLoaded', () => {
1213
2090
  researchLog.scrollTop = researchLog.scrollHeight;
1214
2091
  }
1215
2092
 
1216
- // Back to history button handlers
1217
- document.getElementById('back-to-history').addEventListener('click', () => {
1218
- const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
1219
- historyNav.click();
2093
+ // Back to history button handlers - using direct page switching
2094
+ document.getElementById('back-to-history')?.addEventListener('click', () => {
2095
+ switchPage('history');
1220
2096
  });
1221
2097
 
1222
- document.getElementById('back-to-history-from-details').addEventListener('click', () => {
1223
- const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
1224
- historyNav.click();
2098
+ document.getElementById('back-to-history-from-details')?.addEventListener('click', () => {
2099
+ switchPage('history');
1225
2100
  });
1226
2101
 
1227
2102
  // Helper functions
@@ -1229,31 +2104,6 @@ document.addEventListener('DOMContentLoaded', () => {
1229
2104
  return string.charAt(0).toUpperCase() + string.slice(1);
1230
2105
  }
1231
2106
 
1232
- function formatDate(date) {
1233
- // Ensure we're handling the date properly
1234
- if (!(date instanceof Date) || isNaN(date)) {
1235
- console.warn('Invalid date provided to formatDate:', date);
1236
- return 'Invalid date';
1237
- }
1238
-
1239
- // Get current year to compare with date year
1240
- const currentYear = new Date().getFullYear();
1241
- const dateYear = date.getFullYear();
1242
-
1243
- // Get month name, day, and time
1244
- const month = date.toLocaleString('en-US', { month: 'short' });
1245
- const day = date.getDate();
1246
- const hours = date.getHours().toString().padStart(2, '0');
1247
- const minutes = date.getMinutes().toString().padStart(2, '0');
1248
-
1249
- // Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
1250
- if (dateYear === currentYear) {
1251
- return `${month} ${day}, ${hours}:${minutes}`;
1252
- } else {
1253
- return `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
1254
- }
1255
- }
1256
-
1257
2107
  // Function to update progress UI from current research
1258
2108
  function updateProgressFromCurrentResearch() {
1259
2109
  if (!isResearchInProgress || !currentResearchId) return;
@@ -1276,108 +2126,99 @@ document.addEventListener('DOMContentLoaded', () => {
1276
2126
 
1277
2127
  // Function to update the sidebar navigation based on research status
1278
2128
  function updateNavigationBasedOnResearchStatus() {
1279
- console.log("Updating navigation based on research status");
1280
- console.log("isResearchInProgress:", isResearchInProgress);
1281
- console.log("currentResearchId:", currentResearchId);
1282
-
1283
- // Get nav items for each update to ensure we have fresh references
1284
- const navItems = document.querySelectorAll('.sidebar-nav li');
1285
- const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
1286
- // Get all pages
1287
- const pages = document.querySelectorAll('.page');
2129
+ const isResearchPage = document.getElementById('research-progress').classList.contains('active');
2130
+ const isAnyResearchInProgress = window.currentResearchId !== null && isResearchInProgress;
1288
2131
 
1289
- const newResearchNav = Array.from(navItems).find(item =>
1290
- item.getAttribute('data-page') === 'new-research' ||
1291
- (item.getAttribute('data-original-page') === 'new-research' &&
1292
- item.getAttribute('data-page') === 'research-progress')
1293
- );
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"]');
1294
2137
 
1295
- if (newResearchNav) {
1296
- if (isResearchInProgress) {
1297
- console.log("Research is in progress, updating navigation");
1298
- // Change text to "Research in Progress"
1299
- newResearchNav.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Research in Progress';
1300
-
1301
- // Also update the listener to navigate to progress page
1302
- if (newResearchNav.getAttribute('data-page') !== 'research-progress') {
1303
- newResearchNav.setAttribute('data-original-page', 'new-research');
1304
- newResearchNav.setAttribute('data-page', 'research-progress');
1305
- }
1306
-
1307
- // If on new-research page, redirect to research-progress
1308
- if (document.getElementById('new-research').classList.contains('active')) {
1309
- pages.forEach(page => page.classList.remove('active'));
1310
- document.getElementById('research-progress').classList.add('active');
1311
-
1312
- // Update the research progress page
1313
- updateProgressFromCurrentResearch();
1314
- }
1315
- } else {
1316
- console.log("Research is not in progress, resetting navigation");
1317
- // Reset to "New Research" if there's no active research
1318
- newResearchNav.innerHTML = '<i class="fas fa-search"></i> New Research';
1319
-
1320
- // Reset the listener
1321
- if (newResearchNav.hasAttribute('data-original-page')) {
1322
- newResearchNav.setAttribute('data-page', newResearchNav.getAttribute('data-original-page'));
1323
- newResearchNav.removeAttribute('data-original-page');
1324
- }
1325
-
1326
- // If the terminate button is visible, hide it
1327
- const terminateBtn = document.getElementById('terminate-research-btn');
1328
- if (terminateBtn) {
1329
- terminateBtn.style.display = 'none';
1330
- terminateBtn.disabled = false;
1331
- terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1332
- }
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');
2142
+
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');
1333
2157
  }
1334
2158
 
1335
- // Make sure the navigation highlights the correct item
1336
- navItems.forEach(item => {
1337
- if (item === newResearchNav) {
1338
- if (isResearchInProgress) {
1339
- if (document.getElementById('research-progress').classList.contains('active')) {
1340
- item.classList.add('active');
1341
- }
1342
- } else if (document.getElementById('new-research').classList.contains('active')) {
1343
- item.classList.add('active');
1344
- }
1345
- }
1346
- });
1347
- }
1348
-
1349
- // Connect to socket for updates
1350
- if (currentResearchId) {
1351
- window.connectToResearchSocket(currentResearchId);
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';
2163
+ } else {
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';
1352
2172
  }
2173
+
2174
+ console.log('Updated navigation based on research status. In progress:', isAnyResearchInProgress);
1353
2175
  }
1354
2176
 
1355
2177
  // Function to update the research progress page from nav click
1356
2178
  function updateProgressPage() {
1357
- if (!isResearchInProgress || !currentResearchId) {
2179
+ if (!currentResearchId && !window.currentResearchId) {
1358
2180
  return;
1359
2181
  }
1360
2182
 
2183
+ const researchId = currentResearchId || window.currentResearchId;
2184
+
1361
2185
  // Update the progress page
1362
- fetch(getApiUrl(`/api/research/${currentResearchId}`))
2186
+ fetch(getApiUrl(`/api/research/${researchId}`))
1363
2187
  .then(response => response.json())
1364
2188
  .then(data => {
1365
2189
  // Update the query display
1366
2190
  document.getElementById('current-query').textContent = data.query;
1367
2191
 
1368
- // Update the progress bar
1369
- updateProgressUI(data.progress || 0, data.status);
1370
-
1371
- // Connect to socket for live updates
1372
- window.connectToResearchSocket(currentResearchId);
1373
-
1374
- // Check if we need to show the terminate button
2192
+ // Check status before updating UI
1375
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
1376
2198
  const terminateBtn = document.getElementById('terminate-research-btn');
1377
2199
  if (terminateBtn) {
1378
2200
  terminateBtn.style.display = 'inline-flex';
1379
2201
  terminateBtn.disabled = false;
1380
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}%`;
1381
2222
  }
1382
2223
  })
1383
2224
  .catch(error => {
@@ -1389,14 +2230,22 @@ document.addEventListener('DOMContentLoaded', () => {
1389
2230
  function updateHistoryItemStatus(researchId, status, statusText) {
1390
2231
  const historyList = document.getElementById('history-list');
1391
2232
 
2233
+ // Format the status for display
2234
+ const displayStatus = statusText ||
2235
+ (status === 'in_progress' ? 'In Progress' :
2236
+ status.charAt(0).toUpperCase() + status.slice(1));
2237
+
2238
+ // Format the CSS class
2239
+ const statusClass = `status-${status.replace('_', '-')}`;
2240
+
1392
2241
  // Look for the item in the active research banner
1393
2242
  const activeBanner = historyList.querySelector(`.active-research-banner[data-research-id="${researchId}"]`);
1394
2243
  if (activeBanner) {
1395
2244
  const statusEl = activeBanner.querySelector('.history-item-status');
1396
2245
  if (statusEl) {
1397
- statusEl.textContent = statusText || capitalizeFirstLetter(status);
2246
+ statusEl.textContent = displayStatus;
1398
2247
  statusEl.className = 'history-item-status';
1399
- statusEl.classList.add(`status-${status}`);
2248
+ statusEl.classList.add(statusClass);
1400
2249
  }
1401
2250
 
1402
2251
  // Update buttons
@@ -1423,9 +2272,9 @@ document.addEventListener('DOMContentLoaded', () => {
1423
2272
  if (historyItem) {
1424
2273
  const statusEl = historyItem.querySelector('.history-item-status');
1425
2274
  if (statusEl) {
1426
- statusEl.textContent = statusText || capitalizeFirstLetter(status);
2275
+ statusEl.textContent = displayStatus;
1427
2276
  statusEl.className = 'history-item-status';
1428
- statusEl.classList.add(`status-${status}`);
2277
+ statusEl.classList.add(statusClass);
1429
2278
  }
1430
2279
 
1431
2280
  // Update view button
@@ -1437,6 +2286,15 @@ document.addEventListener('DOMContentLoaded', () => {
1437
2286
  } else if (status === 'failed') {
1438
2287
  viewBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
1439
2288
  viewBtn.disabled = true;
2289
+ } else if (status === 'completed') {
2290
+ viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
2291
+ viewBtn.disabled = false;
2292
+
2293
+ // Also make the PDF button visible if not already
2294
+ const pdfBtn = historyItem.querySelector('.pdf-btn');
2295
+ if (pdfBtn) {
2296
+ pdfBtn.style.display = 'inline-flex';
2297
+ }
1440
2298
  }
1441
2299
  }
1442
2300
  }
@@ -1953,22 +2811,11 @@ document.addEventListener('DOMContentLoaded', () => {
1953
2811
  hljs.highlightElement(block);
1954
2812
  });
1955
2813
 
1956
- // Format date
1957
- let dateText = formatDate(new Date(details.completed_at || details.created_at));
1958
- if (details.duration_seconds) {
1959
- let durationText = '';
1960
- const duration = parseInt(details.duration_seconds);
1961
-
1962
- if (duration < 60) {
1963
- durationText = `${duration}s`;
1964
- } else if (duration < 3600) {
1965
- durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
1966
- } else {
1967
- durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
1968
- }
1969
-
1970
- dateText += ` (Duration: ${durationText})`;
1971
- }
2814
+ // Format date with duration
2815
+ let dateText = formatDate(
2816
+ new Date(details.completed_at || details.created_at),
2817
+ details.duration_seconds
2818
+ );
1972
2819
 
1973
2820
  // Set up data for PDF generation
1974
2821
  document.getElementById('result-query').textContent = details.query;
@@ -2003,11 +2850,12 @@ document.addEventListener('DOMContentLoaded', () => {
2003
2850
  // Initialize the terminate button event listener
2004
2851
  const terminateBtn = document.getElementById('terminate-research-btn');
2005
2852
  if (terminateBtn) {
2006
- terminateBtn.addEventListener('click', () => {
2007
- if (confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
2008
- if (currentResearchId) {
2009
- terminateResearch(currentResearchId);
2010
- }
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.');
2011
2859
  }
2012
2860
  });
2013
2861
  }
@@ -2075,4 +2923,841 @@ document.addEventListener('DOMContentLoaded', () => {
2075
2923
  // Update navigation
2076
2924
  updateNavigationBasedOnResearchStatus();
2077
2925
  });
2926
+
2927
+ // Function to reset progress animations and UI elements
2928
+ function resetProgressAnimations() {
2929
+ // Reset the progress bar animation
2930
+ const progressFill = document.getElementById('progress-fill');
2931
+ if (progressFill) {
2932
+ // Force a reflow to reset the animation
2933
+ progressFill.style.display = 'none';
2934
+ progressFill.offsetHeight; // Trigger reflow
2935
+ progressFill.style.display = 'block';
2936
+ }
2937
+
2938
+ // Reset any spinning icons
2939
+ const spinners = document.querySelectorAll('.fa-spinner.fa-spin');
2940
+ spinners.forEach(spinner => {
2941
+ const parent = spinner.parentElement;
2942
+ if (parent && parent.tagName === 'BUTTON') {
2943
+ if (parent.classList.contains('terminate-btn')) {
2944
+ parent.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
2945
+ parent.disabled = false;
2946
+ }
2947
+ }
2948
+ });
2949
+
2950
+ // Ensure the isTerminating flag is reset
2951
+ isTerminating = false;
2952
+ }
2953
+
2954
+ // Create a dynamic favicon
2955
+ function createDynamicFavicon(emoji = '⚡') {
2956
+ console.log(`Creating dynamic favicon with emoji: ${emoji}`);
2957
+
2958
+ // Create a canvas element
2959
+ const canvas = document.createElement('canvas');
2960
+ canvas.width = 32;
2961
+ canvas.height = 32;
2962
+
2963
+ // Get the canvas context
2964
+ const ctx = canvas.getContext('2d');
2965
+
2966
+ // Clear the canvas with a transparent background
2967
+ ctx.clearRect(0, 0, 32, 32);
2968
+
2969
+ // Set the font size and font family
2970
+ ctx.font = '24px Arial';
2971
+ ctx.textAlign = 'center';
2972
+ ctx.textBaseline = 'middle';
2973
+
2974
+ // Draw the emoji in the center of the canvas
2975
+ ctx.fillText(emoji, 16, 16);
2976
+
2977
+ // Convert canvas to favicon URL
2978
+ const faviconUrl = canvas.toDataURL('image/png');
2979
+
2980
+ // Find existing favicon or create a new one
2981
+ let link = document.querySelector('link[rel="icon"]');
2982
+ if (!link) {
2983
+ link = document.createElement('link');
2984
+ link.rel = 'icon';
2985
+ link.id = 'dynamic-favicon';
2986
+ document.head.appendChild(link);
2987
+ }
2988
+
2989
+ // Update the favicon
2990
+ link.type = 'image/x-icon';
2991
+ link.href = faviconUrl;
2992
+
2993
+ return faviconUrl;
2994
+ }
2995
+
2996
+ // Function to set favicon based on research mode
2997
+ function setFavicon(mode) {
2998
+ const emoji = mode === 'detailed' ? '🔬' : '⚡';
2999
+ return createDynamicFavicon(emoji);
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
+ }
2078
3763
  });