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,348 @@
1
+ /**
2
+ * Detail Component
3
+ * Manages the display of detailed information for a specific research topic
4
+ */
5
+ (function() {
6
+ // DOM Elements
7
+ let detailContainer = null;
8
+ let sourcesList = null;
9
+ let tabsContainer = null;
10
+
11
+ // Component state
12
+ let currentResearchId = null;
13
+ let currentTopicId = null;
14
+ let detailData = null;
15
+
16
+ /**
17
+ * Initialize the detail component
18
+ */
19
+ function initializeDetail() {
20
+ // Get IDs from URL
21
+ const ids = getIdsFromUrl();
22
+ currentResearchId = ids.researchId;
23
+ currentTopicId = ids.topicId;
24
+
25
+ if (!currentResearchId || !currentTopicId) {
26
+ console.error('No research or topic ID found');
27
+ if (window.ui && window.ui.showError) {
28
+ window.ui.showError('Invalid research or topic. Please return to the results page.');
29
+ }
30
+ return;
31
+ }
32
+
33
+ // Get DOM elements
34
+ detailContainer = document.getElementById('research-log');
35
+
36
+ if (!detailContainer) {
37
+ console.error('Required DOM elements not found for detail component');
38
+ return;
39
+ }
40
+
41
+ // Set up event listeners
42
+ setupEventListeners();
43
+
44
+ // Load topic detail
45
+ loadTopicDetail();
46
+
47
+ console.log('Detail component initialized for research:', currentResearchId, 'topic:', currentTopicId);
48
+ }
49
+
50
+ /**
51
+ * Extract research and topic IDs from URL
52
+ * @returns {Object} Object with researchId and topicId
53
+ */
54
+ function getIdsFromUrl() {
55
+ const pathParts = window.location.pathname.split('/');
56
+ const detailIndex = pathParts.indexOf('detail');
57
+
58
+ if (detailIndex > 0 && detailIndex + 2 < pathParts.length) {
59
+ return {
60
+ researchId: pathParts[detailIndex + 1],
61
+ topicId: pathParts[detailIndex + 2]
62
+ };
63
+ }
64
+
65
+ return { researchId: null, topicId: null };
66
+ }
67
+
68
+ /**
69
+ * Set up event listeners
70
+ */
71
+ function setupEventListeners() {
72
+ // Back button
73
+ const backButton = document.getElementById('back-to-history-from-details');
74
+ if (backButton) {
75
+ backButton.addEventListener('click', function() {
76
+ window.location.href = '/research/history';
77
+ });
78
+ }
79
+
80
+ // Progress elements
81
+ const progressBar = document.getElementById('detail-progress-fill');
82
+ const progressPercentage = document.getElementById('detail-progress-percentage');
83
+
84
+ // Tab click events
85
+ if (tabsContainer) {
86
+ tabsContainer.addEventListener('click', function(e) {
87
+ if (e.target && e.target.matches('.tab-item')) {
88
+ const tabId = e.target.dataset.tab;
89
+ switchTab(tabId);
90
+ }
91
+ });
92
+ }
93
+
94
+ // Source highlight and citation copy
95
+ if (sourcesList) {
96
+ sourcesList.addEventListener('click', function(e) {
97
+ // Handle citation copy
98
+ if (e.target && e.target.matches('.copy-citation-btn')) {
99
+ const sourceId = e.target.closest('.source-item').dataset.id;
100
+ handleCopyCitation(sourceId);
101
+ }
102
+
103
+ // Handle source highlighting
104
+ if (e.target && e.target.closest('.source-item')) {
105
+ const sourceItem = e.target.closest('.source-item');
106
+ if (!e.target.matches('.copy-citation-btn')) {
107
+ toggleSourceHighlight(sourceItem);
108
+ }
109
+ }
110
+ });
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Load topic detail from API
116
+ */
117
+ async function loadTopicDetail() {
118
+ // Show loading state
119
+ window.ui.showSpinner(detailContainer, 'Loading topic details...');
120
+
121
+ try {
122
+ // Get topic detail
123
+ detailData = await window.api.getTopicDetail(currentResearchId, currentTopicId);
124
+
125
+ if (!detailData) {
126
+ throw new Error('No topic details found');
127
+ }
128
+
129
+ // Render detail
130
+ renderDetail(detailData);
131
+ } catch (error) {
132
+ console.error('Error loading topic detail:', error);
133
+ window.ui.hideSpinner(detailContainer);
134
+ window.ui.showError('Error loading topic detail: ' + error.message);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Render topic detail
140
+ * @param {Object} data - The topic detail data
141
+ */
142
+ function renderDetail(data) {
143
+ // Hide spinner
144
+ window.ui.hideSpinner(detailContainer);
145
+
146
+ // Set page title
147
+ document.title = `${data.title || 'Topic Detail'} - Local Deep Research`;
148
+
149
+ // Update page header
150
+ const pageTitle = document.querySelector('h1.page-title');
151
+ if (pageTitle && data.title) {
152
+ pageTitle.textContent = data.title;
153
+ }
154
+
155
+ // Render content
156
+ if (data.content) {
157
+ // Render markdown
158
+ const renderedHtml = window.ui.renderMarkdown(data.content);
159
+ detailContainer.innerHTML = renderedHtml;
160
+
161
+ // Add syntax highlighting
162
+ highlightCodeBlocks();
163
+ } else {
164
+ detailContainer.innerHTML = '<p class="text-error">No content available for this topic.</p>';
165
+ }
166
+
167
+ // Render sources
168
+ if (sourcesList && data.sources && data.sources.length > 0) {
169
+ renderSources(data.sources);
170
+ }
171
+
172
+ // Set sources count
173
+ const sourcesCountEl = document.getElementById('sources-count');
174
+ if (sourcesCountEl && data.sources) {
175
+ sourcesCountEl.textContent = `${data.sources.length} source${data.sources.length !== 1 ? 's' : ''}`;
176
+ }
177
+
178
+ // Show first tab
179
+ if (tabsContainer) {
180
+ const firstTab = tabsContainer.querySelector('.tab-item');
181
+ if (firstTab) {
182
+ switchTab(firstTab.dataset.tab);
183
+ }
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Render sources list
189
+ * @param {Array} sources - Array of source objects
190
+ */
191
+ function renderSources(sources) {
192
+ sourcesList.innerHTML = '';
193
+
194
+ sources.forEach((source, index) => {
195
+ const sourceEl = document.createElement('div');
196
+ sourceEl.className = 'source-item';
197
+ sourceEl.dataset.id = index;
198
+
199
+ const title = source.title || source.url || `Source ${index + 1}`;
200
+ const url = source.url || '';
201
+ const author = source.author || '';
202
+ const date = source.date || '';
203
+
204
+ sourceEl.innerHTML = `
205
+ <div class="source-header">
206
+ <h4 class="source-title">${title}</h4>
207
+ <button class="copy-citation-btn" title="Copy citation">
208
+ <i class="fas fa-clipboard"></i>
209
+ </button>
210
+ </div>
211
+ ${url ? `<div class="source-url"><a href="${url}" target="_blank">${url}</a></div>` : ''}
212
+ <div class="source-metadata">
213
+ ${author ? `<span><i class="fas fa-user"></i> ${author}</span>` : ''}
214
+ ${date ? `<span><i class="far fa-calendar"></i> ${date}</span>` : ''}
215
+ </div>
216
+ ${source.relevance ? `<div class="source-relevance">Relevance: ${source.relevance}</div>` : ''}
217
+ `;
218
+
219
+ sourcesList.appendChild(sourceEl);
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Switch between tabs
225
+ * @param {string} tabId - The tab ID to switch to
226
+ */
227
+ function switchTab(tabId) {
228
+ // Update active tab
229
+ const tabs = document.querySelectorAll('.tab-item');
230
+ tabs.forEach(tab => {
231
+ tab.classList.toggle('active', tab.dataset.tab === tabId);
232
+ });
233
+
234
+ // Update visible content
235
+ const tabContents = document.querySelectorAll('.tab-content');
236
+ tabContents.forEach(content => {
237
+ content.style.display = content.dataset.tab === tabId ? 'block' : 'none';
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Toggle source highlight
243
+ * @param {HTMLElement} sourceEl - The source element to highlight
244
+ */
245
+ function toggleSourceHighlight(sourceEl) {
246
+ // Remove highlight from all sources
247
+ const allSources = document.querySelectorAll('.source-item');
248
+ allSources.forEach(s => s.classList.remove('highlighted'));
249
+
250
+ // Add highlight to clicked source
251
+ sourceEl.classList.add('highlighted');
252
+
253
+ // Get source index
254
+ const sourceIndex = parseInt(sourceEl.dataset.id);
255
+
256
+ // Highlight content with this source
257
+ highlightContentFromSource(sourceIndex);
258
+ }
259
+
260
+ /**
261
+ * Highlight content sections from a specific source
262
+ * @param {number} sourceIndex - The source index to highlight
263
+ */
264
+ function highlightContentFromSource(sourceIndex) {
265
+ // Remove all existing highlights
266
+ const allCitations = document.querySelectorAll('.citation-highlight');
267
+ allCitations.forEach(el => {
268
+ el.classList.remove('citation-highlight');
269
+ });
270
+
271
+ // Add highlight to citations from this source
272
+ const citations = document.querySelectorAll(`.citation[data-source="${sourceIndex}"]`);
273
+ citations.forEach(citation => {
274
+ citation.classList.add('citation-highlight');
275
+
276
+ // Scroll to first citation
277
+ if (citations.length > 0) {
278
+ citations[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
279
+ }
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Handle copying citation
285
+ * @param {number} sourceIndex - The source index to copy
286
+ */
287
+ async function handleCopyCitation(sourceIndex) {
288
+ if (!detailData || !detailData.sources || !detailData.sources[sourceIndex]) {
289
+ return;
290
+ }
291
+
292
+ const source = detailData.sources[sourceIndex];
293
+
294
+ // Format citation
295
+ let citation = '';
296
+
297
+ if (source.author) {
298
+ citation += source.author;
299
+ if (source.date) {
300
+ citation += ` (${source.date})`;
301
+ }
302
+ citation += '. ';
303
+ }
304
+
305
+ if (source.title) {
306
+ citation += `"${source.title}". `;
307
+ }
308
+
309
+ if (source.url) {
310
+ citation += `Retrieved from ${source.url}`;
311
+ }
312
+
313
+ // Copy to clipboard
314
+ try {
315
+ await navigator.clipboard.writeText(citation);
316
+
317
+ // Show tooltip
318
+ const btn = document.querySelector(`.source-item[data-id="${sourceIndex}"] .copy-citation-btn`);
319
+ if (btn) {
320
+ const originalHTML = btn.innerHTML;
321
+ btn.innerHTML = '<i class="fas fa-check"></i>';
322
+
323
+ setTimeout(() => {
324
+ btn.innerHTML = originalHTML;
325
+ }, 2000);
326
+ }
327
+ } catch (error) {
328
+ console.error('Failed to copy citation:', error);
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Apply syntax highlighting to code blocks
334
+ */
335
+ function highlightCodeBlocks() {
336
+ // Check if Prism is available
337
+ if (typeof Prism !== 'undefined') {
338
+ Prism.highlightAllUnder(detailContainer);
339
+ }
340
+ }
341
+
342
+ // Initialize on DOM content loaded
343
+ if (document.readyState === 'loading') {
344
+ document.addEventListener('DOMContentLoaded', initializeDetail);
345
+ } else {
346
+ initializeDetail();
347
+ }
348
+ })();
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Formatting Fallback Utilities
3
+ * Basic implementations of formatting utilities that can be used if the main formatting module is not available
4
+ */
5
+ (function() {
6
+ // Only initialize if window.formatting is not already defined
7
+ if (window.formatting) {
8
+ console.log('Main formatting utilities already available, skipping fallback');
9
+ return;
10
+ }
11
+
12
+ console.log('Initializing fallback formatting utilities');
13
+
14
+ /**
15
+ * Format a date
16
+ * @param {string} dateStr - ISO date string
17
+ * @returns {string} Formatted date
18
+ */
19
+ function formatDate(dateStr) {
20
+ if (!dateStr) return 'N/A';
21
+
22
+ try {
23
+ const date = new Date(dateStr);
24
+ return date.toLocaleString(undefined, {
25
+ year: 'numeric',
26
+ month: 'short',
27
+ day: 'numeric',
28
+ hour: '2-digit',
29
+ minute: '2-digit',
30
+ second: '2-digit'
31
+ });
32
+ } catch (e) {
33
+ console.error('Error formatting date:', e);
34
+ return dateStr;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Format a status string
40
+ * @param {string} status - Status code
41
+ * @returns {string} Formatted status
42
+ */
43
+ function formatStatus(status) {
44
+ if (!status) return 'Unknown';
45
+
46
+ const statusMap = {
47
+ 'in_progress': 'In Progress',
48
+ 'completed': 'Completed',
49
+ 'failed': 'Failed',
50
+ 'suspended': 'Suspended',
51
+ 'pending': 'Pending',
52
+ 'error': 'Error'
53
+ };
54
+
55
+ return statusMap[status] || status;
56
+ }
57
+
58
+ /**
59
+ * Format a mode string
60
+ * @param {string} mode - Mode code
61
+ * @returns {string} Formatted mode
62
+ */
63
+ function formatMode(mode) {
64
+ if (!mode) return 'Unknown';
65
+
66
+ const modeMap = {
67
+ 'quick': 'Quick Summary',
68
+ 'detailed': 'Detailed Report',
69
+ 'standard': 'Standard Research',
70
+ 'advanced': 'Advanced Research'
71
+ };
72
+
73
+ return modeMap[mode] || mode;
74
+ }
75
+
76
+ /**
77
+ * Format duration in seconds to readable text
78
+ * @param {number} seconds - Duration in seconds
79
+ * @returns {string} Formatted duration
80
+ */
81
+ function formatDuration(seconds) {
82
+ if (!seconds && seconds !== 0) return 'N/A';
83
+
84
+ const minutes = Math.floor(seconds / 60);
85
+ const remainingSeconds = seconds % 60;
86
+
87
+ if (minutes === 0) {
88
+ return `${remainingSeconds}s`;
89
+ } else {
90
+ return `${minutes}m ${remainingSeconds}s`;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Format file size in bytes to human readable format
96
+ * @param {number} bytes - Size in bytes
97
+ * @returns {string} Formatted size
98
+ */
99
+ function formatFileSize(bytes) {
100
+ if (!bytes && bytes !== 0) return 'N/A';
101
+
102
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
103
+ let size = bytes;
104
+ let unitIndex = 0;
105
+
106
+ while (size >= 1024 && unitIndex < units.length - 1) {
107
+ size /= 1024;
108
+ unitIndex++;
109
+ }
110
+
111
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
112
+ }
113
+
114
+ // Export utilities to window.formatting
115
+ window.formatting = {
116
+ formatDate,
117
+ formatStatus,
118
+ formatMode,
119
+ formatDuration,
120
+ formatFileSize
121
+ };
122
+ })();
@@ -0,0 +1,215 @@
1
+ /**
2
+ * UI Fallback Utilities
3
+ * Basic implementations of UI utilities that can be used if the main UI module is not available
4
+ */
5
+ (function() {
6
+ // Only initialize if window.ui is not already defined
7
+ if (window.ui) {
8
+ console.log('Main UI utilities already available, skipping fallback');
9
+ return;
10
+ }
11
+
12
+ console.log('Initializing fallback UI utilities');
13
+
14
+ /**
15
+ * Show a loading spinner
16
+ * @param {HTMLElement} container - Container element for spinner
17
+ * @param {string} message - Optional loading message
18
+ */
19
+ function showSpinner(container, message) {
20
+ if (!container) container = document.body;
21
+ const spinnerHtml = `
22
+ <div class="loading-spinner centered">
23
+ <div class="spinner"></div>
24
+ ${message ? `<div class="spinner-message">${message}</div>` : ''}
25
+ </div>
26
+ `;
27
+ container.innerHTML = spinnerHtml;
28
+ }
29
+
30
+ /**
31
+ * Hide loading spinner
32
+ * @param {HTMLElement} container - Container with spinner
33
+ */
34
+ function hideSpinner(container) {
35
+ if (!container) container = document.body;
36
+ const spinner = container.querySelector('.loading-spinner');
37
+ if (spinner) {
38
+ spinner.remove();
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Show an error message
44
+ * @param {string} message - Error message to display
45
+ */
46
+ function showError(message) {
47
+ console.error(message);
48
+
49
+ // Create a notification element
50
+ const notification = document.createElement('div');
51
+ notification.className = 'notification error';
52
+ notification.innerHTML = `
53
+ <i class="fas fa-exclamation-circle"></i>
54
+ <span>${message}</span>
55
+ <button class="close-notification"><i class="fas fa-times"></i></button>
56
+ `;
57
+
58
+ // Add to the page if a notification container exists, otherwise use alert
59
+ const container = document.querySelector('.notifications-container');
60
+ if (container) {
61
+ container.appendChild(notification);
62
+
63
+ // Remove after a delay
64
+ setTimeout(() => {
65
+ notification.classList.add('removing');
66
+ setTimeout(() => notification.remove(), 500);
67
+ }, 5000);
68
+
69
+ // Set up close button
70
+ const closeBtn = notification.querySelector('.close-notification');
71
+ if (closeBtn) {
72
+ closeBtn.addEventListener('click', () => {
73
+ notification.classList.add('removing');
74
+ setTimeout(() => notification.remove(), 500);
75
+ });
76
+ }
77
+ } else {
78
+ // Fallback to alert
79
+ alert(message);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Show a success/info message
85
+ * @param {string} message - Message to display
86
+ */
87
+ function showMessage(message) {
88
+ console.log(message);
89
+
90
+ // Create a notification element
91
+ const notification = document.createElement('div');
92
+ notification.className = 'notification success';
93
+ notification.innerHTML = `
94
+ <i class="fas fa-check-circle"></i>
95
+ <span>${message}</span>
96
+ <button class="close-notification"><i class="fas fa-times"></i></button>
97
+ `;
98
+
99
+ // Add to the page if a notification container exists, otherwise use alert
100
+ const container = document.querySelector('.notifications-container');
101
+ if (container) {
102
+ container.appendChild(notification);
103
+
104
+ // Remove after a delay
105
+ setTimeout(() => {
106
+ notification.classList.add('removing');
107
+ setTimeout(() => notification.remove(), 500);
108
+ }, 5000);
109
+
110
+ // Set up close button
111
+ const closeBtn = notification.querySelector('.close-notification');
112
+ if (closeBtn) {
113
+ closeBtn.addEventListener('click', () => {
114
+ notification.classList.add('removing');
115
+ setTimeout(() => notification.remove(), 500);
116
+ });
117
+ }
118
+ } else {
119
+ // Fallback to alert
120
+ alert(message);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Simple markdown renderer
126
+ * @param {string} markdown - Markdown content
127
+ * @returns {string} HTML content
128
+ */
129
+ function renderMarkdown(markdown) {
130
+ if (!markdown) return '';
131
+
132
+ // This is a very basic markdown renderer for fallback purposes
133
+ let html = markdown;
134
+
135
+ // Convert headers
136
+ html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
137
+ html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
138
+ html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
139
+ html = html.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
140
+ html = html.replace(/^##### (.*$)/gm, '<h5>$1</h5>');
141
+
142
+ // Convert code blocks
143
+ html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
144
+
145
+ // Convert inline code
146
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
147
+
148
+ // Convert bold
149
+ html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
150
+
151
+ // Convert italic
152
+ html = html.replace(/\*(.*?)\*/g, '<em>$1</em>');
153
+
154
+ // Convert links
155
+ html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
156
+
157
+ // Convert paragraphs - this is simplistic
158
+ html = html.replace(/\n\s*\n/g, '</p><p>');
159
+ html = '<p>' + html + '</p>';
160
+
161
+ // Fix potentially broken paragraph tags
162
+ html = html.replace(/<\/p><p><\/p><p>/g, '</p><p>');
163
+ html = html.replace(/<\/p><p><(h[1-5])/g, '</p><$1');
164
+ html = html.replace(/<\/(h[1-5])><p>/g, '</$1>');
165
+
166
+ return html;
167
+ }
168
+
169
+ /**
170
+ * Update favicon to indicate status
171
+ * @param {string} status - Status to indicate (active, complete, error)
172
+ */
173
+ function updateFavicon(status) {
174
+ try {
175
+ const faviconLink = document.querySelector('link[rel="icon"]') ||
176
+ document.querySelector('link[rel="shortcut icon"]');
177
+
178
+ if (!faviconLink) {
179
+ console.warn('Favicon link not found');
180
+ return;
181
+ }
182
+
183
+ let iconPath;
184
+ switch (status) {
185
+ case 'active':
186
+ iconPath = '/research/static/img/favicon-active.ico';
187
+ break;
188
+ case 'complete':
189
+ iconPath = '/research/static/img/favicon-complete.ico';
190
+ break;
191
+ case 'error':
192
+ iconPath = '/research/static/img/favicon-error.ico';
193
+ break;
194
+ default:
195
+ iconPath = '/research/static/img/favicon.ico';
196
+ }
197
+
198
+ // Add cache busting parameter to force reload
199
+ faviconLink.href = iconPath + '?v=' + new Date().getTime();
200
+ console.log('Updated favicon to:', status);
201
+ } catch (error) {
202
+ console.error('Failed to update favicon:', error);
203
+ }
204
+ }
205
+
206
+ // Export utilities to window.ui
207
+ window.ui = {
208
+ showSpinner,
209
+ hideSpinner,
210
+ showError,
211
+ showMessage,
212
+ renderMarkdown,
213
+ updateFavicon
214
+ };
215
+ })();