local-deep-research 0.1.26__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +154 -160
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +87 -45
  41. local_deep_research/search_system.py +153 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1583 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +212 -160
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.2.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,3763 +0,0 @@
1
- // Main application functionality
2
- document.addEventListener('DOMContentLoaded', () => {
3
- // Global socket variable - initialize as null
4
- let socket = null;
5
- let socketConnected = false;
6
-
7
- // Global state variables
8
- let isResearchInProgress = false;
9
- let currentResearchId = null;
10
- window.currentResearchId = null;
11
-
12
- // Polling interval for research status
13
- let pollingInterval = null;
14
-
15
- // Sound notification variables
16
- let successSound = null;
17
- let errorSound = null;
18
- let notificationsEnabled = true;
19
-
20
- // 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
-
44
- // Initialize notification sounds
45
- function initializeSounds() {
46
- successSound = new Audio('/research/static/sounds/success.mp3');
47
- errorSound = new Audio('/research/static/sounds/error.mp3');
48
- successSound.volume = 0.7;
49
- errorSound.volume = 0.7;
50
- }
51
-
52
- // Function to play a notification sound
53
- function playNotificationSound(type) {
54
- console.log(`Attempting to play ${type} notification sound`);
55
- if (!notificationsEnabled) {
56
- console.log('Notifications are disabled');
57
- return;
58
- }
59
-
60
- // Play sounds regardless of tab focus
61
- if (type === 'success' && successSound) {
62
- console.log('Playing success sound');
63
- successSound.play().catch(err => console.error('Error playing success sound:', err));
64
- } else if (type === 'error' && errorSound) {
65
- console.log('Playing error sound');
66
- errorSound.play().catch(err => console.error('Error playing error sound:', err));
67
- } else {
68
- console.warn(`Unknown sound type or sound not initialized: ${type}`);
69
- }
70
- }
71
-
72
- // Initialize socket only when needed with a timeout for safety
73
- function initializeSocket() {
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
- }
89
-
90
- console.log('Initializing socket connection...');
91
- // Create new socket connection with optimized settings for threading mode
92
- socket = io({
93
- path: '/research/socket.io',
94
- transports: ['polling', 'websocket'], // Try polling first, then websocket
95
- reconnection: true,
96
- reconnectionAttempts: 10,
97
- reconnectionDelay: 1000,
98
- timeout: 25000, // Increase timeout further
99
- autoConnect: true,
100
- forceNew: true,
101
- upgrade: false // Disable automatic transport upgrade for more stability
102
- });
103
-
104
- // Add event handlers
105
- socket.on('connect', () => {
106
- console.log('Socket connected');
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
- }
114
- });
115
-
116
- socket.on('disconnect', () => {
117
- console.log('Socket disconnected');
118
- socketConnected = false;
119
- });
120
-
121
- socket.on('connect_error', (error) => {
122
- console.error('Socket connection error:', error);
123
- socketConnected = false;
124
- });
125
-
126
- socket.on('error', (error) => {
127
- console.error('Socket error:', error);
128
- });
129
-
130
- // Set a timeout to detect hanging connections
131
- let connectionTimeoutId = setTimeout(() => {
132
- if (!socketConnected) {
133
- console.log('Socket connection timeout - forcing reconnect');
134
- try {
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
- }
156
- } catch (e) {
157
- console.error('Error during forced reconnect:', e);
158
- // Force create a new socket
159
- socket = null;
160
- socket = initializeSocket();
161
- }
162
- }
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
- });
172
-
173
- return socket;
174
- }
175
-
176
- // Function to safely disconnect socket
177
- window.disconnectSocket = function() {
178
- try {
179
- if (socket) {
180
- console.log('Manually disconnecting socket');
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
196
- socket = null;
197
- socketConnected = false;
198
- }
199
- } catch (e) {
200
- console.error('Error disconnecting socket:', e);
201
- // Ensure socket is nullified even if errors occur
202
- socket = null;
203
- socketConnected = false;
204
- }
205
- };
206
-
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
-
214
- try {
215
- // Check if research is terminated/suspended before connecting
216
- const response = await fetch(getApiUrl(`/api/research/${researchId}`));
217
- const data = await response.json();
218
-
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
- }
232
-
233
- console.log(`Connecting to socket for research ${researchId} (status: ${data.status})`);
234
-
235
- // Initialize socket if it doesn't exist
236
- if (!socket) {
237
- initializeSocket();
238
- }
239
-
240
- // Subscribe to the research channel
241
- if (socket && socket.connected) {
242
- socket.emit('subscribe_to_research', { research_id: researchId });
243
- console.log(`Subscribed to research ${researchId}`);
244
- } else {
245
- console.warn('Socket not connected, waiting for connection...');
246
- // Wait for socket to connect
247
- const maxAttempts = 5;
248
- let attempts = 0;
249
-
250
- const socketConnectInterval = setInterval(() => {
251
- attempts++;
252
- if (socket && socket.connected) {
253
- socket.emit('subscribe_to_research', { research_id: researchId });
254
- console.log(`Subscribed to research ${researchId} after ${attempts} attempts`);
255
- clearInterval(socketConnectInterval);
256
- } else if (attempts >= maxAttempts) {
257
- console.error(`Failed to connect to socket after ${maxAttempts} attempts`);
258
- clearInterval(socketConnectInterval);
259
- addConsoleLog('Failed to connect to real-time updates', 'error');
260
- }
261
- }, 1000);
262
- }
263
- } catch (error) {
264
- console.error(`Error connecting to socket for research ${researchId}:`, error);
265
- }
266
- };
267
-
268
- // Format the research status for display
269
- function formatStatus(status) {
270
- if (!status) return 'Unknown';
271
-
272
- // Handle in_progress specially
273
- if (status === 'in_progress') return 'In Progress';
274
-
275
- // Capitalize first letter for other statuses
276
- return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
277
- }
278
-
279
- // Format the research mode for display
280
- function formatMode(mode) {
281
- if (!mode) return 'Unknown';
282
-
283
- return mode === 'detailed' ? 'Detailed Report' : 'Quick Summary';
284
- }
285
-
286
- // Format a date for display
287
- function formatDate(date, duration = null) {
288
- // Handle null/undefined gracefully
289
- if (!date) return 'Unknown';
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+-]/);
303
-
304
- if (tzIndex !== -1) {
305
- timezone = microsecondPart.substring(tzIndex);
306
- }
307
-
308
- // Use only milliseconds (first 3 digits after dot) or none if format issues
309
- const milliseconds = microsecondPart.substring(0, Math.min(3, tzIndex !== -1 ? tzIndex : microsecondPart.length));
310
-
311
- // Reconstruct with controlled precision
312
- const cleanedDateStr = parts[0] + (milliseconds.length > 0 ? '.' + milliseconds : '') + timezone;
313
- date = new Date(cleanedDateStr);
314
- } else {
315
- date = new Date(date);
316
- }
317
- } else {
318
- date = new Date(date);
319
- }
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;
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
512
- updateNavigationBasedOnResearchStatus();
513
-
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;
532
-
533
- // Update navigation - important for correctly showing/hiding various elements
534
- // based on the current research state
535
- updateNavigationBasedOnResearchStatus();
536
- }
537
-
538
- // Refresh the history list to show the completed research
539
- if (document.getElementById('history').classList.contains('active')) {
540
- loadResearchHistory();
541
- }
542
- }
543
- }
544
- };
545
-
546
- // Check for active research on page load
547
- async function checkActiveResearch() {
548
- try {
549
- const response = await fetch(getApiUrl('/api/history'));
550
- const history = await response.json();
551
-
552
- // Find in-progress research
553
- const activeResearch = history.find(item => item.status === 'in_progress');
554
-
555
- if (activeResearch) {
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
- }
591
-
592
- // If we get here, the research seems to be genuinely active
593
- isResearchInProgress = true;
594
- currentResearchId = activeResearch.id;
595
- window.currentResearchId = currentResearchId;
596
-
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);
612
- }
613
- }
614
- } catch (error) {
615
- console.error('Error checking for active research:', error);
616
- }
617
- }
618
-
619
- // Add unload event listener
620
- window.addEventListener('beforeunload', function() {
621
- window.disconnectSocket();
622
- });
623
-
624
- // Function to start research
625
- async function startResearch(query, mode) {
626
- // First validate that we have a query
627
- if (!query || query.trim() === '') {
628
- alert('Please enter a query');
629
- return;
630
- }
631
-
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
659
- const response = await fetch(getApiUrl('/api/start_research'), {
660
- method: 'POST',
661
- headers: {
662
- 'Content-Type': 'application/json'
663
- },
664
- body: JSON.stringify(payload)
665
- });
666
-
667
- // Parse the response
668
- const result = await response.json();
669
-
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
678
- isResearchInProgress = true;
679
-
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
- }
685
-
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
693
- updateNavigationBasedOnResearchStatus();
694
-
695
- // Navigate to the progress page
696
- switchPage('research-progress');
697
-
698
- // Connect to the socket for this research
699
- window.connectToResearchSocket(currentResearchId);
700
-
701
- // Start polling for status
702
- pollResearchStatus(currentResearchId);
703
- } else {
704
- // Handle error
705
- const errorMessage = result.message || 'Failed to start research';
706
- console.error('Research start error:', errorMessage);
707
-
708
- // Add error to log
709
- addConsoleLog(`Error: ${errorMessage}`, 'error');
710
-
711
- alert(errorMessage);
712
-
713
- // Reset the favicon
714
- setFavicon('default');
715
-
716
- // Reset button state
717
- if (startBtn) {
718
- startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
719
- startBtn.disabled = false;
720
- }
721
- }
722
- } catch (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
-
738
- alert('An error occurred while starting the research. Please try again.');
739
- }
740
- }
741
-
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
752
- function pollResearchStatus(researchId) {
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;
802
- }
803
-
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
- }
922
- }
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
- }
929
- }
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
-
940
- // Main initialization function
941
- function initializeApp() {
942
- console.log('Initializing application...');
943
-
944
- // Initialize the sounds
945
- initializeSounds();
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
-
968
- // Get navigation elements
969
- const navItems = document.querySelectorAll('.sidebar-nav li');
970
- const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
971
- const pages = document.querySelectorAll('.page');
972
- const mobileTabBar = document.querySelector('.mobile-tab-bar');
973
- const logo = document.getElementById('logo-link');
974
-
975
- // Handle responsive navigation based on screen size
976
- function handleResponsiveNavigation() {
977
- // Mobile tab bar should only be visible on small screens
978
- if (window.innerWidth <= 767) {
979
- if (mobileTabBar) {
980
- mobileTabBar.style.display = 'flex';
981
- }
982
- } else {
983
- if (mobileTabBar) {
984
- mobileTabBar.style.display = 'none';
985
- }
986
- }
987
- }
988
-
989
- // Call on initial load
990
- handleResponsiveNavigation();
991
-
992
- // Add resize listener for responsive design
993
- window.addEventListener('resize', handleResponsiveNavigation);
994
-
995
- // Handle logo click
996
- if (logo) {
997
- logo.addEventListener('click', () => {
998
- switchPage('new-research');
999
- resetStartResearchButton();
1000
- });
1001
- }
1002
-
1003
- // Setup navigation click handlers
1004
- navItems.forEach(item => {
1005
- if (!item.classList.contains('external-link')) {
1006
- item.addEventListener('click', function() {
1007
- const pageId = this.dataset.page;
1008
- if (pageId) {
1009
- switchPage(pageId);
1010
- // Reset Start Research button when returning to the form
1011
- if (pageId === 'new-research') {
1012
- resetStartResearchButton();
1013
- }
1014
- }
1015
- });
1016
- }
1017
- });
1018
-
1019
- mobileNavItems.forEach(item => {
1020
- if (!item.classList.contains('external-link')) {
1021
- item.addEventListener('click', function() {
1022
- const pageId = this.dataset.page;
1023
- if (pageId) {
1024
- switchPage(pageId);
1025
- // Reset Start Research button when returning to the form
1026
- if (pageId === 'new-research') {
1027
- resetStartResearchButton();
1028
- }
1029
- }
1030
- });
1031
- }
1032
- });
1033
-
1034
- // Setup form submission
1035
- const researchForm = document.getElementById('research-form');
1036
- if (researchForm) {
1037
- researchForm.addEventListener('submit', function(e) {
1038
- e.preventDefault();
1039
- const query = document.getElementById('query').value.trim();
1040
- if (!query) {
1041
- alert('Please enter a research query');
1042
- return;
1043
- }
1044
-
1045
- const mode = document.querySelector('.mode-option.active')?.dataset.mode || 'quick';
1046
- startResearch(query, mode);
1047
- });
1048
- }
1049
-
1050
- // Initialize research mode selection
1051
- const modeOptions = document.querySelectorAll('.mode-option');
1052
- modeOptions.forEach(option => {
1053
- option.addEventListener('click', function() {
1054
- modeOptions.forEach(opt => opt.classList.remove('active'));
1055
- this.classList.add('active');
1056
-
1057
- // Update favicon based on selected mode
1058
- const mode = this.dataset.mode;
1059
- setFavicon(mode);
1060
- });
1061
- });
1062
-
1063
- // Load research history initially
1064
- if (document.getElementById('history-list')) {
1065
- loadResearchHistory();
1066
- }
1067
-
1068
- // Check for active research
1069
- checkActiveResearch();
1070
-
1071
- // Setup notification toggle and other form elements
1072
- setupResearchForm();
1073
-
1074
- console.log('Application initialized');
1075
- }
1076
-
1077
- // Initialize the app
1078
- initializeApp();
1079
-
1080
- // Function to switch between pages
1081
- function switchPage(pageId) {
1082
- // First hide all pages
1083
- const pages = document.querySelectorAll('.page');
1084
- pages.forEach(page => page.classList.remove('active'));
1085
-
1086
- // Then activate the selected page
1087
- const selectedPage = document.getElementById(pageId);
1088
- if (selectedPage) {
1089
- selectedPage.classList.add('active');
1090
- }
1091
-
1092
- // Update the URL hash
1093
- window.location.hash = '#' + pageId;
1094
-
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
- }
1104
-
1105
- // Special handling for history page
1106
- if (pageId === 'history') {
1107
- loadResearchHistory();
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);
1117
- }
1118
-
1119
- // Track termination status
1120
- let isTerminating = false;
1121
-
1122
- // Check if we're on the history page and load history if needed
1123
- const historyPage = document.getElementById('history');
1124
- if (historyPage && historyPage.classList.contains('active')) {
1125
- // Use setTimeout to ensure the DOM is fully loaded
1126
- setTimeout(() => loadResearchHistory(), 100);
1127
- }
1128
-
1129
- // Add a prefix helper function at the top of the file
1130
- function getApiUrl(path) {
1131
- // This function adds the /research prefix to all API URLs
1132
- return `/research${path}`;
1133
- }
1134
-
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
1162
- async function terminateResearch(researchId) {
1163
- if (!researchId) {
1164
- console.error('No research ID provided for termination');
1165
- return;
1166
- }
1167
-
1168
- // Prevent multiple termination requests
1169
- if (document.getElementById('terminate-research-btn')?.disabled) {
1170
- console.log('Termination already in progress');
1171
- return;
1172
- }
1173
-
1174
- // Confirm with the user
1175
- if (!confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
1176
- return;
1177
- }
1178
-
1179
- console.log(`Attempting to terminate research: ${researchId}`);
1180
-
1181
- try {
1182
- // Get the terminate button
1183
- const terminateBtn = document.getElementById('terminate-research-btn');
1184
- if (terminateBtn) {
1185
- // Disable the button to prevent multiple clicks
1186
- terminateBtn.disabled = true;
1187
- terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
1188
- }
1189
-
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
1211
- const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
1212
- method: 'POST',
1213
- headers: {
1214
- 'Content-Type': 'application/json'
1215
- }
1216
- });
1217
-
1218
- const data = await response.json();
1219
-
1220
- if (data.status === 'success') {
1221
- console.log(`Research ${researchId} termination requested successfully`);
1222
-
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
1291
-
1292
- } else {
1293
- console.error(`Error terminating research: ${data.message}`);
1294
- addConsoleLog(`Error terminating research: ${data.message}`, 'error');
1295
-
1296
- // Re-enable the button
1297
- if (terminateBtn) {
1298
- terminateBtn.disabled = false;
1299
- terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1300
- }
1301
- }
1302
- } catch (error) {
1303
- console.error(`Error in terminate request: ${error}`);
1304
- addConsoleLog(`Error in terminate request: ${error}`, 'error');
1305
-
1306
- // Re-enable the button
1307
- const terminateBtn = document.getElementById('terminate-research-btn');
1308
- if (terminateBtn) {
1309
- terminateBtn.disabled = false;
1310
- terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
1311
- }
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';
1328
- }
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;
1392
- }
1393
- }
1394
-
1395
- // Expose the terminate function to the window object
1396
- window.terminateResearch = terminateResearch;
1397
-
1398
- // Function to update the progress UI
1399
- function updateProgressUI(progress, status, message) {
1400
- const progressFill = document.getElementById('progress-fill');
1401
- const progressPercentage = document.getElementById('progress-percentage');
1402
- const progressStatus = document.getElementById('progress-status');
1403
- const errorMessage = document.getElementById('error-message');
1404
- const tryAgainBtn = document.getElementById('try-again-btn');
1405
-
1406
- if (progressFill && progressPercentage) {
1407
- progressFill.style.width = `${progress}%`;
1408
- progressPercentage.textContent = `${progress}%`;
1409
- }
1410
-
1411
- if (progressStatus && message) {
1412
- progressStatus.textContent = message;
1413
-
1414
- // Update status class
1415
- progressStatus.className = 'progress-status';
1416
- if (status) {
1417
- progressStatus.classList.add(`status-${status}`);
1418
- }
1419
- }
1420
-
1421
- // Show/hide terminate button based on status
1422
- const terminateBtn = document.getElementById('terminate-research-btn');
1423
- if (terminateBtn) {
1424
- if (status === 'in_progress') {
1425
- terminateBtn.style.display = 'inline-flex';
1426
- terminateBtn.disabled = false;
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
- }
1433
- } else if (status === 'terminating') {
1434
- terminateBtn.style.display = 'inline-flex';
1435
- terminateBtn.disabled = true;
1436
- terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
1437
- } else {
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';
1473
- }
1474
- }
1475
- }
1476
-
1477
- // Completely rewritten function to ensure reliable history loading
1478
- async function loadResearchHistory() {
1479
- const historyList = document.getElementById('history-list');
1480
-
1481
- // Make sure we have the history list element
1482
- if (!historyList) {
1483
- console.error('History list element not found');
1484
- return;
1485
- }
1486
-
1487
- historyList.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1488
-
1489
- try {
1490
- const response = await fetch(getApiUrl('/api/history'));
1491
-
1492
- if (!response.ok) {
1493
- throw new Error(`Server returned ${response.status}`);
1494
- }
1495
-
1496
- const data = await response.json();
1497
-
1498
- // Clear the loading spinner
1499
- historyList.innerHTML = '';
1500
-
1501
- // Handle empty data
1502
- if (!data || !Array.isArray(data) || data.length === 0) {
1503
- historyList.innerHTML = '<div class="empty-state">No research history found. Start a new research project!</div>';
1504
- return;
1505
- }
1506
-
1507
- // Check if any research is in progress
1508
- const inProgressResearch = data.find(item => item.status === 'in_progress');
1509
-
1510
- // Get the start research button
1511
- const startResearchBtn = document.getElementById('start-research-btn');
1512
-
1513
- if (inProgressResearch) {
1514
- isResearchInProgress = true;
1515
- currentResearchId = inProgressResearch.id;
1516
- if (startResearchBtn) {
1517
- startResearchBtn.disabled = true;
1518
- }
1519
- } else {
1520
- isResearchInProgress = false;
1521
- if (startResearchBtn) {
1522
- startResearchBtn.disabled = false;
1523
- }
1524
- }
1525
-
1526
- // Display each history item
1527
- data.forEach(item => {
1528
- try {
1529
- // Skip if item is invalid
1530
- if (!item || !item.id) {
1531
- return;
1532
- }
1533
-
1534
- // Create container
1535
- const historyItem = document.createElement('div');
1536
- historyItem.className = 'history-item';
1537
- historyItem.dataset.researchId = item.id;
1538
-
1539
- // Create header with title and status
1540
- const header = document.createElement('div');
1541
- header.className = 'history-item-header';
1542
-
1543
- const title = document.createElement('div');
1544
- title.className = 'history-item-title';
1545
- title.textContent = item.query || 'Untitled Research';
1546
-
1547
- const status = document.createElement('div');
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';
1553
-
1554
- header.appendChild(title);
1555
- header.appendChild(status);
1556
- historyItem.appendChild(header);
1557
-
1558
- // Create meta section
1559
- const meta = document.createElement('div');
1560
- meta.className = 'history-item-meta';
1561
-
1562
- const date = document.createElement('div');
1563
- date.className = 'history-item-date';
1564
- try {
1565
- // Use completed_at if available, fall back to created_at if not
1566
- const dateToUse = item.completed_at || item.created_at;
1567
- date.textContent = dateToUse ? formatDate(new Date(dateToUse)) : 'Unknown date';
1568
- } catch (e) {
1569
- date.textContent = item.completed_at || item.created_at || 'Unknown date';
1570
- }
1571
-
1572
- const mode = document.createElement('div');
1573
- mode.className = 'history-item-mode';
1574
- const modeIcon = item.mode === 'quick' ? 'bolt' : 'microscope';
1575
- const modeText = item.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
1576
- mode.innerHTML = `<i class="fas fa-${modeIcon}"></i> ${modeText}`;
1577
-
1578
- meta.appendChild(date);
1579
- meta.appendChild(mode);
1580
- historyItem.appendChild(meta);
1581
-
1582
- // Create actions section
1583
- const actions = document.createElement('div');
1584
- actions.className = 'history-item-actions';
1585
-
1586
- // View button
1587
- const viewBtn = document.createElement('button');
1588
- viewBtn.className = 'btn btn-sm btn-outline view-btn';
1589
-
1590
- if (item.status === 'completed') {
1591
- viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
1592
- viewBtn.addEventListener('click', (e) => {
1593
- e.stopPropagation();
1594
- loadResearch(item.id);
1595
- });
1596
-
1597
- // PDF button for completed research
1598
- const pdfBtn = document.createElement('button');
1599
- pdfBtn.className = 'btn btn-sm btn-outline pdf-btn';
1600
- pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> PDF';
1601
- pdfBtn.addEventListener('click', (e) => {
1602
- e.stopPropagation();
1603
- generatePdfFromResearch(item.id);
1604
- });
1605
- actions.appendChild(pdfBtn);
1606
- } else if (item.status === 'in_progress') {
1607
- viewBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> In Progress';
1608
- viewBtn.addEventListener('click', (e) => {
1609
- e.stopPropagation();
1610
- navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
1611
- });
1612
- } else {
1613
- viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
1614
- viewBtn.disabled = true;
1615
- }
1616
-
1617
- actions.appendChild(viewBtn);
1618
-
1619
- // Delete button
1620
- const deleteBtn = document.createElement('button');
1621
- deleteBtn.className = 'btn btn-sm btn-outline delete-btn';
1622
- deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
1623
- deleteBtn.addEventListener('click', (e) => {
1624
- e.stopPropagation();
1625
- if (confirm(`Are you sure you want to delete this research: "${item.query}"?`)) {
1626
- deleteResearch(item.id);
1627
- }
1628
- });
1629
- actions.appendChild(deleteBtn);
1630
-
1631
- historyItem.appendChild(actions);
1632
-
1633
- // Add click handler for the entire item
1634
- historyItem.addEventListener('click', (e) => {
1635
- // Skip if clicking on a button
1636
- if (e.target.closest('button')) {
1637
- return;
1638
- }
1639
-
1640
- if (item.status === 'completed') {
1641
- loadResearch(item.id);
1642
- } else if (item.status === 'in_progress') {
1643
- navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
1644
- }
1645
- });
1646
-
1647
- // Add to the history list
1648
- historyList.appendChild(historyItem);
1649
- } catch (itemError) {
1650
- console.error('Error processing history item:', itemError, item);
1651
- }
1652
- });
1653
-
1654
- } catch (error) {
1655
- console.error('Error loading history:', error);
1656
- historyList.innerHTML = `
1657
- <div class="error-message">
1658
- Error loading history: ${error.message}
1659
- </div>
1660
- <div style="text-align: center; margin-top: 1rem;">
1661
- <button id="retry-history-btn" class="btn btn-primary">
1662
- <i class="fas fa-sync"></i> Retry
1663
- </button>
1664
- </div>`;
1665
-
1666
- const retryBtn = document.getElementById('retry-history-btn');
1667
- if (retryBtn) {
1668
- retryBtn.addEventListener('click', () => {
1669
- loadResearchHistory();
1670
- });
1671
- }
1672
- }
1673
-
1674
- // Add a fallback in case something goes wrong and the history list is still empty
1675
- setTimeout(() => {
1676
- if (historyList.innerHTML === '' || historyList.innerHTML.includes('loading-spinner')) {
1677
- console.warn('History list is still empty or showing spinner after load attempt - applying fallback');
1678
- historyList.innerHTML = `
1679
- <div class="error-message">
1680
- Something went wrong while loading the history.
1681
- </div>
1682
- <div style="text-align: center; margin-top: 1rem;">
1683
- <button id="fallback-retry-btn" class="btn btn-primary">
1684
- <i class="fas fa-sync"></i> Retry
1685
- </button>
1686
- </div>`;
1687
-
1688
- const fallbackRetryBtn = document.getElementById('fallback-retry-btn');
1689
- if (fallbackRetryBtn) {
1690
- fallbackRetryBtn.addEventListener('click', () => {
1691
- loadResearchHistory();
1692
- });
1693
- }
1694
- }
1695
- }, 5000); // Check after 5 seconds
1696
- }
1697
-
1698
- // Function to navigate to research progress
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
-
1726
- document.getElementById('current-query').textContent = research.query;
1727
- document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
1728
- document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
1729
-
1730
- // Navigate to progress page
1731
- switchPage('research-progress');
1732
-
1733
- // Load logs for this research
1734
- loadLogsForResearch(research.id);
1735
-
1736
- // Connect to socket for this research
1737
- window.connectToResearchSocket(research.id);
1738
-
1739
- // Start polling for status
1740
- pollResearchStatus(research.id);
1741
- }
1742
-
1743
- // Function to delete a research record
1744
- async function deleteResearch(researchId) {
1745
- try {
1746
- const response = await fetch(getApiUrl(`/api/research/${researchId}/delete`), {
1747
- method: 'DELETE'
1748
- });
1749
-
1750
- if (response.ok) {
1751
- // Reload the history
1752
- loadResearchHistory();
1753
- } else {
1754
- alert('Failed to delete research. Please try again.');
1755
- }
1756
- } catch (error) {
1757
- console.error('Error deleting research:', error);
1758
- alert('An error occurred while deleting the research.');
1759
- }
1760
- }
1761
-
1762
- // Update the loadResearch function to handle terminated/suspended research better
1763
- async function loadResearch(researchId) {
1764
- try {
1765
- console.log(`Loading research results for research ID: ${researchId}`);
1766
-
1767
- // Set the current viewing research ID
1768
- viewingResearchId = researchId;
1769
-
1770
- // Get research data first to check status
1771
- fetch(getApiUrl(`/api/research/${researchId}`))
1772
- .then(response => response.json())
1773
- .then(researchData => {
1774
- // Check if research was terminated or failed
1775
- if (researchData.status === 'suspended' || researchData.status === 'failed') {
1776
- console.log(`Research ${researchId} was ${researchData.status}, not loading results`);
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}`);
1879
- });
1880
- } catch (error) {
1881
- console.error(`Error loading research: ${error}`);
1882
- }
1883
- }
1884
-
1885
- // Function to load research details
1886
- async function loadResearchDetails(researchId) {
1887
- try {
1888
- // Show loading indicators
1889
- document.getElementById('research-log').innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
1890
-
1891
- // Set the current viewing research ID
1892
- viewingResearchId = researchId;
1893
-
1894
- // Fetch the research data
1895
- const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
1896
- const data = await response.json();
1897
-
1898
- if (data.status === 'success') {
1899
- // Update the metadata display
1900
- document.getElementById('detail-query').textContent = data.query || 'Unknown query';
1901
- document.getElementById('detail-status').textContent = formatStatus(data.status);
1902
- document.getElementById('detail-mode').textContent = formatMode(data.mode);
1903
-
1904
- // Update progress percentage
1905
- const progressFill = document.getElementById('detail-progress-fill');
1906
- const progressPercentage = document.getElementById('detail-progress-percentage');
1907
-
1908
- if (progressFill && progressPercentage) {
1909
- const progress = data.progress || 0;
1910
- progressFill.style.width = `${progress}%`;
1911
- progressPercentage.textContent = `${progress}%`;
1912
- }
1913
-
1914
- // Update duration if available
1915
- const metadata = {
1916
- created_at: data.created_at,
1917
- completed_at: data.completed_at
1918
- };
1919
- updateResearchDuration(metadata);
1920
-
1921
- // Render the log entries from the response directly
1922
- if (data.log && Array.isArray(data.log)) {
1923
- renderResearchLog(data.log, researchId);
1924
- } else {
1925
- // Fallback to the dedicated logs endpoint if log data is missing
1926
- try {
1927
- const logResponse = await fetch(getApiUrl(`/api/research/${researchId}/logs`));
1928
- const logData = await logResponse.json();
1929
-
1930
- if (logData.status === 'success' && logData.logs && Array.isArray(logData.logs)) {
1931
- renderResearchLog(logData.logs, researchId);
1932
- } else {
1933
- document.getElementById('research-log').innerHTML = '<div class="empty-state">No log entries available</div>';
1934
- }
1935
- } catch (logError) {
1936
- console.error('Error fetching logs:', logError);
1937
- document.getElementById('research-log').innerHTML = '<div class="empty-state">Failed to load log entries</div>';
1938
- }
1939
- }
1940
-
1941
- // Update detail actions based on research status
1942
- updateDetailActions(data);
1943
-
1944
- // Load logs for the console panel as well
1945
- loadLogsForResearch(researchId);
1946
-
1947
- // Switch to the details page
1948
- switchPage('research-details');
1949
- } else {
1950
- alert('Failed to load research details: ' + (data.message || 'Unknown error'));
1951
- }
1952
- } catch (error) {
1953
- console.error('Error loading research details:', error);
1954
- alert('An error occurred while loading the research details');
1955
- }
1956
- }
1957
-
1958
- // Function to render log entries
1959
- function renderResearchLog(logEntries, researchId) {
1960
- const researchLog = document.getElementById('research-log');
1961
- researchLog.innerHTML = '';
1962
-
1963
- if (!logEntries || logEntries.length === 0) {
1964
- researchLog.innerHTML = '<div class="empty-state">No log entries available.</div>';
1965
- return;
1966
- }
1967
-
1968
- try {
1969
- // Use a document fragment for better performance
1970
- const fragment = document.createDocumentFragment();
1971
- const template = document.getElementById('log-entry-template');
1972
-
1973
- if (!template) {
1974
- console.error('Log entry template not found');
1975
- researchLog.innerHTML = '<div class="error-message">Error rendering log entries: Template not found</div>';
1976
- return;
1977
- }
1978
-
1979
- logEntries.forEach(entry => {
1980
- if (!entry) return; // Skip invalid entries
1981
-
1982
- try {
1983
- const clone = document.importNode(template.content, true);
1984
-
1985
- // Format the timestamp
1986
- let timeStr = 'N/A';
1987
- try {
1988
- if (entry.time) {
1989
- const time = new Date(entry.time);
1990
- timeStr = time.toLocaleTimeString();
1991
- }
1992
- } catch (timeErr) {
1993
- console.warn('Error formatting time:', timeErr);
1994
- }
1995
-
1996
- const timeEl = clone.querySelector('.log-entry-time');
1997
- if (timeEl) timeEl.textContent = timeStr;
1998
-
1999
- // Add message with phase highlighting if available
2000
- const messageEl = clone.querySelector('.log-entry-message');
2001
- if (messageEl) {
2002
- let phaseClass = '';
2003
- if (entry.metadata && entry.metadata.phase) {
2004
- phaseClass = `phase-${entry.metadata.phase}`;
2005
- }
2006
- messageEl.textContent = entry.message || 'No message';
2007
- messageEl.classList.add(phaseClass);
2008
- }
2009
-
2010
- // Add progress information if available
2011
- const progressEl = clone.querySelector('.log-entry-progress');
2012
- if (progressEl) {
2013
- if (entry.progress !== null && entry.progress !== undefined) {
2014
- progressEl.textContent = `Progress: ${entry.progress}%`;
2015
- } else {
2016
- progressEl.textContent = '';
2017
- }
2018
- }
2019
-
2020
- fragment.appendChild(clone);
2021
- } catch (entryError) {
2022
- console.error('Error processing log entry:', entryError, entry);
2023
- // Continue with other entries
2024
- }
2025
- });
2026
-
2027
- researchLog.appendChild(fragment);
2028
-
2029
- // Scroll to the bottom
2030
- researchLog.scrollTop = researchLog.scrollHeight;
2031
- } catch (error) {
2032
- console.error('Error rendering log entries:', error);
2033
- researchLog.innerHTML = '<div class="error-message">Error rendering log entries. Please try again later.</div>';
2034
- }
2035
-
2036
- // Connect to socket for updates if this is an in-progress research
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}`));
2049
- }
2050
- }
2051
-
2052
- // Function to update detail log with a new entry
2053
- function updateDetailLogEntry(logEntry) {
2054
- if (!logEntry || !document.getElementById('research-details').classList.contains('active')) {
2055
- return;
2056
- }
2057
-
2058
- const researchLog = document.getElementById('research-log');
2059
- const template = document.getElementById('log-entry-template');
2060
- const clone = document.importNode(template.content, true);
2061
-
2062
- // Format the timestamp
2063
- const time = new Date(logEntry.time);
2064
- clone.querySelector('.log-entry-time').textContent = time.toLocaleTimeString();
2065
-
2066
- // Add message with phase highlighting if available
2067
- const messageEl = clone.querySelector('.log-entry-message');
2068
- let phaseClass = '';
2069
- if (logEntry.metadata && logEntry.metadata.phase) {
2070
- phaseClass = `phase-${logEntry.metadata.phase}`;
2071
- }
2072
- messageEl.textContent = logEntry.message;
2073
- messageEl.classList.add(phaseClass);
2074
-
2075
- // Add progress information if available
2076
- const progressEl = clone.querySelector('.log-entry-progress');
2077
- if (logEntry.progress !== null && logEntry.progress !== undefined) {
2078
- progressEl.textContent = `Progress: ${logEntry.progress}%`;
2079
-
2080
- // Also update the progress bar in the details view
2081
- document.getElementById('detail-progress-fill').style.width = `${logEntry.progress}%`;
2082
- document.getElementById('detail-progress-percentage').textContent = `${logEntry.progress}%`;
2083
- } else {
2084
- progressEl.textContent = '';
2085
- }
2086
-
2087
- researchLog.appendChild(clone);
2088
-
2089
- // Scroll to the bottom
2090
- researchLog.scrollTop = researchLog.scrollHeight;
2091
- }
2092
-
2093
- // Back to history button handlers - using direct page switching
2094
- document.getElementById('back-to-history')?.addEventListener('click', () => {
2095
- switchPage('history');
2096
- });
2097
-
2098
- document.getElementById('back-to-history-from-details')?.addEventListener('click', () => {
2099
- switchPage('history');
2100
- });
2101
-
2102
- // Helper functions
2103
- function capitalizeFirstLetter(string) {
2104
- return string.charAt(0).toUpperCase() + string.slice(1);
2105
- }
2106
-
2107
- // Function to update progress UI from current research
2108
- function updateProgressFromCurrentResearch() {
2109
- if (!isResearchInProgress || !currentResearchId) return;
2110
-
2111
- // Fetch current status
2112
- fetch(getApiUrl(`/api/research/${currentResearchId}`))
2113
- .then(response => response.json())
2114
- .then(data => {
2115
- document.getElementById('current-query').textContent = data.query || '';
2116
- document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
2117
- document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
2118
-
2119
- // Connect to socket for this research
2120
- window.connectToResearchSocket(currentResearchId);
2121
- })
2122
- .catch(error => {
2123
- console.error('Error fetching research status:', error);
2124
- });
2125
- }
2126
-
2127
- // Function to update the sidebar navigation based on research status
2128
- function updateNavigationBasedOnResearchStatus() {
2129
- const isResearchPage = document.getElementById('research-progress').classList.contains('active');
2130
- const isAnyResearchInProgress = window.currentResearchId !== null && isResearchInProgress;
2131
-
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"]');
2137
-
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');
2157
- }
2158
-
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';
2172
- }
2173
-
2174
- console.log('Updated navigation based on research status. In progress:', isAnyResearchInProgress);
2175
- }
2176
-
2177
- // Function to update the research progress page from nav click
2178
- function updateProgressPage() {
2179
- if (!currentResearchId && !window.currentResearchId) {
2180
- return;
2181
- }
2182
-
2183
- const researchId = currentResearchId || window.currentResearchId;
2184
-
2185
- // Update the progress page
2186
- fetch(getApiUrl(`/api/research/${researchId}`))
2187
- .then(response => response.json())
2188
- .then(data => {
2189
- // Update the query display
2190
- document.getElementById('current-query').textContent = data.query;
2191
-
2192
- // Check status before updating UI
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
2198
- const terminateBtn = document.getElementById('terminate-research-btn');
2199
- if (terminateBtn) {
2200
- terminateBtn.style.display = 'inline-flex';
2201
- terminateBtn.disabled = false;
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}%`;
2222
- }
2223
- })
2224
- .catch(error => {
2225
- console.error('Error fetching research status:', error);
2226
- });
2227
- }
2228
-
2229
- // Function to update a specific history item without reloading the whole list
2230
- function updateHistoryItemStatus(researchId, status, statusText) {
2231
- const historyList = document.getElementById('history-list');
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
-
2241
- // Look for the item in the active research banner
2242
- const activeBanner = historyList.querySelector(`.active-research-banner[data-research-id="${researchId}"]`);
2243
- if (activeBanner) {
2244
- const statusEl = activeBanner.querySelector('.history-item-status');
2245
- if (statusEl) {
2246
- statusEl.textContent = displayStatus;
2247
- statusEl.className = 'history-item-status';
2248
- statusEl.classList.add(statusClass);
2249
- }
2250
-
2251
- // Update buttons
2252
- const terminateBtn = activeBanner.querySelector('.terminate-btn');
2253
- if (terminateBtn) {
2254
- terminateBtn.style.display = 'none';
2255
- }
2256
-
2257
- const viewProgressBtn = activeBanner.querySelector('.view-progress-btn');
2258
- if (viewProgressBtn) {
2259
- if (status === 'suspended') {
2260
- viewProgressBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
2261
- } else if (status === 'failed') {
2262
- viewProgressBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
2263
- }
2264
- viewProgressBtn.disabled = true;
2265
- }
2266
-
2267
- return;
2268
- }
2269
-
2270
- // Look for the item in the regular list
2271
- const historyItem = historyList.querySelector(`.history-item[data-research-id="${researchId}"]`);
2272
- if (historyItem) {
2273
- const statusEl = historyItem.querySelector('.history-item-status');
2274
- if (statusEl) {
2275
- statusEl.textContent = displayStatus;
2276
- statusEl.className = 'history-item-status';
2277
- statusEl.classList.add(statusClass);
2278
- }
2279
-
2280
- // Update view button
2281
- const viewBtn = historyItem.querySelector('.view-btn');
2282
- if (viewBtn) {
2283
- if (status === 'suspended') {
2284
- viewBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
2285
- viewBtn.disabled = true;
2286
- } else if (status === 'failed') {
2287
- viewBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
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
- }
2298
- }
2299
- }
2300
- }
2301
- }
2302
-
2303
- // PDF Generation Functions
2304
- function generatePdf() {
2305
- const resultsContent = document.getElementById('results-content');
2306
- const query = document.getElementById('result-query').textContent;
2307
- const date = document.getElementById('result-date').textContent;
2308
- const mode = document.getElementById('result-mode').textContent;
2309
-
2310
- // Show loading indicator
2311
- const loadingIndicator = document.createElement('div');
2312
- loadingIndicator.className = 'loading-spinner centered';
2313
- loadingIndicator.innerHTML = '<div class="spinner"></div><p style="margin-top: 10px;">Generating PDF...</p>';
2314
- resultsContent.parentNode.insertBefore(loadingIndicator, resultsContent);
2315
- resultsContent.style.display = 'none';
2316
-
2317
- // Create a clone of the content for PDF generation
2318
- const contentClone = resultsContent.cloneNode(true);
2319
- contentClone.style.display = 'block';
2320
- contentClone.style.position = 'absolute';
2321
- contentClone.style.left = '-9999px';
2322
- contentClone.style.width = '800px';
2323
-
2324
- // Apply PDF-specific styling for better readability
2325
- contentClone.style.background = '#ffffff';
2326
- contentClone.style.color = '#333333';
2327
- contentClone.style.padding = '20px';
2328
-
2329
- // Improve visibility by adjusting styles specifically for PDF
2330
- const applyPdfStyles = (element) => {
2331
- // Set all text to dark color for better readability on white background
2332
- element.querySelectorAll('*').forEach(el => {
2333
- // Skip elements that already have inline color styles
2334
- if (!el.style.color) {
2335
- el.style.color = '#333333';
2336
- }
2337
-
2338
- // Fix background colors
2339
- if (el.style.backgroundColor &&
2340
- (el.style.backgroundColor.includes('var(--bg') ||
2341
- el.style.backgroundColor.includes('#121212') ||
2342
- el.style.backgroundColor.includes('#1e1e2d') ||
2343
- el.style.backgroundColor.includes('#2a2a3a'))) {
2344
- el.style.backgroundColor = '#f8f8f8';
2345
- }
2346
-
2347
- // Handle code blocks specifically
2348
- if (el.tagName === 'PRE' || el.tagName === 'CODE') {
2349
- el.style.backgroundColor = '#f5f5f5';
2350
- el.style.border = '1px solid #e0e0e0';
2351
- el.style.color = '#333333';
2352
- }
2353
-
2354
- // Make links visible
2355
- if (el.tagName === 'A') {
2356
- el.style.color = '#0066cc';
2357
- el.style.textDecoration = 'underline';
2358
- }
2359
- });
2360
-
2361
- // Fix specific syntax highlighting elements for PDF
2362
- element.querySelectorAll('.hljs').forEach(hljs => {
2363
- hljs.style.backgroundColor = '#f8f8f8';
2364
- hljs.style.color = '#333333';
2365
-
2366
- // Fix common syntax highlighting colors for PDF
2367
- hljs.querySelectorAll('.hljs-keyword').forEach(el => el.style.color = '#0000cc');
2368
- hljs.querySelectorAll('.hljs-string').forEach(el => el.style.color = '#008800');
2369
- hljs.querySelectorAll('.hljs-number').forEach(el => el.style.color = '#aa0000');
2370
- hljs.querySelectorAll('.hljs-comment').forEach(el => el.style.color = '#888888');
2371
- hljs.querySelectorAll('.hljs-function').forEach(el => el.style.color = '#880000');
2372
- });
2373
- };
2374
-
2375
- document.body.appendChild(contentClone);
2376
-
2377
- // Apply PDF-specific styles
2378
- applyPdfStyles(contentClone);
2379
-
2380
- // Add title and metadata to the PDF content
2381
- const headerDiv = document.createElement('div');
2382
- headerDiv.innerHTML = `
2383
- <h1 style="color: #6e4ff6; font-size: 24px; margin-bottom: 10px;">${query}</h1>
2384
- <div style="margin-bottom: 20px; font-size: 14px; color: #666;">
2385
- <p><strong>Generated:</strong> ${date}</p>
2386
- <p><strong>Mode:</strong> ${mode}</p>
2387
- <p><strong>Source:</strong> Deep Research Lab</p>
2388
- </div>
2389
- <hr style="margin-bottom: 20px; border: 1px solid #eee;">
2390
- `;
2391
- contentClone.insertBefore(headerDiv, contentClone.firstChild);
2392
-
2393
- setTimeout(() => {
2394
- try {
2395
- // Use window.jspdf which is from the UMD bundle
2396
- const { jsPDF } = window.jspdf;
2397
- const pdf = new jsPDF('p', 'pt', 'a4');
2398
- const pdfWidth = pdf.internal.pageSize.getWidth();
2399
- const pdfHeight = pdf.internal.pageSize.getHeight();
2400
- const margin = 40;
2401
- const contentWidth = pdfWidth - 2 * margin;
2402
-
2403
- // Create a more efficient PDF generation approach that keeps text selectable
2404
- const generateTextBasedPDF = async () => {
2405
- try {
2406
- // Get all text elements and handle them differently than images and special content
2407
- const elements = Array.from(contentClone.children);
2408
- let currentY = margin;
2409
- let pageNum = 1;
2410
-
2411
- // Function to add a page with header
2412
- const addPageWithHeader = (pageNum) => {
2413
- if (pageNum > 1) {
2414
- pdf.addPage();
2415
- }
2416
- pdf.setFontSize(8);
2417
- pdf.setTextColor(100, 100, 100);
2418
- pdf.text(`Deep Research - ${query} - Page ${pageNum}`, margin, pdfHeight - 20);
2419
- };
2420
-
2421
- addPageWithHeader(pageNum);
2422
-
2423
- // Process each element
2424
- for (const element of elements) {
2425
- // Simple text content - handled directly by jsPDF
2426
- if ((element.tagName === 'P' || element.tagName === 'DIV') &&
2427
- !element.querySelector('img, canvas, svg') &&
2428
- element.children.length === 0) {
2429
-
2430
- pdf.setFontSize(11);
2431
- pdf.setTextColor(0, 0, 0);
2432
-
2433
- const text = element.textContent.trim();
2434
- if (!text) continue; // Skip empty text
2435
-
2436
- const textLines = pdf.splitTextToSize(text, contentWidth);
2437
-
2438
- // Check if we need a new page
2439
- if (currentY + (textLines.length * 14) > pdfHeight - margin) {
2440
- pageNum++;
2441
- addPageWithHeader(pageNum);
2442
- currentY = margin;
2443
- }
2444
-
2445
- pdf.text(textLines, margin, currentY + 12);
2446
- currentY += (textLines.length * 14) + 10;
2447
- }
2448
- // Handle headings
2449
- else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
2450
- const fontSize = {
2451
- 'H1': 24,
2452
- 'H2': 20,
2453
- 'H3': 16,
2454
- 'H4': 14,
2455
- 'H5': 12,
2456
- 'H6': 11
2457
- }[element.tagName];
2458
-
2459
- // Add heading text as native PDF text
2460
- pdf.setFontSize(fontSize);
2461
-
2462
- // Use a different color for headings to match styling
2463
- if (element.tagName === 'H1') {
2464
- pdf.setTextColor(110, 79, 246); // Purple for main headers
2465
- } else if (element.tagName === 'H2') {
2466
- pdf.setTextColor(70, 90, 150); // Darker blue for H2
2467
- } else {
2468
- pdf.setTextColor(0, 0, 0); // Black for other headings
2469
- }
2470
-
2471
- const text = element.textContent.trim();
2472
- if (!text) continue; // Skip empty headings
2473
-
2474
- const textLines = pdf.splitTextToSize(text, contentWidth);
2475
-
2476
- // Check if we need a new page
2477
- if (currentY + (textLines.length * (fontSize + 4)) > pdfHeight - margin) {
2478
- pageNum++;
2479
- addPageWithHeader(pageNum);
2480
- currentY = margin + 40; // Reset Y position after header
2481
- }
2482
-
2483
- pdf.text(textLines, margin, currentY + fontSize);
2484
- currentY += (textLines.length * (fontSize + 4)) + 10;
2485
-
2486
- // Add a subtle underline for H1 and H2
2487
- if (element.tagName === 'H1' || element.tagName === 'H2') {
2488
- pdf.setDrawColor(110, 79, 246, 0.5);
2489
- pdf.setLineWidth(0.5);
2490
- pdf.line(
2491
- margin,
2492
- currentY - 5,
2493
- margin + Math.min(contentWidth, pdf.getTextWidth(text) * 1.2),
2494
- currentY - 5
2495
- );
2496
- currentY += 5; // Add a bit more space after underlined headings
2497
- }
2498
- }
2499
- // Handle lists
2500
- else if (element.tagName === 'UL' || element.tagName === 'OL') {
2501
- pdf.setFontSize(11);
2502
- pdf.setTextColor(0, 0, 0);
2503
-
2504
- const listItems = element.querySelectorAll('li');
2505
- let itemNumber = 1;
2506
-
2507
- for (const item of listItems) {
2508
- const prefix = element.tagName === 'UL' ? '• ' : `${itemNumber}. `;
2509
- const text = item.textContent.trim();
2510
-
2511
- if (!text) continue; // Skip empty list items
2512
-
2513
- // Split text to fit width, accounting for bullet/number indent
2514
- const textLines = pdf.splitTextToSize(text, contentWidth - 15);
2515
-
2516
- // Check if we need a new page
2517
- if (currentY + (textLines.length * 14) > pdfHeight - margin) {
2518
- pageNum++;
2519
- addPageWithHeader(pageNum);
2520
- currentY = margin;
2521
- }
2522
-
2523
- // Add the bullet/number
2524
- pdf.text(prefix, margin, currentY + 12);
2525
-
2526
- // Add the text with indent
2527
- pdf.text(textLines, margin + 15, currentY + 12);
2528
- currentY += (textLines.length * 14) + 5;
2529
-
2530
- if (element.tagName === 'OL') itemNumber++;
2531
- }
2532
-
2533
- currentY += 5; // Extra space after list
2534
- }
2535
- // Handle code blocks as text
2536
- else if (element.tagName === 'PRE' || element.querySelector('pre')) {
2537
- const codeElement = element.tagName === 'PRE' ? element : element.querySelector('pre');
2538
- const codeText = codeElement.textContent.trim();
2539
-
2540
- if (!codeText) continue; // Skip empty code blocks
2541
-
2542
- // Use monospace font for code
2543
- pdf.setFont("courier", "normal");
2544
- pdf.setFontSize(9); // Smaller font for code
2545
-
2546
- // Calculate code block size
2547
- const codeLines = codeText.split('\n');
2548
- const lineHeight = 10; // Smaller line height for code
2549
- const codeBlockHeight = (codeLines.length * lineHeight) + 20; // Add padding
2550
-
2551
- // Add a background for the code block
2552
- if (currentY + codeBlockHeight > pdfHeight - margin) {
2553
- pageNum++;
2554
- addPageWithHeader(pageNum);
2555
- currentY = margin;
2556
- }
2557
-
2558
- // Draw code block background
2559
- pdf.setFillColor(245, 245, 245); // Light gray background
2560
- pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'F');
2561
-
2562
- // Draw a border
2563
- pdf.setDrawColor(220, 220, 220);
2564
- pdf.setLineWidth(0.5);
2565
- pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'S');
2566
-
2567
- // Add the code text
2568
- pdf.setTextColor(0, 0, 0);
2569
- currentY += 10; // Add padding at top
2570
-
2571
- codeLines.forEach(line => {
2572
- // Handle indentation by preserving leading spaces
2573
- const spacePadding = line.match(/^(\s*)/)[0].length;
2574
- const visibleLine = line.trimLeft();
2575
-
2576
- // Calculate width of space character
2577
- const spaceWidth = pdf.getStringUnitWidth(' ') * 9 / pdf.internal.scaleFactor;
2578
-
2579
- pdf.text(visibleLine, margin + (spacePadding * spaceWidth), currentY);
2580
- currentY += lineHeight;
2581
- });
2582
-
2583
- currentY += 10; // Add padding at bottom
2584
-
2585
- // Reset to normal font
2586
- pdf.setFont("helvetica", "normal");
2587
- pdf.setFontSize(11);
2588
- }
2589
- // Handle tables as text
2590
- else if (element.tagName === 'TABLE' || element.querySelector('table')) {
2591
- const tableElement = element.tagName === 'TABLE' ? element : element.querySelector('table');
2592
-
2593
- if (!tableElement) continue;
2594
-
2595
- // Get table rows
2596
- const rows = Array.from(tableElement.querySelectorAll('tr'));
2597
- if (rows.length === 0) continue;
2598
-
2599
- // Calculate column widths
2600
- const headerCells = Array.from(rows[0].querySelectorAll('th, td'));
2601
- const numColumns = headerCells.length;
2602
-
2603
- if (numColumns === 0) continue;
2604
-
2605
- // Default column width distribution (equal)
2606
- const colWidth = contentWidth / numColumns;
2607
-
2608
- // Start drawing table
2609
- let tableY = currentY + 10;
2610
-
2611
- // Check if we need a new page
2612
- if (tableY + (rows.length * 20) > pdfHeight - margin) {
2613
- pageNum++;
2614
- addPageWithHeader(pageNum);
2615
- tableY = margin + 10;
2616
- currentY = margin;
2617
- }
2618
-
2619
- // Draw table header
2620
- pdf.setFillColor(240, 240, 240);
2621
- pdf.rect(margin, tableY, contentWidth, 20, 'F');
2622
-
2623
- pdf.setFont("helvetica", "bold");
2624
- pdf.setFontSize(10);
2625
- pdf.setTextColor(0, 0, 0);
2626
-
2627
- headerCells.forEach((cell, index) => {
2628
- const text = cell.textContent.trim();
2629
- const x = margin + (index * colWidth) + 5;
2630
- pdf.text(text, x, tableY + 13);
2631
- });
2632
-
2633
- // Draw horizontal line after header
2634
- pdf.setDrawColor(200, 200, 200);
2635
- pdf.setLineWidth(0.5);
2636
- pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
2637
-
2638
- tableY += 20;
2639
-
2640
- // Draw table rows
2641
- pdf.setFont("helvetica", "normal");
2642
- for (let i = 1; i < rows.length; i++) {
2643
- // Check if we need a new page
2644
- if (tableY + 20 > pdfHeight - margin) {
2645
- // Draw bottom border for last row on current page
2646
- pdf.line(margin, tableY, margin + contentWidth, tableY);
2647
-
2648
- // Add new page
2649
- pageNum++;
2650
- addPageWithHeader(pageNum);
2651
- tableY = margin + 10;
2652
-
2653
- // Redraw header on new page
2654
- pdf.setFillColor(240, 240, 240);
2655
- pdf.rect(margin, tableY, contentWidth, 20, 'F');
2656
-
2657
- pdf.setFont("helvetica", "bold");
2658
- headerCells.forEach((cell, index) => {
2659
- const text = cell.textContent.trim();
2660
- const x = margin + (index * colWidth) + 5;
2661
- pdf.text(text, x, tableY + 13);
2662
- });
2663
-
2664
- pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
2665
- tableY += 20;
2666
- pdf.setFont("helvetica", "normal");
2667
- }
2668
-
2669
- // Get cells for this row
2670
- const cells = Array.from(rows[i].querySelectorAll('td, th'));
2671
-
2672
- // Alternate row background for better readability
2673
- if (i % 2 === 0) {
2674
- pdf.setFillColor(250, 250, 250);
2675
- pdf.rect(margin, tableY, contentWidth, 20, 'F');
2676
- }
2677
-
2678
- // Add cell content
2679
- cells.forEach((cell, index) => {
2680
- const text = cell.textContent.trim();
2681
- const x = margin + (index * colWidth) + 5;
2682
- pdf.text(text, x, tableY + 13);
2683
- });
2684
-
2685
- // Draw horizontal line after row
2686
- pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
2687
- tableY += 20;
2688
- }
2689
-
2690
- // Draw vertical lines for columns
2691
- for (let i = 0; i <= numColumns; i++) {
2692
- const x = margin + (i * colWidth);
2693
- pdf.line(x, currentY + 10, x, tableY);
2694
- }
2695
-
2696
- currentY = tableY + 10;
2697
- }
2698
- // Images still need to be handled as images
2699
- else if (element.tagName === 'IMG' || element.querySelector('img')) {
2700
- const imgElement = element.tagName === 'IMG' ? element : element.querySelector('img');
2701
-
2702
- if (!imgElement || !imgElement.src) continue;
2703
-
2704
- try {
2705
- // Create a new image to get dimensions
2706
- const img = new Image();
2707
- img.src = imgElement.src;
2708
-
2709
- // Calculate dimensions
2710
- const imgWidth = contentWidth;
2711
- const imgHeight = img.height * (contentWidth / img.width);
2712
-
2713
- // Check if we need a new page
2714
- if (currentY + imgHeight > pdfHeight - margin) {
2715
- pageNum++;
2716
- addPageWithHeader(pageNum);
2717
- currentY = margin;
2718
- }
2719
-
2720
- // Add image to PDF
2721
- pdf.addImage(img.src, 'JPEG', margin, currentY, imgWidth, imgHeight);
2722
- currentY += imgHeight + 10;
2723
- } catch (imgError) {
2724
- console.error('Error adding image:', imgError);
2725
- pdf.text("[Image could not be rendered]", margin, currentY + 12);
2726
- currentY += 20;
2727
- }
2728
- }
2729
- // Other complex elements still use html2canvas as fallback
2730
- else {
2731
- try {
2732
- const canvas = await html2canvas(element, {
2733
- scale: 2,
2734
- useCORS: true,
2735
- logging: false,
2736
- backgroundColor: '#FFFFFF'
2737
- });
2738
-
2739
- const imgData = canvas.toDataURL('image/png');
2740
- const imgWidth = contentWidth;
2741
- const imgHeight = (canvas.height * contentWidth) / canvas.width;
2742
-
2743
- if (currentY + imgHeight > pdfHeight - margin) {
2744
- pageNum++;
2745
- addPageWithHeader(pageNum);
2746
- currentY = margin;
2747
- }
2748
-
2749
- pdf.addImage(imgData, 'PNG', margin, currentY, imgWidth, imgHeight);
2750
- currentY += imgHeight + 10;
2751
- } catch (canvasError) {
2752
- console.error('Error rendering complex element:', canvasError);
2753
- pdf.text("[Complex content could not be rendered]", margin, currentY + 12);
2754
- currentY += 20;
2755
- }
2756
- }
2757
- }
2758
-
2759
- // Download the PDF
2760
- const filename = `${query.replace(/[^a-z0-9]/gi, '_').substring(0, 30).toLowerCase()}_research.pdf`;
2761
- pdf.save(filename);
2762
-
2763
- // Clean up
2764
- document.body.removeChild(contentClone);
2765
- resultsContent.style.display = 'block';
2766
- loadingIndicator.remove();
2767
- } catch (error) {
2768
- console.error('Error generating PDF:', error);
2769
- alert('An error occurred while generating the PDF. Please try again.');
2770
- document.body.removeChild(contentClone);
2771
- resultsContent.style.display = 'block';
2772
- loadingIndicator.remove();
2773
- }
2774
- };
2775
-
2776
- generateTextBasedPDF();
2777
- } catch (error) {
2778
- console.error('Error initializing PDF generation:', error);
2779
- alert('An error occurred while preparing the PDF. Please try again.');
2780
- document.body.removeChild(contentClone);
2781
- resultsContent.style.display = 'block';
2782
- loadingIndicator.remove();
2783
- }
2784
- }, 100);
2785
- }
2786
-
2787
- // Function to generate PDF from a specific research ID
2788
- async function generatePdfFromResearch(researchId) {
2789
- try {
2790
- // Load research details
2791
- const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
2792
- const details = await detailsResponse.json();
2793
-
2794
- // Load the report content
2795
- const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
2796
- const reportData = await reportResponse.json();
2797
-
2798
- if (reportData.status === 'success') {
2799
- // Create a temporary container to render the content
2800
- const tempContainer = document.createElement('div');
2801
- tempContainer.className = 'results-content pdf-optimized';
2802
- tempContainer.style.display = 'none';
2803
- document.body.appendChild(tempContainer);
2804
-
2805
- // Render markdown with optimized styles for PDF
2806
- const renderedContent = marked.parse(reportData.content);
2807
- tempContainer.innerHTML = renderedContent;
2808
-
2809
- // Apply syntax highlighting
2810
- tempContainer.querySelectorAll('pre code').forEach((block) => {
2811
- hljs.highlightElement(block);
2812
- });
2813
-
2814
- // Format date with duration
2815
- let dateText = formatDate(
2816
- new Date(details.completed_at || details.created_at),
2817
- details.duration_seconds
2818
- );
2819
-
2820
- // Set up data for PDF generation
2821
- document.getElementById('result-query').textContent = details.query;
2822
- document.getElementById('result-date').textContent = dateText;
2823
- document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
2824
-
2825
- // Replace the current content with our temporary content
2826
- const resultsContent = document.getElementById('results-content');
2827
- const originalContent = resultsContent.innerHTML;
2828
- resultsContent.innerHTML = tempContainer.innerHTML;
2829
-
2830
- // Generate the PDF
2831
- generatePdf();
2832
-
2833
- // Restore original content if we're not on the results page
2834
- setTimeout(() => {
2835
- if (!document.getElementById('research-results').classList.contains('active')) {
2836
- resultsContent.innerHTML = originalContent;
2837
- }
2838
- document.body.removeChild(tempContainer);
2839
- }, 500);
2840
-
2841
- } else {
2842
- alert('Error loading report. Could not generate PDF.');
2843
- }
2844
- } catch (error) {
2845
- console.error('Error generating PDF:', error);
2846
- alert('An error occurred while generating the PDF. Please try again.');
2847
- }
2848
- }
2849
-
2850
- // Initialize the terminate button event listener
2851
- const terminateBtn = document.getElementById('terminate-research-btn');
2852
- if (terminateBtn) {
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.');
2859
- }
2860
- });
2861
- }
2862
-
2863
- // Initialize PDF download button
2864
- const downloadPdfBtn = document.getElementById('download-pdf-btn');
2865
- if (downloadPdfBtn) {
2866
- downloadPdfBtn.addEventListener('click', generatePdf);
2867
- }
2868
-
2869
- // Function to set up the research form
2870
- function setupResearchForm() {
2871
- const researchForm = document.getElementById('research-form');
2872
- const notificationToggle = document.getElementById('notification-toggle');
2873
-
2874
- // Set notification state from toggle
2875
- if (notificationToggle) {
2876
- notificationsEnabled = notificationToggle.checked;
2877
-
2878
- // Listen for changes to the toggle
2879
- notificationToggle.addEventListener('change', function() {
2880
- notificationsEnabled = this.checked;
2881
- // Store preference in localStorage for persistence
2882
- localStorage.setItem('notificationsEnabled', notificationsEnabled);
2883
- });
2884
-
2885
- // Load saved preference from localStorage
2886
- const savedPref = localStorage.getItem('notificationsEnabled');
2887
- if (savedPref !== null) {
2888
- notificationsEnabled = savedPref === 'true';
2889
- notificationToggle.checked = notificationsEnabled;
2890
- }
2891
- }
2892
-
2893
- // ... existing form setup ...
2894
- }
2895
-
2896
- // Add event listener for view results button
2897
- const viewResultsBtn = document.getElementById('view-results-btn');
2898
- if (viewResultsBtn) {
2899
- viewResultsBtn.addEventListener('click', () => {
2900
- console.log('View results button clicked');
2901
- if (currentResearchId) {
2902
- loadResearch(currentResearchId);
2903
- } else {
2904
- console.error('No research ID available');
2905
- }
2906
- });
2907
- }
2908
-
2909
- // Add listener for research_completed custom event
2910
- document.addEventListener('research_completed', (event) => {
2911
- console.log('Research completed event received:', event.detail);
2912
- const data = event.detail;
2913
-
2914
- // Mark research as no longer in progress
2915
- isResearchInProgress = false;
2916
-
2917
- // Hide terminate button
2918
- const terminateBtn = document.getElementById('terminate-research-btn');
2919
- if (terminateBtn) {
2920
- terminateBtn.style.display = 'none';
2921
- }
2922
-
2923
- // Update navigation
2924
- updateNavigationBasedOnResearchStatus();
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
- }
3763
- });