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
@@ -0,0 +1,1107 @@
1
+ /**
2
+ * Progress Component
3
+ * Manages research progress display and updates via Socket.IO
4
+ */
5
+ (function() {
6
+ // Component state
7
+ let currentResearchId = null;
8
+ let pollInterval = null;
9
+ let isCompleted = false;
10
+ let socketErrorShown = false;
11
+
12
+ // DOM Elements
13
+ let progressBar = null;
14
+ let progressPercentage = null;
15
+ let statusText = null;
16
+ let currentTaskText = null;
17
+ let cancelButton = null;
18
+ let viewResultsButton = null;
19
+
20
+ // Socket instance
21
+ let socket = null;
22
+ let reconnectAttempts = 0;
23
+ const MAX_RECONNECT_ATTEMPTS = 5;
24
+ const RECONNECT_DELAY = 3000;
25
+
26
+ // Current research info
27
+ let researchCompleted = false;
28
+ let notificationsEnabled = false;
29
+
30
+ /**
31
+ * Initialize the progress component
32
+ */
33
+ function initializeProgress() {
34
+ // Get research ID from URL or localStorage
35
+ currentResearchId = getResearchIdFromUrl() || localStorage.getItem('currentResearchId');
36
+
37
+ if (!currentResearchId) {
38
+ console.error('No research ID found');
39
+ if (window.ui) window.ui.showError('No active research found. Please start a new research.');
40
+ setTimeout(() => {
41
+ window.location.href = '/';
42
+ }, 3000);
43
+ return;
44
+ }
45
+
46
+ // Get DOM elements
47
+ progressBar = document.getElementById('progress-bar');
48
+ progressPercentage = document.getElementById('progress-percentage');
49
+ statusText = document.getElementById('status-text');
50
+ currentTaskText = document.getElementById('current-task');
51
+ cancelButton = document.getElementById('cancel-research-btn');
52
+ viewResultsButton = document.getElementById('view-results-btn');
53
+
54
+ // Log available elements for debugging
55
+ console.log('Progress DOM elements:', {
56
+ progressBar: !!progressBar,
57
+ progressPercentage: !!progressPercentage,
58
+ statusText: !!statusText,
59
+ currentTaskText: !!currentTaskText,
60
+ cancelButton: !!cancelButton,
61
+ viewResultsButton: !!viewResultsButton
62
+ });
63
+
64
+ // Check for required elements
65
+ const missingElements = [];
66
+ if (!progressBar) missingElements.push('progress-bar');
67
+ if (!statusText) missingElements.push('status-text');
68
+ if (!currentTaskText) missingElements.push('current-task');
69
+
70
+ if (missingElements.length > 0) {
71
+ console.error('Required DOM elements not found for progress component:', missingElements.join(', '));
72
+ // Try to create fallback elements if not found
73
+ createFallbackElements(missingElements);
74
+ }
75
+
76
+ // Set up event listeners
77
+ if (cancelButton) {
78
+ cancelButton.addEventListener('click', handleCancelResearch);
79
+ }
80
+
81
+ // Note: Log panel is now automatically initialized by logpanel.js
82
+ // No need to manually initialize it here
83
+
84
+ // Make sure navigation stays working even if Socket.IO fails
85
+ setupSafeNavigationHandling();
86
+
87
+ // Initialize socket connection if available
88
+ if (window.socket) {
89
+ initializeSocket();
90
+ } else {
91
+ console.warn('Socket service not available, falling back to polling');
92
+ // Set up polling as fallback
93
+ pollInterval = setInterval(checkProgress, 3000);
94
+ }
95
+
96
+ // Initial progress check
97
+ checkProgress();
98
+
99
+ console.log('Progress component initialized for research ID:', currentResearchId);
100
+
101
+ // Get notification preference
102
+ notificationsEnabled = localStorage.getItem('notificationsEnabled') === 'true';
103
+
104
+ // Get initial research status
105
+ getInitialStatus();
106
+ }
107
+
108
+ /**
109
+ * Set up safe navigation handling to prevent WebSocket errors from blocking navigation
110
+ */
111
+ function setupSafeNavigationHandling() {
112
+ // Find all navigation links
113
+ const navLinks = document.querySelectorAll('a, .sidebar-nav li, .mobile-tab-bar li');
114
+
115
+ navLinks.forEach(link => {
116
+ // Don't override existing click handlers, add our handler
117
+ const originalClickHandler = link.onclick;
118
+
119
+ link.onclick = function(event) {
120
+ // If socket has errors, disconnect it before navigation
121
+ if (window.socket && typeof window.socket.isUsingPolling === 'function' && window.socket.isUsingPolling()) {
122
+ console.log('Navigation with polling fallback active, ensuring clean state');
123
+ try {
124
+ // Clean up any polling intervals
125
+ if (window.pollIntervals) {
126
+ Object.keys(window.pollIntervals).forEach(id => {
127
+ clearInterval(window.pollIntervals[id]);
128
+ });
129
+ }
130
+ } catch (e) {
131
+ console.error('Error cleaning up before navigation:', e);
132
+ }
133
+ }
134
+
135
+ // Call the original click handler if it exists
136
+ if (typeof originalClickHandler === 'function') {
137
+ return originalClickHandler.call(this, event);
138
+ }
139
+
140
+ // Default behavior
141
+ return true;
142
+ };
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Create fallback elements if they're missing
148
+ * @param {Array} missingElements - Array of missing element IDs
149
+ */
150
+ function createFallbackElements(missingElements) {
151
+ const progressContainer = document.querySelector('.progress-container');
152
+ const statusContainer = document.querySelector('.status-container');
153
+ const taskContainer = document.querySelector('.task-container');
154
+
155
+ if (missingElements.includes('progress-bar') && progressContainer) {
156
+ console.log('Creating fallback progress bar');
157
+ const progressBarContainer = document.createElement('div');
158
+ progressBarContainer.className = 'progress-bar';
159
+ progressBarContainer.innerHTML = '<div id="progress-bar" class="progress-fill" style="width: 0%"></div>';
160
+ progressContainer.prepend(progressBarContainer);
161
+ progressBar = document.getElementById('progress-bar');
162
+
163
+ if (!progressPercentage) {
164
+ const percentEl = document.createElement('div');
165
+ percentEl.id = 'progress-percentage';
166
+ percentEl.className = 'progress-percentage';
167
+ percentEl.textContent = '0%';
168
+ progressContainer.appendChild(percentEl);
169
+ progressPercentage = percentEl;
170
+ }
171
+ }
172
+
173
+ if (missingElements.includes('status-text') && statusContainer) {
174
+ console.log('Creating fallback status text');
175
+ const statusEl = document.createElement('div');
176
+ statusEl.id = 'status-text';
177
+ statusEl.className = 'status-indicator';
178
+ statusEl.textContent = 'Initializing';
179
+ statusContainer.appendChild(statusEl);
180
+ statusText = statusEl;
181
+ }
182
+
183
+ if (missingElements.includes('current-task') && taskContainer) {
184
+ console.log('Creating fallback task text');
185
+ const taskEl = document.createElement('div');
186
+ taskEl.id = 'current-task';
187
+ taskEl.className = 'task-text';
188
+ taskEl.textContent = 'Starting research...';
189
+ taskContainer.appendChild(taskEl);
190
+ currentTaskText = taskEl;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Extract research ID from URL
196
+ * @returns {string|null} The research ID or null if not found
197
+ */
198
+ function getResearchIdFromUrl() {
199
+ const pathParts = window.location.pathname.split('/');
200
+ const idIndex = pathParts.indexOf('progress') + 1;
201
+
202
+ if (idIndex > 0 && idIndex < pathParts.length) {
203
+ return pathParts[idIndex];
204
+ }
205
+
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * Initialize Socket.IO connection and listeners
211
+ */
212
+ function initializeSocket() {
213
+ try {
214
+ console.log('Initializing socket connection for research ID:', currentResearchId);
215
+
216
+ // Check if socket service is available
217
+ if (!window.socket) {
218
+ console.warn('Socket service not available, falling back to polling');
219
+ // Set up polling as fallback
220
+ fallbackToPolling();
221
+ return;
222
+ }
223
+
224
+ // Subscribe to research events
225
+ window.socket.subscribeToResearch(currentResearchId, handleProgressUpdate);
226
+
227
+ // Handle socket reconnection
228
+ window.socket.onReconnect(() => {
229
+ console.log('Socket reconnected, resubscribing to research events');
230
+ window.socket.subscribeToResearch(currentResearchId, handleProgressUpdate);
231
+ });
232
+
233
+ // Check socket status after a short delay to see if we're connected
234
+ setTimeout(() => {
235
+ if (window.socket.isUsingPolling && window.socket.isUsingPolling()) {
236
+ console.log('Socket using polling fallback');
237
+ if (!socketErrorShown) {
238
+ socketErrorShown = true;
239
+ // Add an info message to the console log if it exists
240
+ if (window.addConsoleLog) {
241
+ window.addConsoleLog('Using polling for updates due to WebSocket connection issues', 'info');
242
+ }
243
+ }
244
+
245
+ // Ensure we check for updates right away
246
+ checkProgress();
247
+ } else {
248
+ console.log('Socket using WebSockets successfully');
249
+ }
250
+ }, 2000);
251
+ } catch (error) {
252
+ console.error('Error initializing socket:', error);
253
+ // Fall back to polling
254
+ fallbackToPolling();
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Fall back to polling for updates
260
+ */
261
+ function fallbackToPolling() {
262
+ console.log('Setting up polling fallback for research updates');
263
+
264
+ if (!pollInterval) {
265
+ pollInterval = setInterval(checkProgress, 3000);
266
+
267
+ // Add a log entry about polling
268
+ if (window.addConsoleLog) {
269
+ window.addConsoleLog('Using polling for research updates instead of WebSockets', 'info');
270
+ }
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Handle progress update from socket
276
+ * @param {Object} data - The progress data
277
+ */
278
+ function handleProgressUpdate(data) {
279
+ console.log('Received progress update:', data);
280
+
281
+ if (!data) return;
282
+
283
+ // Process progress_log if available and add to logs
284
+ // NOTE: This is now handled by the logpanel component directly
285
+ // We'll just ensure the panel is visible and let it manage logs
286
+ if (data.progress_log && typeof data.progress_log === 'string') {
287
+ try {
288
+ // Validate that the progress_log is valid JSON
289
+ const progressLogsCheck = JSON.parse(data.progress_log);
290
+ if (Array.isArray(progressLogsCheck) && progressLogsCheck.length > 0) {
291
+ console.log(`Found ${progressLogsCheck.length} logs in progress update - forwarding to log panel`);
292
+
293
+ // Make the log panel visible if it exists
294
+ const logPanel = document.querySelector('.collapsible-log-panel');
295
+ if (logPanel && window.getComputedStyle(logPanel).display === 'none') {
296
+ logPanel.style.display = 'flex';
297
+ }
298
+
299
+ // The actual log processing is now handled by socket.js and logpanel.js
300
+ // We don't need to process logs here anymore
301
+ }
302
+ } catch (e) {
303
+ console.error('Error checking progress_log format:', e);
304
+ }
305
+ }
306
+
307
+ // Update progress UI
308
+ updateProgressUI(data);
309
+
310
+ // Check if research is completed
311
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
312
+ handleResearchCompletion(data);
313
+ }
314
+
315
+ // Update the current query text if available
316
+ const currentQueryEl = document.getElementById('current-query');
317
+ if (currentQueryEl && localStorage.getItem('currentQuery')) {
318
+ currentQueryEl.textContent = localStorage.getItem('currentQuery');
319
+ }
320
+
321
+ // Check for task message updates with better fallbacks
322
+ let taskUpdated = false;
323
+
324
+ if (data.task_message && data.task_message.trim() !== '') {
325
+ // Direct task message is highest priority
326
+ setCurrentTask(data.task_message);
327
+ taskUpdated = true;
328
+ } else if (data.current_task && data.current_task.trim() !== '') {
329
+ // Then try current_task field
330
+ setCurrentTask(data.current_task);
331
+ taskUpdated = true;
332
+ } else if (data.message && data.message.trim() !== '') {
333
+ // Finally fall back to general message
334
+ // But only if it's informative (not just a status update)
335
+ const msg = data.message.toLowerCase();
336
+ if (!msg.includes('in progress') && !msg.includes('status update')) {
337
+ setCurrentTask(data.message);
338
+ taskUpdated = true;
339
+ }
340
+ }
341
+
342
+ // If no task info was provided, leave the current task as is
343
+ // This prevents tasks from being overwritten by empty updates
344
+ }
345
+
346
+ /**
347
+ * Determine log level based on status
348
+ * @param {string} status - The research status
349
+ * @returns {string} Log level (info, milestone, error, etc)
350
+ */
351
+ function determineLogLevel(status) {
352
+ if (!status) return 'info';
353
+
354
+ if (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'error') {
355
+ return 'milestone';
356
+ }
357
+
358
+ if (status === 'error' || status.includes('error')) {
359
+ return 'error';
360
+ }
361
+
362
+ return 'info';
363
+ }
364
+
365
+ /**
366
+ * Check research progress via API
367
+ */
368
+ async function checkProgress() {
369
+ try {
370
+ if (!window.api || !window.api.getResearchStatus) {
371
+ console.error('API service not available');
372
+ return;
373
+ }
374
+
375
+ console.log('Checking research progress for ID:', currentResearchId);
376
+ const data = await window.api.getResearchStatus(currentResearchId);
377
+
378
+ if (data) {
379
+ console.log('Got research status update:', data);
380
+
381
+ // Update progress UI
382
+ updateProgressUI(data);
383
+
384
+ // Check if research is completed
385
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
386
+ handleResearchCompletion(data);
387
+ } else {
388
+ // Set up polling for status updates as backup for socket
389
+ if (!pollInterval && (!window.socket || (window.socket.isUsingPolling && window.socket.isUsingPolling()))) {
390
+ console.log('Setting up polling interval for progress updates');
391
+ pollInterval = setInterval(checkProgress, 5000);
392
+ }
393
+
394
+ // Log a message every 5th poll to show activity
395
+ if (reconnectAttempts % 5 === 0) {
396
+ console.log('Still monitoring research progress...');
397
+ }
398
+ reconnectAttempts++; // Just using this as a counter for logging
399
+ }
400
+ } else {
401
+ console.warn('No data received from API');
402
+ }
403
+ } catch (error) {
404
+ console.error('Error checking research progress:', error);
405
+ if (statusText) {
406
+ statusText.textContent = 'Error checking research status';
407
+ }
408
+ }
409
+ }
410
+
411
+ /**
412
+ * Update progress bar
413
+ * @param {HTMLElement} progressBar - The progress bar element
414
+ * @param {number} progress - Progress percentage (0-100)
415
+ */
416
+ function updateProgressBar(progressBar, progress) {
417
+ if (!progressBar) return;
418
+
419
+ // Ensure progress is between 0-100
420
+ const percentage = Math.max(0, Math.min(100, Math.floor(progress)));
421
+
422
+ // Update progress bar width with transition for smooth animation
423
+ progressBar.style.transition = 'width 0.3s ease-in-out';
424
+ progressBar.style.width = `${percentage}%`;
425
+
426
+ // Update percentage text if available
427
+ if (progressPercentage) {
428
+ progressPercentage.textContent = `${percentage}%`;
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Update the progress UI with data
434
+ * @param {Object} data - The progress data
435
+ */
436
+ function updateProgressUI(data) {
437
+ console.log('Updating progress UI with data:', data);
438
+
439
+ // Update progress bar
440
+ if (data.progress !== undefined && progressBar) {
441
+ updateProgressBar(progressBar, data.progress);
442
+ }
443
+
444
+ // Update status text with better formatting
445
+ if (data.status && statusText) {
446
+ let formattedStatus;
447
+ if (window.formatting && typeof window.formatting.formatStatus === 'function') {
448
+ formattedStatus = window.formatting.formatStatus(data.status);
449
+ } else {
450
+ // Manual status formatting for better display
451
+ switch (data.status) {
452
+ case 'in_progress':
453
+ // Don't show "In Progress" at all in status text
454
+ return; // Skip status update entirely for in_progress
455
+ case 'completed':
456
+ formattedStatus = 'Completed';
457
+ break;
458
+ case 'failed':
459
+ formattedStatus = 'Failed';
460
+ break;
461
+ case 'cancelled':
462
+ formattedStatus = 'Cancelled';
463
+ break;
464
+ default:
465
+ formattedStatus = data.status.charAt(0).toUpperCase() +
466
+ data.status.slice(1).replace(/_/g, ' ');
467
+ }
468
+ }
469
+
470
+ // Only update status text if we have a non-empty formatted status
471
+ if (formattedStatus && formattedStatus.trim() !== '') {
472
+ statusText.textContent = formattedStatus;
473
+
474
+ // Add status class for styling
475
+ document.querySelectorAll('.status-indicator').forEach(el => {
476
+ el.className = 'status-indicator';
477
+ el.classList.add(`status-${data.status}`);
478
+ });
479
+ }
480
+ }
481
+
482
+ // Extract current task from progress_log
483
+ if (currentTaskText) {
484
+ let taskMessage = null;
485
+
486
+ // Try to parse progress_log to get the latest task
487
+ if (data.progress_log && typeof data.progress_log === 'string') {
488
+ try {
489
+ const progressLogs = JSON.parse(data.progress_log);
490
+ if (Array.isArray(progressLogs) && progressLogs.length > 0) {
491
+ // Get the latest log entry with a non-null message
492
+ for (let i = progressLogs.length - 1; i >= 0; i--) {
493
+ if (progressLogs[i].message && progressLogs[i].message.trim() !== '') {
494
+ taskMessage = progressLogs[i].message;
495
+ break;
496
+ }
497
+ }
498
+ }
499
+ } catch (e) {
500
+ console.error('Error parsing progress_log for task message:', e);
501
+ }
502
+ }
503
+
504
+ // Check various fields that might contain the current task message
505
+ if (!taskMessage) {
506
+ if (data.current_task) {
507
+ taskMessage = data.current_task;
508
+ } else if (data.message) {
509
+ taskMessage = data.message;
510
+ } else if (data.task) {
511
+ taskMessage = data.task;
512
+ } else if (data.step) {
513
+ taskMessage = data.step;
514
+ } else if (data.phase) {
515
+ taskMessage = `Phase: ${data.phase}`;
516
+ } else if (data.log_entry && data.log_entry.message) {
517
+ taskMessage = data.log_entry.message;
518
+ }
519
+ }
520
+
521
+ // Update the task text if we found a message AND it's not just "In Progress"
522
+ if (taskMessage && taskMessage.trim() !== 'In Progress' && taskMessage.trim() !== 'in progress') {
523
+ console.log('Updating current task text to:', taskMessage);
524
+ currentTaskText.textContent = taskMessage;
525
+ // Remember this message to avoid overwriting with generic messages
526
+ currentTaskText.dataset.lastMessage = taskMessage;
527
+ }
528
+
529
+ // If no message but we have a status, generate a more descriptive message
530
+ // BUT ONLY if we don't already have a meaningful message displayed
531
+ if (!taskMessage && data.status && (!currentTaskText.dataset.lastMessage || currentTaskText.textContent === 'In Progress')) {
532
+ let statusMsg;
533
+ switch (data.status) {
534
+ case 'starting':
535
+ statusMsg = 'Starting research process...';
536
+ break;
537
+ case 'searching':
538
+ statusMsg = 'Searching for information...';
539
+ break;
540
+ case 'processing':
541
+ statusMsg = 'Processing search results...';
542
+ break;
543
+ case 'analyzing':
544
+ statusMsg = 'Analyzing gathered information...';
545
+ break;
546
+ case 'writing':
547
+ statusMsg = 'Writing research report...';
548
+ break;
549
+ case 'reviewing':
550
+ statusMsg = 'Reviewing and finalizing report...';
551
+ break;
552
+ case 'in_progress':
553
+ // Don't overwrite existing content with generic "In Progress" message
554
+ if (!currentTaskText.dataset.lastMessage || currentTaskText.textContent === '') {
555
+ statusMsg = 'Performing research...';
556
+ } else {
557
+ statusMsg = null; // Skip update
558
+ }
559
+ break;
560
+ default:
561
+ statusMsg = `${data.status.charAt(0).toUpperCase() + data.status.slice(1).replace('_', ' ')}...`;
562
+ }
563
+
564
+ // Only update if we have a new message
565
+ if (statusMsg) {
566
+ console.log('Using enhanced status-based message:', statusMsg);
567
+ currentTaskText.textContent = statusMsg;
568
+ // Don't remember generic messages
569
+ delete currentTaskText.dataset.lastMessage;
570
+ }
571
+ }
572
+ }
573
+
574
+ // Update page title with progress
575
+ if (data.progress !== undefined) {
576
+ document.title = `Research (${Math.floor(data.progress)}%) - Local Deep Research`;
577
+ }
578
+
579
+ // Update favicon based on status
580
+ if (window.ui && typeof window.ui.updateFavicon === 'function') {
581
+ window.ui.updateFavicon(data.status || 'in_progress');
582
+ }
583
+
584
+ // Show notification if enabled
585
+ if (data.status === 'completed' && localStorage.getItem('notificationsEnabled') === 'true') {
586
+ showNotification('Research Completed', 'Your research has been completed successfully.');
587
+ }
588
+
589
+ // Ensure log entry is added if message exists but no specific log_entry
590
+ if (data.message && window.addConsoleLog && !data.log_entry) {
591
+ console.log('Adding message to console log:', data.message);
592
+ window.addConsoleLog(data.message, determineLogLevel(data.status));
593
+ }
594
+ }
595
+
596
+ /**
597
+ * Handle research completion
598
+ * @param {Object} data - The completion data
599
+ */
600
+ function handleResearchCompletion(data) {
601
+ if (isCompleted) return;
602
+ isCompleted = true;
603
+
604
+ // Clear polling interval
605
+ if (pollInterval) {
606
+ clearInterval(pollInterval);
607
+ pollInterval = null;
608
+ }
609
+
610
+ // Update UI for completion
611
+ if (data.status === 'completed') {
612
+ // Show view results button
613
+ if (viewResultsButton) {
614
+ viewResultsButton.style.display = 'inline-block';
615
+ viewResultsButton.href = `/research/results/${currentResearchId}`;
616
+ }
617
+
618
+ // Hide cancel button
619
+ if (cancelButton) {
620
+ cancelButton.style.display = 'none';
621
+ }
622
+ } else if (data.status === 'failed' || data.status === 'cancelled') {
623
+ // Show error message
624
+ if (window.ui) {
625
+ window.ui.showError(data.error || 'Research was unsuccessful');
626
+ } else {
627
+ console.error('Research failed:', data.error || 'Unknown error');
628
+ }
629
+
630
+ // Update button to go back to home
631
+ if (viewResultsButton) {
632
+ viewResultsButton.textContent = 'Start New Research';
633
+ viewResultsButton.href = '/';
634
+ viewResultsButton.style.display = 'inline-block';
635
+ }
636
+
637
+ // Hide cancel button
638
+ if (cancelButton) {
639
+ cancelButton.style.display = 'none';
640
+ }
641
+ }
642
+ }
643
+
644
+ /**
645
+ * Handle research cancellation
646
+ */
647
+ async function handleCancelResearch() {
648
+ if (!confirm('Are you sure you want to cancel this research?')) {
649
+ return;
650
+ }
651
+
652
+ // Disable cancel button
653
+ if (cancelButton) {
654
+ cancelButton.disabled = true;
655
+ cancelButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Cancelling...';
656
+ }
657
+
658
+ try {
659
+ if (!window.api || !window.api.terminateResearch) {
660
+ throw new Error('API service not available');
661
+ }
662
+
663
+ await window.api.terminateResearch(currentResearchId);
664
+
665
+ // Update status manually (in case socket fails)
666
+ if (statusText) {
667
+ statusText.textContent = 'Cancelled';
668
+ document.querySelectorAll('.status-indicator').forEach(el => {
669
+ el.className = 'status-indicator status-cancelled';
670
+ });
671
+ }
672
+
673
+ // Show message
674
+ if (window.ui) {
675
+ window.ui.showMessage('Research has been cancelled.');
676
+ }
677
+
678
+ // Update cancel button
679
+ if (cancelButton) {
680
+ cancelButton.style.display = 'none';
681
+ }
682
+
683
+ // Show go home button
684
+ if (viewResultsButton) {
685
+ viewResultsButton.textContent = 'Start New Research';
686
+ viewResultsButton.href = '/';
687
+ viewResultsButton.style.display = 'inline-block';
688
+ }
689
+
690
+ } catch (error) {
691
+ console.error('Error cancelling research:', error);
692
+
693
+ // Re-enable cancel button
694
+ if (cancelButton) {
695
+ cancelButton.disabled = false;
696
+ cancelButton.innerHTML = '<i class="fas fa-stop-circle"></i> Cancel Research';
697
+ }
698
+
699
+ // Show error message
700
+ if (window.ui) {
701
+ window.ui.showError('Failed to cancel research. Please try again.');
702
+ }
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Show a notification to the user
708
+ * @param {string} title - Notification title
709
+ * @param {string} message - Notification message
710
+ * @param {string} type - Notification type ('info', 'warning', 'error')
711
+ * @param {number} duration - Duration in ms to show in-app notification (0 to not auto-hide)
712
+ */
713
+ function showNotification(title, message, type = 'info', duration = 5000) {
714
+ // First attempt browser notification if enabled
715
+ if ('Notification' in window) {
716
+ // Check if permission is already granted
717
+ if (Notification.permission === 'granted') {
718
+ try {
719
+ const notification = new Notification(title, {
720
+ body: message,
721
+ icon: type === 'error' ? '/research/static/img/error-icon.png' : '/research/static/img/favicon.png'
722
+ });
723
+
724
+ // Auto-close after 10 seconds
725
+ setTimeout(() => notification.close(), 10000);
726
+ } catch (e) {
727
+ console.warn('Browser notification failed, falling back to in-app notification', e);
728
+ }
729
+ }
730
+ // Otherwise, request permission (only if it's not been denied)
731
+ else if (Notification.permission !== 'denied') {
732
+ Notification.requestPermission().then(permission => {
733
+ if (permission === 'granted') {
734
+ new Notification(title, {
735
+ body: message,
736
+ icon: type === 'error' ? '/research/static/img/error-icon.png' : '/research/static/img/favicon.png'
737
+ });
738
+ }
739
+ });
740
+ }
741
+ }
742
+
743
+ // Also show in-app notification
744
+ try {
745
+ // Create or get notification container
746
+ let notificationContainer = document.getElementById('notification-container');
747
+ if (!notificationContainer) {
748
+ notificationContainer = document.createElement('div');
749
+ notificationContainer.id = 'notification-container';
750
+ notificationContainer.style.position = 'fixed';
751
+ notificationContainer.style.top = '20px';
752
+ notificationContainer.style.right = '20px';
753
+ notificationContainer.style.zIndex = '9999';
754
+ notificationContainer.style.width = '350px';
755
+ document.body.appendChild(notificationContainer);
756
+ }
757
+
758
+ // Create notification element
759
+ const notificationEl = document.createElement('div');
760
+ notificationEl.className = 'alert alert-dismissible fade show';
761
+
762
+ // Set type-specific styling
763
+ switch(type) {
764
+ case 'error':
765
+ notificationEl.classList.add('alert-danger');
766
+ break;
767
+ case 'warning':
768
+ notificationEl.classList.add('alert-warning');
769
+ break;
770
+ default:
771
+ notificationEl.classList.add('alert-info');
772
+ }
773
+
774
+ // Add title and message
775
+ notificationEl.innerHTML = `
776
+ <strong>${title}</strong>
777
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
778
+ <hr>
779
+ <p>${message}</p>
780
+ `;
781
+
782
+ // Add to container
783
+ notificationContainer.appendChild(notificationEl);
784
+
785
+ // Set up auto-dismiss if duration is provided
786
+ if (duration > 0) {
787
+ setTimeout(() => {
788
+ notificationEl.classList.remove('show');
789
+ setTimeout(() => {
790
+ notificationContainer.removeChild(notificationEl);
791
+ }, 300); // Wait for fade animation
792
+ }, duration);
793
+ }
794
+
795
+ // Set up click to dismiss
796
+ notificationEl.querySelector('.btn-close').addEventListener('click', () => {
797
+ notificationEl.classList.remove('show');
798
+ setTimeout(() => {
799
+ if (notificationContainer.contains(notificationEl)) {
800
+ notificationContainer.removeChild(notificationEl);
801
+ }
802
+ }, 300);
803
+ });
804
+
805
+ } catch (e) {
806
+ console.error('Failed to show in-app notification', e);
807
+ }
808
+
809
+ // Also log to console
810
+ const logMethod = type === 'error' ? console.error :
811
+ type === 'warning' ? console.warn : console.log;
812
+ logMethod(`${title}: ${message}`);
813
+ }
814
+
815
+ /**
816
+ * Get initial research status from API
817
+ */
818
+ async function getInitialStatus() {
819
+ try {
820
+ const status = await window.api.getResearchStatus(currentResearchId);
821
+
822
+ // Process status
823
+ if (status) {
824
+ // If complete, show complete UI
825
+ if (status.status === 'completed') {
826
+ handleResearchComplete({ research_id: currentResearchId });
827
+ }
828
+ // If error, show error UI
829
+ else if (status.status === 'error') {
830
+ handleResearchError({
831
+ research_id: currentResearchId,
832
+ error: status.message || 'Unknown error'
833
+ });
834
+ }
835
+ // Otherwise update progress
836
+ else {
837
+ updateProgressUI(status);
838
+ }
839
+ }
840
+ } catch (error) {
841
+ console.error('Error getting initial status:', error);
842
+ setErrorState('Error loading research status. Please refresh the page to try again.');
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Handle research complete event
848
+ * @param {Object} data - Complete event data
849
+ */
850
+ function handleResearchComplete(data) {
851
+ console.log('Research complete received:', data);
852
+
853
+ if (data.research_id != currentResearchId) {
854
+ console.warn('Received complete event for different research ID');
855
+ return;
856
+ }
857
+
858
+ // Update UI
859
+ setProgressValue(100);
860
+ setStatus('completed');
861
+ setCurrentTask('Research completed successfully');
862
+
863
+ // Hide cancel button
864
+ if (cancelButton) {
865
+ cancelButton.style.display = 'none';
866
+ }
867
+
868
+ // Show results button
869
+ showResultsButton();
870
+
871
+ // Show notification if enabled
872
+ showNotification('Research Complete', 'Your research has been completed successfully.');
873
+
874
+ // Update favicon
875
+ updateFavicon(100);
876
+
877
+ // Set flag
878
+ researchCompleted = true;
879
+ }
880
+
881
+ /**
882
+ * Handle research error event
883
+ * @param {Object} data - Error event data
884
+ */
885
+ function handleResearchError(data) {
886
+ console.error('Research error received:', data);
887
+
888
+ if (data.research_id != currentResearchId) {
889
+ console.warn('Received error event for different research ID');
890
+ return;
891
+ }
892
+
893
+ // Update UI to error state
894
+ setProgressValue(100);
895
+ setStatus('error');
896
+ setCurrentTask(`Error: ${data.error || 'Unknown error'}`);
897
+
898
+ // Add error class to progress bar
899
+ if (progressBar) {
900
+ progressBar.classList.remove('bg-primary', 'bg-success');
901
+ progressBar.classList.add('bg-danger');
902
+ }
903
+
904
+ // Hide cancel button
905
+ if (cancelButton) {
906
+ cancelButton.style.display = 'none';
907
+ }
908
+
909
+ // Show results button (might have partial results)
910
+ showResultsButton();
911
+
912
+ // Show notification if enabled
913
+ showNotification('Research Error', `There was an error with your research: ${data.error}`);
914
+
915
+ // Update favicon
916
+ updateFavicon(100, true);
917
+ }
918
+
919
+ /**
920
+ * Set progress bar value
921
+ * @param {number} value - Progress value (0-100)
922
+ */
923
+ function setProgressValue(value) {
924
+ if (!progressBar) return;
925
+
926
+ // Ensure value is in range
927
+ value = Math.min(Math.max(value, 0), 100);
928
+
929
+ // Update progress bar
930
+ progressBar.style.width = `${value}%`;
931
+ progressBar.setAttribute('aria-valuenow', value);
932
+
933
+ // Update classes based on progress
934
+ if (value >= 100) {
935
+ progressBar.classList.remove('bg-primary');
936
+ progressBar.classList.add('bg-success');
937
+ } else {
938
+ progressBar.classList.remove('bg-success', 'bg-danger');
939
+ progressBar.classList.add('bg-primary');
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Set status text
945
+ * @param {string} status - Status string
946
+ */
947
+ function setStatus(status) {
948
+ if (!statusText) return;
949
+
950
+ let statusDisplay = 'Unknown';
951
+
952
+ // Map status to display text
953
+ switch (status) {
954
+ case 'not_started':
955
+ statusDisplay = 'Not Started';
956
+ break;
957
+ case 'in_progress':
958
+ statusDisplay = 'In Progress';
959
+ break;
960
+ case 'completed':
961
+ statusDisplay = 'Completed';
962
+ break;
963
+ case 'cancelled':
964
+ statusDisplay = 'Cancelled';
965
+ break;
966
+ case 'error':
967
+ statusDisplay = 'Error';
968
+ break;
969
+ default:
970
+ statusDisplay = status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown';
971
+ }
972
+
973
+ statusText.textContent = statusDisplay;
974
+ }
975
+
976
+ /**
977
+ * Set current task text
978
+ * @param {string} task - Current task description
979
+ */
980
+ function setCurrentTask(task) {
981
+ if (!currentTaskText) return;
982
+ currentTaskText.textContent = task || 'No active task';
983
+ }
984
+
985
+ /**
986
+ * Set error state for the UI
987
+ * @param {string} message - Error message
988
+ */
989
+ function setErrorState(message) {
990
+ // Update progress UI
991
+ setProgressValue(100);
992
+ setStatus('error');
993
+ setCurrentTask(`Error: ${message}`);
994
+
995
+ // Add error class to progress bar
996
+ if (progressBar) {
997
+ progressBar.classList.remove('bg-primary', 'bg-success');
998
+ progressBar.classList.add('bg-danger');
999
+ }
1000
+
1001
+ // Hide cancel button
1002
+ if (cancelButton) {
1003
+ cancelButton.style.display = 'none';
1004
+ }
1005
+ }
1006
+
1007
+ /**
1008
+ * Show results button
1009
+ */
1010
+ function showResultsButton() {
1011
+ if (!viewResultsButton) return;
1012
+
1013
+ viewResultsButton.style.display = 'inline-block';
1014
+ viewResultsButton.disabled = false;
1015
+ }
1016
+
1017
+ /**
1018
+ * Update favicon with progress
1019
+ * @param {number} progress - Progress value (0-100)
1020
+ * @param {boolean} isError - Whether there is an error
1021
+ */
1022
+ function updateFavicon(progress, isError = false) {
1023
+ try {
1024
+ // Find favicon link or create it if it doesn't exist
1025
+ let link = document.querySelector("link[rel='icon']") ||
1026
+ document.querySelector("link[rel='shortcut icon']");
1027
+
1028
+ if (!link) {
1029
+ // If no favicon link exists, don't try to create it
1030
+ // This avoids error spam in the console
1031
+ console.debug('Favicon link not found, skipping dynamic favicon update');
1032
+ return;
1033
+ }
1034
+
1035
+ // Create canvas for drawing the favicon
1036
+ const canvas = document.createElement('canvas');
1037
+ canvas.width = 32;
1038
+ canvas.height = 32;
1039
+
1040
+ const ctx = canvas.getContext('2d');
1041
+
1042
+ // Draw background
1043
+ ctx.fillStyle = '#343a40'; // Dark background
1044
+ ctx.beginPath();
1045
+ ctx.arc(16, 16, 16, 0, 2 * Math.PI);
1046
+ ctx.fill();
1047
+
1048
+ // Draw progress arc
1049
+ ctx.beginPath();
1050
+ ctx.moveTo(16, 16);
1051
+ ctx.arc(16, 16, 14, -0.5 * Math.PI, (-0.5 + 2 * progress / 100) * Math.PI);
1052
+ ctx.lineTo(16, 16);
1053
+
1054
+ // Color based on status
1055
+ if (isError) {
1056
+ ctx.fillStyle = '#dc3545'; // Danger red
1057
+ } else if (progress >= 100) {
1058
+ ctx.fillStyle = '#28a745'; // Success green
1059
+ } else {
1060
+ ctx.fillStyle = '#007bff'; // Primary blue
1061
+ }
1062
+
1063
+ ctx.fill();
1064
+
1065
+ // Draw center circle
1066
+ ctx.fillStyle = '#343a40';
1067
+ ctx.beginPath();
1068
+ ctx.arc(16, 16, 8, 0, 2 * Math.PI);
1069
+ ctx.fill();
1070
+
1071
+ // Draw letter R
1072
+ ctx.fillStyle = '#ffffff';
1073
+ ctx.font = 'bold 14px Arial';
1074
+ ctx.textAlign = 'center';
1075
+ ctx.textBaseline = 'middle';
1076
+ ctx.fillText('R', 16, 16);
1077
+
1078
+ // Update favicon
1079
+ link.href = canvas.toDataURL('image/png');
1080
+
1081
+ } catch (error) {
1082
+ console.error('Error updating favicon:', error);
1083
+ // Failure to update favicon is not critical, so we just log the error
1084
+ }
1085
+ }
1086
+
1087
+ // Initialize on page load
1088
+ document.addEventListener('DOMContentLoaded', initializeProgress);
1089
+
1090
+ // Expose components publicly for testing and debugging
1091
+ window.progressComponent = {
1092
+ checkProgress,
1093
+ handleCancelResearch
1094
+ };
1095
+
1096
+ // Add global error handler for WebSocket errors
1097
+ window.addEventListener('error', function(event) {
1098
+ if (event.message && event.message.includes('WebSocket') && event.message.includes('frame header')) {
1099
+ console.warn('Caught WebSocket frame header error, suppressing');
1100
+ event.preventDefault();
1101
+ return true; // Prevent the error from showing in console
1102
+ }
1103
+ });
1104
+
1105
+ // Expose notification function globally
1106
+ window.showNotification = showNotification;
1107
+ })();