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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +96 -84
  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 +72 -44
  41. local_deep_research/search_system.py +147 -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 +1592 -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 +211 -159
  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.0.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.0.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.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.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,882 @@
1
+ /**
2
+ * Socket Service
3
+ * Manages WebSocket communication using Socket.IO
4
+ */
5
+
6
+ window.socket = (function() {
7
+ let socket = null;
8
+ let researchEventHandlers = {};
9
+ let reconnectCallback = null;
10
+ let connectionAttempts = 0;
11
+ const MAX_CONNECTION_ATTEMPTS = 3;
12
+
13
+ // Keep track of the research we're currently subscribed to
14
+ let currentResearchId = null;
15
+
16
+ // Track if we're using polling fallback
17
+ let usingPolling = false;
18
+
19
+ /**
20
+ * Initialize the Socket.IO connection
21
+ */
22
+ function initializeSocket() {
23
+ if (socket) {
24
+ // Already initialized
25
+ return socket;
26
+ }
27
+
28
+ // Get the base URL from the current page
29
+ const baseUrl = window.location.protocol + '//' + window.location.host;
30
+
31
+ // Create a new socket instance
32
+ try {
33
+ // Use polling only to avoid WebSocket issues
34
+ socket = io(baseUrl, {
35
+ path: '/research/socket.io',
36
+ reconnection: true,
37
+ reconnectionDelay: 1000,
38
+ reconnectionAttempts: 5,
39
+ transports: ['polling'] // Use only polling to avoid WebSocket issues
40
+ });
41
+
42
+ setupSocketEvents();
43
+ console.log('Socket.IO initialized with polling only strategy');
44
+ } catch (error) {
45
+ console.error('Error initializing Socket.IO:', error);
46
+ // Set a flag that we're not connected - will use polling for updates
47
+ usingPolling = true;
48
+ }
49
+
50
+ return socket;
51
+ }
52
+
53
+ /**
54
+ * Set up the socket event handlers
55
+ */
56
+ function setupSocketEvents() {
57
+ socket.on('connect', () => {
58
+ console.log('Socket connected');
59
+ connectionAttempts = 0;
60
+ usingPolling = false;
61
+
62
+ // Re-subscribe to current research if any
63
+ if (currentResearchId) {
64
+ subscribeToResearch(currentResearchId);
65
+ }
66
+
67
+ // Call reconnect callback if exists
68
+ if (reconnectCallback) {
69
+ reconnectCallback();
70
+ }
71
+ });
72
+
73
+ socket.on('connect_error', (error) => {
74
+ console.warn('Socket connection error:', error);
75
+ connectionAttempts++;
76
+
77
+ if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) {
78
+ console.warn(`Failed to connect after ${MAX_CONNECTION_ATTEMPTS} attempts, falling back to polling`);
79
+ usingPolling = true;
80
+
81
+ // If we can't establish a socket connection, use polling for any active research
82
+ if (currentResearchId && typeof window.pollResearchStatus === 'function') {
83
+ window.pollResearchStatus(currentResearchId);
84
+ }
85
+ }
86
+ });
87
+
88
+ socket.on('disconnect', (reason) => {
89
+ console.log('Socket disconnected:', reason);
90
+
91
+ // Fall back to polling on disconnect
92
+ if (currentResearchId) {
93
+ fallbackToPolling(currentResearchId);
94
+ }
95
+ });
96
+
97
+ socket.on('reconnect', (attemptNumber) => {
98
+ console.log('Socket reconnected after', attemptNumber, 'attempts');
99
+ connectionAttempts = 0;
100
+ });
101
+
102
+ socket.on('reconnect_attempt', (attemptNumber) => {
103
+ console.log('Socket reconnection attempt:', attemptNumber);
104
+ });
105
+
106
+ socket.on('error', (error) => {
107
+ console.error('Socket error:', error);
108
+
109
+ // Fall back to polling on any error
110
+ if (currentResearchId) {
111
+ fallbackToPolling(currentResearchId);
112
+ }
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Subscribe to research events
118
+ * @param {string} researchId - The research ID to subscribe to
119
+ * @param {function} callback - Optional callback for progress updates
120
+ */
121
+ function subscribeToResearch(researchId, callback) {
122
+ if (!socket && !usingPolling) {
123
+ console.warn('Socket not initialized, initializing now');
124
+ initializeSocket();
125
+ }
126
+
127
+ if (!researchId) {
128
+ console.error('No research ID provided');
129
+ return;
130
+ }
131
+
132
+ console.log('Subscribing to research:', researchId);
133
+
134
+ // Remember the current research ID
135
+ currentResearchId = researchId;
136
+
137
+ // Add the callback if provided
138
+ if (callback && typeof callback === 'function') {
139
+ addResearchEventHandler(researchId, callback);
140
+ }
141
+
142
+ // If we have a socket connection, join the research room
143
+ if (socket && socket.connected) {
144
+ try {
145
+ socket.emit('join', { research_id: researchId });
146
+
147
+ // Setup direct event handler for progress updates
148
+ socket.on(`progress_${researchId}`, (data) => {
149
+ handleProgressUpdate(researchId, data);
150
+ });
151
+ } catch (error) {
152
+ console.error('Error subscribing to research:', error);
153
+ fallbackToPolling(researchId);
154
+ }
155
+ } else {
156
+ // If no socket connection, use polling
157
+ fallbackToPolling(researchId);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Handle progress updates from research
163
+ * @param {string} researchId - The research ID this update is for
164
+ * @param {Object} data - The progress data
165
+ */
166
+ function handleProgressUpdate(researchId, data) {
167
+ console.log('Progress update for research', researchId, ':', data);
168
+
169
+ // Special handling for synthesis errors to make them more visible to users
170
+ if (data.metadata && (data.metadata.phase === 'synthesis_error' || data.metadata.error_type)) {
171
+ const errorType = data.metadata.error_type || 'unknown';
172
+ let errorMessage = 'Error during research synthesis';
173
+ let detailedMessage = '';
174
+
175
+ // Format user-friendly error messages based on error type
176
+ switch(errorType) {
177
+ case 'timeout':
178
+ errorMessage = 'LLM Timeout Error';
179
+ detailedMessage = 'The AI model took too long to respond. This may be due to server load or the complexity of your query.';
180
+ break;
181
+ case 'token_limit':
182
+ errorMessage = 'Token Limit Exceeded';
183
+ detailedMessage = 'Your research query generated too much data for the AI model to process. Try a more specific query.';
184
+ break;
185
+ case 'connection':
186
+ errorMessage = 'LLM Connection Error';
187
+ detailedMessage = 'Could not connect to the AI service. Please check that your LLM service is running.';
188
+ break;
189
+ case 'rate_limit':
190
+ errorMessage = 'API Rate Limit Reached';
191
+ detailedMessage = 'The AI service API rate limit was reached. Please wait a few minutes and try again.';
192
+ break;
193
+ case 'llm_error':
194
+ default:
195
+ errorMessage = 'LLM Synthesis Error';
196
+ detailedMessage = 'The AI model encountered an error during final answer synthesis. A fallback response will be provided.';
197
+ }
198
+
199
+ // Add prominent error notification
200
+ try {
201
+ // If we have a UI notification function available
202
+ if (typeof window.showNotification === 'function') {
203
+ window.showNotification(errorMessage, detailedMessage, 'error', 10000); // Show for 10 seconds
204
+ }
205
+
206
+ // Log to console
207
+ console.error(`Research error (${errorType}): ${errorMessage} - ${detailedMessage}`);
208
+
209
+ // Add to log panel with the error status
210
+ if (typeof window.addConsoleLog === 'function') {
211
+ window.addConsoleLog(`${errorMessage}: ${detailedMessage}`, 'error', {
212
+ phase: 'synthesis_error',
213
+ error_type: errorType
214
+ });
215
+
216
+ // Add explanation about fallback mode as a separate log entry
217
+ window.addConsoleLog(
218
+ 'Switching to fallback mode. Research will continue with available data.',
219
+ 'milestone',
220
+ {phase: 'synthesis_fallback'}
221
+ );
222
+ }
223
+ } catch (notificationError) {
224
+ console.error('Error showing notification:', notificationError);
225
+ }
226
+ }
227
+
228
+ // Continue with normal progress update handling
229
+ // Call all registered event handlers for this research
230
+ if (researchEventHandlers[researchId]) {
231
+ researchEventHandlers[researchId].forEach(handler => {
232
+ try {
233
+ handler(data);
234
+ } catch (error) {
235
+ console.error('Error in progress update handler:', error);
236
+ }
237
+ });
238
+ }
239
+
240
+ // Initialize message tracking if not exists
241
+ window._processedSocketMessages = window._processedSocketMessages || new Map();
242
+
243
+ // Process logs from progress_log if available
244
+ if (data.progress_log && typeof data.progress_log === 'string') {
245
+ try {
246
+ const progressLogs = JSON.parse(data.progress_log);
247
+ if (Array.isArray(progressLogs) && progressLogs.length > 0) {
248
+ console.log(`Socket received ${progressLogs.length} logs in progress_log`);
249
+
250
+ // Process each log entry
251
+ progressLogs.forEach(logItem => {
252
+ // Skip if no message or time
253
+ if (!logItem.message || !logItem.time) return;
254
+
255
+ // Generate a unique key for this message
256
+ const messageKey = `${logItem.time}-${logItem.message}`;
257
+
258
+ // Skip if we've seen this exact message before
259
+ if (window._processedSocketMessages.has(messageKey)) {
260
+ console.log('Skipping duplicate socket message:', logItem.message);
261
+ return;
262
+ }
263
+
264
+ // Record that we've processed this message
265
+ window._processedSocketMessages.set(messageKey, Date.now());
266
+
267
+ // Determine log type based on metadata
268
+ let logType = 'info';
269
+ if (logItem.metadata) {
270
+ if (logItem.metadata.phase === 'iteration_complete' ||
271
+ logItem.metadata.phase === 'report_complete' ||
272
+ logItem.metadata.phase === 'complete' ||
273
+ logItem.metadata.phase === 'search_complete' ||
274
+ logItem.metadata.is_milestone === true ||
275
+ logItem.metadata.type === 'milestone') {
276
+ logType = 'milestone';
277
+ } else if (logItem.metadata.phase === 'error' ||
278
+ logItem.metadata.type === 'error') {
279
+ logType = 'error';
280
+ }
281
+ }
282
+
283
+ // Also check for keywords in the message for better milestone detection
284
+ if (logType !== 'milestone' && logItem.message) {
285
+ const msg = logItem.message.toLowerCase();
286
+ if (msg.includes('complete') ||
287
+ msg.includes('finished') ||
288
+ msg.includes('starting phase') ||
289
+ msg.includes('generated report')) {
290
+ logType = 'milestone';
291
+ } else if (msg.includes('error') || msg.includes('failed')) {
292
+ logType = 'error';
293
+ }
294
+ }
295
+
296
+ // Send to log panel
297
+ if (typeof window.addConsoleLog === 'function') {
298
+ // Use the main console log function if available
299
+ window.addConsoleLog(logItem.message, logType, logItem.metadata);
300
+ } else if (typeof window._socketAddLogEntry === 'function') {
301
+ // Fallback to the direct connector if needed
302
+ const logEntry = {
303
+ time: logItem.time,
304
+ message: logItem.message,
305
+ type: logType,
306
+ metadata: logItem.metadata || {}
307
+ };
308
+ window._socketAddLogEntry(logEntry);
309
+ } else {
310
+ console.warn('No log handler function available for log:', logItem);
311
+ }
312
+ });
313
+
314
+ // Clean up old entries from message tracking (keep only last 5 minutes)
315
+ const now = Date.now();
316
+ for (const [key, timestamp] of window._processedSocketMessages.entries()) {
317
+ if (now - timestamp > 5 * 60 * 1000) { // 5 minutes
318
+ window._processedSocketMessages.delete(key);
319
+ }
320
+ }
321
+ }
322
+ } catch (error) {
323
+ console.error('Error processing progress_log:', error);
324
+ }
325
+ }
326
+
327
+ // If the event contains log data, add it to the console
328
+ if (data.log_entry) {
329
+ console.log('Adding log entry from socket event:', data.log_entry);
330
+
331
+ // Make sure global tracking is initialized
332
+ window._processedSocketMessages = window._processedSocketMessages || new Map();
333
+
334
+ // Generate a message key
335
+ const messageKey = `${data.log_entry.time || new Date().toISOString()}-${data.log_entry.message}`;
336
+
337
+ // Skip if we've seen this message before
338
+ if (window._processedSocketMessages.has(messageKey)) {
339
+ console.log('Skipping duplicate individual log entry:', data.log_entry.message);
340
+ return;
341
+ }
342
+
343
+ // Record that we've processed this message
344
+ window._processedSocketMessages.set(messageKey, Date.now());
345
+
346
+ if (typeof window.addConsoleLog === 'function') {
347
+ window.addConsoleLog(
348
+ data.log_entry.message,
349
+ data.log_entry.type ||
350
+ (data.log_entry.metadata && data.log_entry.metadata.type) ||
351
+ 'info',
352
+ data.log_entry.metadata
353
+ );
354
+ } else if (typeof window._socketAddLogEntry === 'function') {
355
+ window._socketAddLogEntry(data.log_entry);
356
+ } else {
357
+ console.warn('No log handler function available for direct log entry');
358
+ }
359
+ } else if (data.message && typeof window.addConsoleLog === 'function') {
360
+ // Use the message field if no specific log entry
361
+ console.log('Adding message from socket event:', data.message);
362
+
363
+ // Skip duplicate general messages too
364
+ const messageKey = `${new Date().toISOString()}-${data.message}`;
365
+ if (window._processedSocketMessages.has(messageKey)) {
366
+ console.log('Skipping duplicate message:', data.message);
367
+ return;
368
+ }
369
+
370
+ // Record this message
371
+ window._processedSocketMessages.set(messageKey, Date.now());
372
+
373
+ window.addConsoleLog(data.message, determineLogLevel(data.status));
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Determine log level based on status
379
+ * @param {string} status - The research status
380
+ * @returns {string} Log level (info, milestone, error, etc)
381
+ */
382
+ function determineLogLevel(status) {
383
+ if (!status) return 'info';
384
+
385
+ if (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'error') {
386
+ return 'milestone';
387
+ }
388
+
389
+ if (status === 'error' || status.includes('error')) {
390
+ return 'error';
391
+ }
392
+
393
+ return 'info';
394
+ }
395
+
396
+ /**
397
+ * Add a log entry to the console log container
398
+ * @param {Object} logEntry - The log entry data
399
+ */
400
+ function addLogEntry(logEntry) {
401
+ // If the logpanel's log function is available, use it
402
+ if (typeof window._socketAddLogEntry === 'function') {
403
+ console.log('Using logpanel\'s _socketAddLogEntry for log:', logEntry.message);
404
+ window._socketAddLogEntry(logEntry);
405
+ return;
406
+ }
407
+
408
+ // If window.addConsoleLog is available, use it
409
+ if (typeof window.addConsoleLog === 'function') {
410
+ console.log('Using window.addConsoleLog for log:', logEntry.message);
411
+ let logLevel = 'info';
412
+ if (logEntry.type) {
413
+ logLevel = logEntry.type;
414
+ } else if (logEntry.metadata && logEntry.metadata.type) {
415
+ logLevel = logEntry.metadata.type;
416
+ }
417
+ window.addConsoleLog(logEntry.message, logLevel, logEntry.metadata);
418
+ return;
419
+ }
420
+
421
+ // Fallback implementation if none of the above is available
422
+ console.log('Using socket.js fallback log implementation for:', logEntry.message);
423
+ const consoleLogContainer = document.getElementById('console-log-container');
424
+ if (!consoleLogContainer) return;
425
+
426
+ // Clear empty message if present
427
+ const emptyMessage = consoleLogContainer.querySelector('.empty-log-message');
428
+ if (emptyMessage) {
429
+ emptyMessage.remove();
430
+ }
431
+
432
+ // Get the log template
433
+ const template = document.getElementById('console-log-entry-template');
434
+ if (!template) {
435
+ console.error('Console log entry template not found');
436
+ return;
437
+ }
438
+
439
+ // Create a new log entry from the template
440
+ const entry = document.importNode(template.content, true);
441
+
442
+ // Determine the log level
443
+ let logLevel = 'info';
444
+ if (logEntry.metadata && logEntry.metadata.type) {
445
+ logLevel = logEntry.metadata.type;
446
+ } else if (logEntry.metadata && logEntry.metadata.phase) {
447
+ if (logEntry.metadata.phase === 'complete' ||
448
+ logEntry.metadata.phase === 'iteration_complete' ||
449
+ logEntry.metadata.phase === 'report_complete') {
450
+ logLevel = 'milestone';
451
+ }
452
+ }
453
+
454
+ // Format the timestamp
455
+ const timestamp = new Date(logEntry.time);
456
+ const timeStr = timestamp.toLocaleTimeString();
457
+
458
+ // Set content
459
+ entry.querySelector('.log-timestamp').textContent = timeStr;
460
+ entry.querySelector('.log-badge').textContent = logLevel.charAt(0).toUpperCase() + logLevel.slice(1);
461
+ entry.querySelector('.log-badge').className = `log-badge ${logLevel}`;
462
+ entry.querySelector('.log-message').textContent = logEntry.message;
463
+
464
+ // Add to container (at the beginning for newest first)
465
+ consoleLogContainer.insertBefore(entry, consoleLogContainer.firstChild);
466
+
467
+ // Update log count
468
+ const logIndicator = document.getElementById('log-indicator');
469
+ if (logIndicator) {
470
+ const currentCount = parseInt(logIndicator.textContent) || 0;
471
+ logIndicator.textContent = currentCount + 1;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Fall back to polling for research updates
477
+ * @param {string} researchId - The research ID
478
+ */
479
+ function fallbackToPolling(researchId) {
480
+ console.log('Falling back to polling for research', researchId);
481
+ usingPolling = true;
482
+
483
+ // Start polling if the global polling function exists
484
+ if (typeof window.pollResearchStatus === 'function') {
485
+ window.pollResearchStatus(researchId);
486
+ } else {
487
+ // Define a simple polling function if it doesn't exist
488
+ window.pollResearchStatus = function(id) {
489
+ if (!window.api || !window.api.getResearchStatus) {
490
+ console.error('API service not available for polling');
491
+ return;
492
+ }
493
+
494
+ const pollInterval = setInterval(async () => {
495
+ try {
496
+ const data = await window.api.getResearchStatus(id);
497
+ if (data) {
498
+ handleProgressUpdate(id, data);
499
+
500
+ // Stop polling if the research is complete
501
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
502
+ clearInterval(pollInterval);
503
+ }
504
+ }
505
+ } catch (error) {
506
+ console.error('Error polling research status:', error);
507
+ }
508
+ }, 3000);
509
+
510
+ // Store the interval ID for later cleanup
511
+ window.pollIntervals = window.pollIntervals || {};
512
+ window.pollIntervals[id] = pollInterval;
513
+ };
514
+
515
+ // Start polling for this research
516
+ window.pollResearchStatus(researchId);
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Unsubscribe from research events
522
+ * @param {string} researchId - The research ID to unsubscribe from
523
+ */
524
+ function unsubscribeFromResearch(researchId) {
525
+ if (!researchId) return;
526
+
527
+ console.log('Unsubscribing from research:', researchId);
528
+
529
+ // Clear any polling intervals
530
+ if (window.pollIntervals && window.pollIntervals[researchId]) {
531
+ clearInterval(window.pollIntervals[researchId]);
532
+ delete window.pollIntervals[researchId];
533
+ }
534
+
535
+ // If we have a socket connection, leave the research room
536
+ if (socket && socket.connected) {
537
+ try {
538
+ // Leave the research room
539
+ socket.emit('leave', { research_id: researchId });
540
+
541
+ // Remove the event handler
542
+ socket.off(`progress_${researchId}`);
543
+ } catch (error) {
544
+ console.error('Error unsubscribing from research:', error);
545
+ }
546
+ }
547
+
548
+ // Clear handlers
549
+ if (researchId === currentResearchId) {
550
+ currentResearchId = null;
551
+ }
552
+
553
+ // Clear event handlers
554
+ if (researchEventHandlers[researchId]) {
555
+ delete researchEventHandlers[researchId];
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Add a research event handler
561
+ * @param {string} researchId - The research ID to handle events for
562
+ * @param {function} callback - The function to call when an event occurs
563
+ */
564
+ function addResearchEventHandler(researchId, callback) {
565
+ if (!researchId || typeof callback !== 'function') {
566
+ console.error('Invalid research event handler');
567
+ return;
568
+ }
569
+
570
+ // Initialize the handlers array if needed
571
+ if (!researchEventHandlers[researchId]) {
572
+ researchEventHandlers[researchId] = [];
573
+ }
574
+
575
+ // Add the handler if it's not already in the array
576
+ if (!researchEventHandlers[researchId].includes(callback)) {
577
+ researchEventHandlers[researchId].push(callback);
578
+ }
579
+ }
580
+
581
+ /**
582
+ * Remove a research event handler
583
+ * @param {string} researchId - The research ID to remove handler for
584
+ * @param {function} callback - The function to remove
585
+ */
586
+ function removeResearchEventHandler(researchId, callback) {
587
+ if (!researchId || !researchEventHandlers[researchId]) return;
588
+
589
+ if (callback) {
590
+ // Find the handler index
591
+ const index = researchEventHandlers[researchId].indexOf(callback);
592
+
593
+ // Remove if found
594
+ if (index !== -1) {
595
+ researchEventHandlers[researchId].splice(index, 1);
596
+ }
597
+ } else {
598
+ // Remove all handlers for this research
599
+ delete researchEventHandlers[researchId];
600
+ }
601
+ }
602
+
603
+ /**
604
+ * Set a callback for socket reconnection
605
+ * @param {function} callback - The function to call on reconnection
606
+ */
607
+ function setReconnectCallback(callback) {
608
+ reconnectCallback = callback;
609
+ }
610
+
611
+ /**
612
+ * Disconnect the socket
613
+ */
614
+ function disconnectSocket() {
615
+ // Clear any polling intervals
616
+ if (window.pollIntervals) {
617
+ Object.keys(window.pollIntervals).forEach(id => {
618
+ clearInterval(window.pollIntervals[id]);
619
+ });
620
+ window.pollIntervals = {};
621
+ }
622
+
623
+ if (socket) {
624
+ try {
625
+ socket.disconnect();
626
+ } catch (error) {
627
+ console.error('Error disconnecting socket:', error);
628
+ }
629
+ socket = null;
630
+ }
631
+
632
+ researchEventHandlers = {};
633
+ reconnectCallback = null;
634
+ currentResearchId = null;
635
+ connectionAttempts = 0;
636
+ usingPolling = false;
637
+ }
638
+
639
+ /**
640
+ * Filter logs by type
641
+ * @param {string} type - The log type to filter by ('all', 'info', 'error', 'milestone')
642
+ */
643
+ function filterLogsByType(type) {
644
+ // If the logpanel's filter function is available, use it
645
+ if (typeof window.filterLogsByType === 'function') {
646
+ console.log('Using logpanel\'s filterLogsByType for filter:', type);
647
+ window.filterLogsByType(type);
648
+ return;
649
+ }
650
+
651
+ console.log('Using socket.js filtering implementation for:', type);
652
+
653
+ // Update button UI
654
+ const buttons = document.querySelectorAll('.filter-buttons .small-btn');
655
+ buttons.forEach(button => {
656
+ button.classList.remove('selected');
657
+ if (button.textContent.toLowerCase() === type ||
658
+ (type === 'all' && button.textContent.toLowerCase() === 'all')) {
659
+ button.classList.add('selected');
660
+ }
661
+ });
662
+
663
+ // Get all log entries
664
+ const logEntries = document.querySelectorAll('.console-log-entry');
665
+
666
+ logEntries.forEach(entry => {
667
+ // Use dataset for type if available (new way)
668
+ if (entry.dataset && entry.dataset.logType) {
669
+ if (type === 'all' || entry.dataset.logType === type) {
670
+ entry.style.display = '';
671
+ } else {
672
+ entry.style.display = 'none';
673
+ }
674
+ return;
675
+ }
676
+
677
+ // Fallback to badge content (old way)
678
+ const badge = entry.querySelector('.log-badge');
679
+ const logType = badge ? badge.textContent.toLowerCase() : 'info';
680
+
681
+ if (type === 'all' || logType === type) {
682
+ entry.style.display = '';
683
+ } else {
684
+ entry.style.display = 'none';
685
+ }
686
+ });
687
+
688
+ // Update empty state message if needed
689
+ const logContainer = document.getElementById('console-log-container');
690
+ if (logContainer) {
691
+ const visibleEntries = logContainer.querySelectorAll('.console-log-entry[style="display: ;"], .console-log-entry:not([style])');
692
+ const emptyMessage = logContainer.querySelector('.empty-log-message');
693
+
694
+ if (visibleEntries.length === 0 && !emptyMessage) {
695
+ logContainer.innerHTML = `<div class="empty-log-message">No ${type === 'all' ? '' : type + ' '}logs available.</div>` + logContainer.innerHTML;
696
+ } else if (visibleEntries.length > 0 && emptyMessage) {
697
+ emptyMessage.remove();
698
+ }
699
+ }
700
+ }
701
+
702
+ /**
703
+ * Check if socket is connected
704
+ * @returns {boolean} True if connected
705
+ */
706
+ function isConnected() {
707
+ return socket && socket.connected;
708
+ }
709
+
710
+ /**
711
+ * Check if we're using polling fallback
712
+ * @returns {boolean} True if using polling
713
+ */
714
+ function isUsingPolling() {
715
+ return usingPolling;
716
+ }
717
+
718
+ // Initialize socket on load with a small delay to ensure document is ready
719
+ setTimeout(initializeSocket, 100);
720
+
721
+ // Expose functions globally
722
+ window.filterLogsByType = filterLogsByType;
723
+ window._socketAddLogEntry = addLogEntry; // Expose the addLogEntry function
724
+
725
+ // Public API
726
+ return {
727
+ init: initializeSocket,
728
+ subscribeToResearch,
729
+ unsubscribeFromResearch,
730
+ onReconnect: setReconnectCallback,
731
+ disconnect: disconnectSocket,
732
+ getSocketInstance: () => socket,
733
+ isConnected,
734
+ isUsingPolling
735
+ };
736
+ })();
737
+
738
+ /**
739
+ * Register global functions for filtering logs - ONLY if they don't already exist
740
+ */
741
+ if (!window.filterLogsByType) {
742
+ window.filterLogsByType = function(type) {
743
+ console.log('Filter logs by type (socket.js fallback):', type);
744
+ // If the socket object exists and has the function
745
+ if (window.socket && typeof window.socket.filterLogsByType === 'function') {
746
+ window.socket.filterLogsByType(type);
747
+ return;
748
+ }
749
+
750
+ // Otherwise do basic filtering
751
+ const logEntries = document.querySelectorAll('.console-log-entry');
752
+ logEntries.forEach(entry => {
753
+ // Try using dataset first (new way)
754
+ if (entry.dataset && entry.dataset.logType) {
755
+ if (type === 'all' || entry.dataset.logType === type.toLowerCase()) {
756
+ entry.style.display = '';
757
+ } else {
758
+ entry.style.display = 'none';
759
+ }
760
+ return;
761
+ }
762
+
763
+ // Fallback to badge (old way)
764
+ const badge = entry.querySelector('.log-badge');
765
+ const logType = badge ? badge.textContent.toLowerCase() : 'info';
766
+
767
+ if (type === 'all' || logType === type.toLowerCase()) {
768
+ entry.style.display = '';
769
+ } else {
770
+ entry.style.display = 'none';
771
+ }
772
+ });
773
+ };
774
+ }
775
+
776
+ /**
777
+ * Function to add a log entry to the console - Only create if it doesn't exist
778
+ * @param {string} message - Log message
779
+ * @param {string} level - Log level (info, milestone, error)
780
+ * @param {Object} metadata - Optional metadata
781
+ */
782
+ if (!window.addConsoleLog) {
783
+ window.addConsoleLog = function(message, level = 'info', metadata = null) {
784
+ console.log(`Adding console log (socket.js fallback): ${message} (${level})`);
785
+
786
+ // Create a log entry object
787
+ const logEntry = {
788
+ time: new Date().toISOString(),
789
+ message: message,
790
+ type: level,
791
+ metadata: metadata || { type: level }
792
+ };
793
+
794
+ // Try to use the log panel's direct function first
795
+ if (window.logPanel && typeof window.logPanel.addLog === 'function') {
796
+ console.log('Using logPanel.addLog to add log entry');
797
+ window.logPanel.addLog(message, level, metadata);
798
+ return;
799
+ }
800
+
801
+ // Then try the socket's connector function
802
+ if (window._socketAddLogEntry) {
803
+ console.log('Using _socketAddLogEntry to add log entry');
804
+ window._socketAddLogEntry(logEntry);
805
+ return;
806
+ }
807
+
808
+ console.warn('LogPanel functions not available, using fallback implementation');
809
+
810
+ // FALLBACK IMPLEMENTATION
811
+ const consoleLogContainer = document.getElementById('console-log-container');
812
+ if (!consoleLogContainer) {
813
+ console.warn('Console log container not found, log will be lost');
814
+ return;
815
+ }
816
+
817
+ // Clear empty message if present
818
+ const emptyMessage = consoleLogContainer.querySelector('.empty-log-message');
819
+ if (emptyMessage) {
820
+ emptyMessage.remove();
821
+ }
822
+
823
+ // Get or create a new log entry
824
+ const template = document.getElementById('console-log-entry-template');
825
+ let entry;
826
+
827
+ if (template) {
828
+ entry = document.importNode(template.content, true);
829
+
830
+ // Set content
831
+ entry.querySelector('.log-timestamp').textContent = new Date().toLocaleTimeString();
832
+ entry.querySelector('.log-badge').textContent = level.charAt(0).toUpperCase() + level.slice(1);
833
+ entry.querySelector('.log-badge').className = `log-badge ${level}`;
834
+ entry.querySelector('.log-message').textContent = message;
835
+
836
+ // Add data attribute for filtering
837
+ const logEntry = entry.querySelector('.console-log-entry');
838
+ if (logEntry) {
839
+ logEntry.dataset.logType = level.toLowerCase();
840
+ logEntry.classList.add(`log-${level.toLowerCase()}`);
841
+ }
842
+ } else {
843
+ // Create a simple log entry without template
844
+ entry = document.createElement('div');
845
+ entry.className = 'console-log-entry';
846
+ entry.dataset.logType = level.toLowerCase();
847
+ entry.classList.add(`log-${level.toLowerCase()}`);
848
+
849
+ // Create log content
850
+ entry.innerHTML = `
851
+ <span class="log-timestamp">${new Date().toLocaleTimeString()}</span>
852
+ <span class="log-badge ${level}">${level.charAt(0).toUpperCase() + level.slice(1)}</span>
853
+ <span class="log-message">${message}</span>
854
+ `;
855
+ }
856
+
857
+ // Add to container (at the beginning for newest first)
858
+ consoleLogContainer.insertBefore(entry, consoleLogContainer.firstChild);
859
+
860
+ // Update log count
861
+ const logIndicator = document.getElementById('log-indicator');
862
+ if (logIndicator) {
863
+ const currentCount = parseInt(logIndicator.textContent) || 0;
864
+ logIndicator.textContent = currentCount + 1;
865
+ }
866
+
867
+ // Show log panel if hidden
868
+ const logPanelToggle = document.getElementById('log-panel-toggle');
869
+ const logPanelContent = document.getElementById('log-panel-content');
870
+
871
+ if (logPanelContent && logPanelContent.classList.contains('collapsed') && logPanelToggle) {
872
+ // Auto-expand after a few logs
873
+ if (!window._logAutoExpandTimer) {
874
+ window._logAutoExpandTimer = setTimeout(() => {
875
+ console.log('Auto-expanding log panel due to accumulated logs');
876
+ logPanelToggle.click();
877
+ window._logAutoExpandTimer = null;
878
+ }, 500);
879
+ }
880
+ }
881
+ };
882
+ }