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,766 @@
1
+ /**
2
+ * Results Component
3
+ * Handles the display of research results
4
+ */
5
+ (function() {
6
+ // DOM Elements
7
+ let resultsContainer = null;
8
+ let exportBtn = null;
9
+ let pdfBtn = null;
10
+ let researchId = null;
11
+ let researchData = null;
12
+
13
+ /**
14
+ * Initialize the results component
15
+ */
16
+ function initializeResults() {
17
+ // Get DOM elements
18
+ resultsContainer = document.getElementById('results-content');
19
+ exportBtn = document.getElementById('export-markdown-btn');
20
+ pdfBtn = document.getElementById('download-pdf-btn');
21
+
22
+ if (!resultsContainer) {
23
+ console.error('Results container not found');
24
+ return;
25
+ }
26
+
27
+ console.log('Results component initialized');
28
+
29
+ // Get research ID from URL
30
+ researchId = getResearchIdFromUrl();
31
+
32
+ if (!researchId) {
33
+ showError('Research ID not found in URL');
34
+ return;
35
+ }
36
+
37
+ // Set up event listeners
38
+ setupEventListeners();
39
+
40
+ // Load research results
41
+ loadResearchResults();
42
+
43
+ // Note: Log panel is now automatically initialized by logpanel.js
44
+ // No need to manually initialize it here
45
+ }
46
+
47
+ /**
48
+ * Set up event listeners
49
+ */
50
+ function setupEventListeners() {
51
+ // Export button
52
+ if (exportBtn) {
53
+ exportBtn.addEventListener('click', handleExport);
54
+ }
55
+
56
+ // PDF button
57
+ if (pdfBtn) {
58
+ pdfBtn.addEventListener('click', handlePdfExport);
59
+ }
60
+
61
+ // Back to history button
62
+ const backBtn = document.getElementById('back-to-history');
63
+ if (backBtn) {
64
+ backBtn.addEventListener('click', () => {
65
+ window.location.href = '/research/history';
66
+ });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get research ID from URL
72
+ * @returns {string|null} Research ID
73
+ */
74
+ function getResearchIdFromUrl() {
75
+ const path = window.location.pathname;
76
+ const match = path.match(/\/research\/results\/(\d+)/);
77
+ return match ? match[1] : null;
78
+ }
79
+
80
+ /**
81
+ * Load research results from API
82
+ */
83
+ async function loadResearchResults() {
84
+ try {
85
+ // Show loading state
86
+ resultsContainer.innerHTML = '<div class="text-center my-5"><i class="fas fa-spinner fa-pulse"></i><p class="mt-3">Loading research results...</p></div>';
87
+
88
+ // Fetch result from API
89
+ const response = await fetch(`/research/api/report/${researchId}`);
90
+
91
+ if (!response.ok) {
92
+ throw new Error(`HTTP error ${response.status}`);
93
+ }
94
+
95
+ const responseData = await response.json();
96
+ console.log('Original API response:', responseData);
97
+
98
+ // Store data for export
99
+ researchData = responseData;
100
+
101
+ // Check if we have data to display
102
+ if (!responseData) {
103
+ throw new Error('No data received from server');
104
+ }
105
+
106
+ // Use the API metadata directly
107
+ if (responseData.metadata && typeof responseData.metadata === 'object') {
108
+ console.log('Using metadata directly from API response:', responseData.metadata);
109
+ populateMetadataFromApiResponse(responseData);
110
+ } else {
111
+ // Fallback to content extraction if no metadata in response
112
+ populateMetadata(responseData);
113
+ }
114
+
115
+ // Render the content
116
+ if (responseData.content && typeof responseData.content === 'string') {
117
+ console.log('Rendering content from API response');
118
+ renderResults(responseData.content);
119
+ } else {
120
+ // Try to find content in other response formats
121
+ console.log('No direct content found, trying to find content in response');
122
+ findAndRenderContent(responseData);
123
+ }
124
+
125
+ // Enable export buttons
126
+ if (exportBtn) exportBtn.disabled = false;
127
+ if (pdfBtn) pdfBtn.disabled = false;
128
+
129
+ } catch (error) {
130
+ console.error('Error loading research results:', error);
131
+ showError(`Error loading research results: ${error.message}`);
132
+
133
+ // Disable export buttons
134
+ if (exportBtn) exportBtn.disabled = true;
135
+ if (pdfBtn) pdfBtn.disabled = true;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Populate metadata directly from API response metadata
141
+ * @param {Object} data - API response with metadata
142
+ */
143
+ function populateMetadataFromApiResponse(data) {
144
+ const metadata = data.metadata || {};
145
+ console.log('Using API response metadata:', metadata);
146
+
147
+ // Query field
148
+ const queryElement = document.getElementById('result-query');
149
+ if (queryElement) {
150
+ // Use query from metadata or content title
151
+ const query = metadata.query || metadata.title || data.query || 'Untitled Research';
152
+ console.log('Setting query to:', query);
153
+ queryElement.textContent = query;
154
+ }
155
+
156
+ // Generated date field
157
+ const dateElement = document.getElementById('result-date');
158
+ if (dateElement) {
159
+ let dateStr = 'Unknown date';
160
+
161
+ // Try multiple sources for the timestamp - first from the API response directly, then from metadata
162
+ const timestamp = data.created_at || data.timestamp || data.date ||
163
+ metadata.created_at || metadata.timestamp || metadata.date;
164
+
165
+ console.log('Found timestamp:', timestamp);
166
+
167
+ if (timestamp) {
168
+ if (window.formatting && typeof window.formatting.formatDate === 'function') {
169
+ dateStr = window.formatting.formatDate(timestamp);
170
+ console.log('Formatting timestamp with formatter:', timestamp, '→', dateStr);
171
+ } else {
172
+ try {
173
+ const date = new Date(timestamp);
174
+ dateStr = date.toLocaleString();
175
+ console.log('Formatting timestamp with toLocaleString:', timestamp, '→', dateStr);
176
+ } catch (e) {
177
+ console.error('Error parsing date:', e);
178
+ }
179
+ }
180
+
181
+ // Add duration if available - format as "Xm Ys" for values over 60 seconds
182
+ if (metadata.duration || metadata.duration_seconds || data.duration_seconds) {
183
+ const durationSeconds = parseInt(metadata.duration || metadata.duration_seconds || data.duration_seconds, 10);
184
+
185
+ if (!isNaN(durationSeconds)) {
186
+ let durationStr;
187
+ if (durationSeconds < 60) {
188
+ durationStr = `${durationSeconds}s`;
189
+ } else {
190
+ const minutes = Math.floor(durationSeconds / 60);
191
+ const seconds = durationSeconds % 60;
192
+ durationStr = `${minutes}m ${seconds}s`;
193
+ }
194
+ dateStr += ` (${durationStr})`;
195
+ }
196
+ }
197
+ }
198
+
199
+ console.log('Setting date to:', dateStr);
200
+ dateElement.textContent = dateStr;
201
+ }
202
+
203
+ // Mode field
204
+ const modeElement = document.getElementById('result-mode');
205
+ if (modeElement) {
206
+ // Get mode from metadata or main response
207
+ let mode = metadata.mode || metadata.research_mode || metadata.type ||
208
+ data.mode || data.research_mode || data.type;
209
+
210
+ // Detect if this is a detailed report based on content structure
211
+ if (!mode && data.content) {
212
+ if (data.content.toLowerCase().includes('table of contents') ||
213
+ data.content.match(/^#.*\n+##.*\n+###/m)) {
214
+ mode = 'detailed';
215
+ } else {
216
+ mode = 'quick';
217
+ }
218
+ }
219
+
220
+ // Format mode using available formatter
221
+ if (window.formatting && typeof window.formatting.formatMode === 'function') {
222
+ mode = window.formatting.formatMode(mode);
223
+ console.log('Formatted mode:', mode);
224
+ }
225
+
226
+ console.log('Setting mode to:', mode || 'Quick');
227
+ modeElement.textContent = mode || 'Quick';
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Find and render content from various response formats
233
+ * @param {Object} data - Research data to extract content from
234
+ */
235
+ function findAndRenderContent(data) {
236
+ if (data.content && typeof data.content === 'string') {
237
+ // Direct content property (newer format)
238
+ console.log('Rendering from data.content');
239
+ renderResults(data.content);
240
+ } else if (data.research && data.research.content) {
241
+ // Nested content in research object (older format)
242
+ console.log('Rendering from data.research.content');
243
+ renderResults(data.research.content);
244
+ } else if (data.report && typeof data.report === 'string') {
245
+ // Report format
246
+ console.log('Rendering from data.report');
247
+ renderResults(data.report);
248
+ } else if (data.results && data.results.content) {
249
+ // Results with content field
250
+ console.log('Rendering from data.results.content');
251
+ renderResults(data.results.content);
252
+ } else if (data.results && typeof data.results === 'string') {
253
+ // Results as direct string
254
+ console.log('Rendering from data.results string');
255
+ renderResults(data.results);
256
+ } else if (typeof data === 'string') {
257
+ // Plain string format
258
+ console.log('Rendering from string data');
259
+ renderResults(data);
260
+ } else {
261
+ // Look for any property that might contain the content
262
+ const contentProps = ['markdown', 'text', 'summary', 'output', 'research_output'];
263
+ let foundContent = false;
264
+
265
+ for (const prop of contentProps) {
266
+ if (data[prop] && typeof data[prop] === 'string') {
267
+ console.log(`Rendering from data.${prop}`);
268
+ renderResults(data[prop]);
269
+ foundContent = true;
270
+ break;
271
+ }
272
+ }
273
+
274
+ if (!foundContent) {
275
+ // Last resort: try to render the entire data object
276
+ console.log('No clear content found, rendering entire data object');
277
+ renderResults(data);
278
+ }
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Populate metadata fields with information from the research data
284
+ * @param {Object} data - Research data with metadata
285
+ */
286
+ function populateMetadata(data) {
287
+ // Debug the data structure
288
+ console.log('API response data:', data);
289
+ console.log('Data type:', typeof data);
290
+ console.log('Available top-level keys:', Object.keys(data));
291
+
292
+ // Direct extraction from content
293
+ if (data.content && typeof data.content === 'string') {
294
+ console.log('Attempting to extract metadata from content');
295
+
296
+ // Extract the query from content first line or header
297
+ // Avoid matching "Table of Contents" as query
298
+ const queryMatch = data.content.match(/^#\s*([^\n]+)/m) || // First heading
299
+ data.content.match(/Query:\s*([^\n]+)/i) || // Explicit query label
300
+ data.content.match(/Question:\s*([^\n]+)/i) || // Question label
301
+ data.content.match(/^([^\n#]+)(?=\n)/); // First line if not starting with #
302
+
303
+ if (queryMatch && queryMatch[1] && !queryMatch[1].toLowerCase().includes('table of contents')) {
304
+ const queryElement = document.getElementById('result-query');
305
+ if (queryElement) {
306
+ const extractedQuery = queryMatch[1].trim();
307
+ console.log('Extracted query from content:', extractedQuery);
308
+ queryElement.textContent = extractedQuery;
309
+ }
310
+ } else {
311
+ // Try to find the second heading if first was "Table of Contents"
312
+ const secondHeadingMatch = data.content.match(/^#\s*([^\n]+)[\s\S]*?^##\s*([^\n]+)/m);
313
+ if (secondHeadingMatch && secondHeadingMatch[2]) {
314
+ const queryElement = document.getElementById('result-query');
315
+ if (queryElement) {
316
+ const extractedQuery = secondHeadingMatch[2].trim();
317
+ console.log('Extracted query from second heading:', extractedQuery);
318
+ queryElement.textContent = extractedQuery;
319
+ }
320
+ }
321
+ }
322
+
323
+ // Extract generated date/time - Try multiple formats
324
+ const dateMatch = data.content.match(/Generated at:\s*([^\n]+)/i) ||
325
+ data.content.match(/Date:\s*([^\n]+)/i) ||
326
+ data.content.match(/Generated:\s*([^\n]+)/i) ||
327
+ data.content.match(/Created:\s*([^\n]+)/i);
328
+
329
+ if (dateMatch && dateMatch[1]) {
330
+ const dateElement = document.getElementById('result-date');
331
+ if (dateElement) {
332
+ const extractedDate = dateMatch[1].trim();
333
+ console.log('Extracted date from content:', extractedDate);
334
+
335
+ // Format the date using the available formatter
336
+ let formattedDate = extractedDate;
337
+ if (window.formatting && typeof window.formatting.formatDate === 'function') {
338
+ formattedDate = window.formatting.formatDate(extractedDate);
339
+ console.log('Date formatted using formatter:', formattedDate);
340
+ }
341
+
342
+ dateElement.textContent = formattedDate || new Date().toLocaleString();
343
+ }
344
+ }
345
+
346
+ // Extract mode
347
+ const modeMatch = data.content.match(/Mode:\s*([^\n]+)/i) ||
348
+ data.content.match(/Research type:\s*([^\n]+)/i);
349
+
350
+ if (modeMatch && modeMatch[1]) {
351
+ const modeElement = document.getElementById('result-mode');
352
+ if (modeElement) {
353
+ const extractedMode = modeMatch[1].trim();
354
+ console.log('Extracted mode from content:', extractedMode);
355
+
356
+ // Format mode using available formatter
357
+ let formattedMode = extractedMode;
358
+ if (window.formatting && typeof window.formatting.formatMode === 'function') {
359
+ formattedMode = window.formatting.formatMode(extractedMode);
360
+ console.log('Mode formatted using formatter:', formattedMode);
361
+ }
362
+
363
+ modeElement.textContent = formattedMode || 'Standard';
364
+ }
365
+ } else {
366
+ // Detect mode based on content structure and keywords
367
+ const modeElement = document.getElementById('result-mode');
368
+ if (modeElement) {
369
+ if (data.content.toLowerCase().includes('table of contents') ||
370
+ data.content.toLowerCase().includes('detailed report') ||
371
+ data.content.match(/^#.*\n+##.*\n+###/m)) { // Has H1, H2, H3 structure
372
+ modeElement.textContent = 'Detailed';
373
+ } else if (data.content.toLowerCase().includes('quick research') ||
374
+ data.content.toLowerCase().includes('summary')) {
375
+ modeElement.textContent = 'Quick';
376
+ } else {
377
+ modeElement.textContent = 'Standard'; // Better default
378
+ }
379
+ }
380
+ }
381
+
382
+ return; // Exit early since we've handled extraction from content
383
+ }
384
+
385
+ // Also check the metadata field which likely contains the actual metadata
386
+ const metadata = data.metadata || {};
387
+ console.log('Metadata object:', metadata);
388
+ if (metadata) {
389
+ console.log('Metadata keys:', Object.keys(metadata));
390
+ }
391
+
392
+ // Extract research object if nested
393
+ const researchData = data.research || data;
394
+
395
+ // Debug nested structure if exists
396
+ if (data.research) {
397
+ console.log('Nested research data:', data.research);
398
+ console.log('Research keys:', Object.keys(data.research));
399
+ }
400
+
401
+ // Query field
402
+ const queryElement = document.getElementById('result-query');
403
+ if (queryElement) {
404
+ // Try different possible locations for query data
405
+ let query = 'Unknown query';
406
+
407
+ if (metadata.query) {
408
+ query = metadata.query;
409
+ } else if (metadata.title) {
410
+ query = metadata.title;
411
+ } else if (researchData.query) {
412
+ query = researchData.query;
413
+ } else if (researchData.prompt) {
414
+ query = researchData.prompt;
415
+ } else if (researchData.title) {
416
+ query = researchData.title;
417
+ } else if (researchData.question) {
418
+ query = researchData.question;
419
+ } else if (researchData.input) {
420
+ query = researchData.input;
421
+ }
422
+
423
+ console.log('Setting query to:', query);
424
+ queryElement.textContent = query;
425
+ }
426
+
427
+ // Generated date field
428
+ const dateElement = document.getElementById('result-date');
429
+ if (dateElement) {
430
+ let dateStr = 'Unknown date';
431
+ let timestampField = null;
432
+
433
+ // Try different possible date fields
434
+ if (metadata.created_at) {
435
+ timestampField = metadata.created_at;
436
+ } else if (metadata.timestamp) {
437
+ timestampField = metadata.timestamp;
438
+ } else if (metadata.date) {
439
+ timestampField = metadata.date;
440
+ } else if (researchData.timestamp) {
441
+ timestampField = researchData.timestamp;
442
+ } else if (researchData.created_at) {
443
+ timestampField = researchData.created_at;
444
+ } else if (researchData.date) {
445
+ timestampField = researchData.date;
446
+ } else if (researchData.time) {
447
+ timestampField = researchData.time;
448
+ }
449
+
450
+ // Format the date using the available formatter
451
+ if (timestampField) {
452
+ if (window.formatting && typeof window.formatting.formatDate === 'function') {
453
+ dateStr = window.formatting.formatDate(timestampField);
454
+ console.log('Using formatter for timestamp:', timestampField, '→', dateStr);
455
+ } else {
456
+ try {
457
+ const date = new Date(timestampField);
458
+ dateStr = date.toLocaleString();
459
+ console.log('Using timestamp:', timestampField, '→', dateStr);
460
+ } catch (e) {
461
+ console.error('Error parsing date:', timestampField, e);
462
+ }
463
+ }
464
+ }
465
+
466
+ // Add duration if available
467
+ if (metadata.duration) {
468
+ dateStr += ` (${metadata.duration} seconds)`;
469
+ } else if (metadata.duration_seconds) {
470
+ dateStr += ` (${metadata.duration_seconds} seconds)`;
471
+ } else if (researchData.duration) {
472
+ dateStr += ` (${researchData.duration} seconds)`;
473
+ }
474
+
475
+ console.log('Setting date to:', dateStr);
476
+ dateElement.textContent = dateStr;
477
+ }
478
+
479
+ // Mode field
480
+ const modeElement = document.getElementById('result-mode');
481
+ if (modeElement) {
482
+ let mode = 'Quick'; // Default to Quick
483
+
484
+ if (metadata.mode) {
485
+ mode = metadata.mode;
486
+ } else if (metadata.research_mode) {
487
+ mode = metadata.research_mode;
488
+ } else if (metadata.type) {
489
+ mode = metadata.type;
490
+ } else if (researchData.mode) {
491
+ mode = researchData.mode;
492
+ } else if (researchData.research_mode) {
493
+ mode = researchData.research_mode;
494
+ } else if (researchData.type) {
495
+ mode = researchData.type;
496
+ }
497
+
498
+ // Format mode using available formatter
499
+ if (window.formatting && typeof window.formatting.formatMode === 'function') {
500
+ mode = window.formatting.formatMode(mode);
501
+ }
502
+
503
+ console.log('Setting mode to:', mode);
504
+ modeElement.textContent = mode;
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Render research results in the container
510
+ * @param {Object|string} data - Research data to render
511
+ */
512
+ function renderResults(data) {
513
+ try {
514
+ // Clear container
515
+ resultsContainer.innerHTML = '';
516
+
517
+ // Determine the content to render
518
+ let content = '';
519
+
520
+ if (typeof data === 'string') {
521
+ // Direct string content
522
+ content = data;
523
+ } else if (data.markdown) {
524
+ // Markdown content
525
+ content = data.markdown;
526
+ } else if (data.html) {
527
+ // HTML content
528
+ resultsContainer.innerHTML = data.html;
529
+ return; // Return early since we've set HTML directly
530
+ } else if (data.text) {
531
+ // Text content
532
+ content = data.text;
533
+ } else if (data.summary) {
534
+ // Summary content
535
+ content = data.summary;
536
+ } else if (data.results) {
537
+ // Results array (old format)
538
+ if (Array.isArray(data.results)) {
539
+ content = data.results.join('\n\n');
540
+ } else {
541
+ content = JSON.stringify(data.results, null, 2);
542
+ }
543
+ } else {
544
+ // Last resort: stringify the entire object
545
+ content = JSON.stringify(data, null, 2);
546
+ }
547
+
548
+ // Render the content as Markdown if possible
549
+ if (window.ui && window.ui.renderMarkdown) {
550
+ const renderedHtml = window.ui.renderMarkdown(content);
551
+ resultsContainer.innerHTML = renderedHtml;
552
+ } else {
553
+ // Fall back to basic formatting
554
+ content = content.replace(/\n/g, '<br>');
555
+ resultsContainer.innerHTML = `<div class="markdown-content">${content}</div>`;
556
+ }
557
+
558
+ // Add syntax highlighting if Prism is available
559
+ if (window.Prism) {
560
+ window.Prism.highlightAllUnder(resultsContainer);
561
+ }
562
+
563
+ } catch (error) {
564
+ console.error('Error rendering results:', error);
565
+ showError(`Error rendering results: ${error.message}`);
566
+ }
567
+ }
568
+
569
+ /**
570
+ * Show error message in the results container
571
+ * @param {string} message - Error message
572
+ */
573
+ function showError(message) {
574
+ resultsContainer.innerHTML = `
575
+ <div class="alert alert-danger" role="alert">
576
+ <i class="fas fa-exclamation-triangle"></i> ${message}
577
+ </div>
578
+ <p class="text-center mt-3">
579
+ <a href="/research" class="btn btn-primary">
580
+ <i class="fas fa-arrow-left"></i> Back to Research
581
+ </a>
582
+ </p>
583
+ `;
584
+ }
585
+
586
+ /**
587
+ * Handle export button click
588
+ */
589
+ function handleExport() {
590
+ try {
591
+ if (!researchData) {
592
+ throw new Error('No research data available');
593
+ }
594
+
595
+ // Get metadata from DOM (which should be populated by now)
596
+ const query = document.getElementById('result-query')?.textContent || 'Unknown query';
597
+ const generated = document.getElementById('result-date')?.textContent || 'Unknown date';
598
+ const mode = document.getElementById('result-mode')?.textContent || 'Quick';
599
+
600
+ // Create markdown header with metadata
601
+ let markdownHeader = `# Research Results: ${query}\n\n`;
602
+ markdownHeader += `- **Generated:** ${generated}\n`;
603
+ markdownHeader += `- **Mode:** ${mode}\n\n`;
604
+ markdownHeader += `---\n\n`;
605
+
606
+ // Extract the content to export
607
+ let markdownContent = '';
608
+
609
+ // Try to extract the markdown content from various possible locations
610
+ if (typeof researchData === 'string') {
611
+ markdownContent = researchData;
612
+ } else {
613
+ // Check for content in standard locations
614
+ const contentProps = [
615
+ 'content',
616
+ 'report',
617
+ 'markdown',
618
+ 'text',
619
+ 'summary',
620
+ 'output',
621
+ 'research_output'
622
+ ];
623
+
624
+ let found = false;
625
+
626
+ // First check direct properties
627
+ for (const prop of contentProps) {
628
+ if (researchData[prop] && typeof researchData[prop] === 'string') {
629
+ markdownContent = researchData[prop];
630
+ console.log(`Using ${prop} for markdown content`);
631
+ found = true;
632
+ break;
633
+ }
634
+ }
635
+
636
+ // Then check nested properties
637
+ if (!found && researchData.research) {
638
+ for (const prop of contentProps) {
639
+ if (researchData.research[prop] && typeof researchData.research[prop] === 'string') {
640
+ markdownContent = researchData.research[prop];
641
+ console.log(`Using research.${prop} for markdown content`);
642
+ found = true;
643
+ break;
644
+ }
645
+ }
646
+ }
647
+
648
+ // Check results property
649
+ if (!found && researchData.results) {
650
+ if (typeof researchData.results === 'string') {
651
+ markdownContent = researchData.results;
652
+ console.log('Using results string for markdown content');
653
+ } else {
654
+ for (const prop of contentProps) {
655
+ if (researchData.results[prop] && typeof researchData.results[prop] === 'string') {
656
+ markdownContent = researchData.results[prop];
657
+ console.log(`Using results.${prop} for markdown content`);
658
+ found = true;
659
+ break;
660
+ }
661
+ }
662
+ }
663
+ }
664
+
665
+ // Last resort
666
+ if (!markdownContent) {
667
+ console.warn('Could not extract markdown content, using JSON');
668
+ markdownContent = "```json\n" + JSON.stringify(researchData, null, 2) + "\n```";
669
+ }
670
+ }
671
+
672
+ // Combine header and content
673
+ const fullMarkdown = markdownHeader + markdownContent;
674
+
675
+ // Create blob and trigger download
676
+ const blob = new Blob([fullMarkdown], { type: 'text/markdown' });
677
+ const link = document.createElement('a');
678
+ link.href = URL.createObjectURL(blob);
679
+ link.download = `research_${researchId}.md`;
680
+
681
+ // Trigger download
682
+ document.body.appendChild(link);
683
+ link.click();
684
+ document.body.removeChild(link);
685
+
686
+ } catch (error) {
687
+ console.error('Error exporting markdown:', error);
688
+ alert(`Error exporting markdown: ${error.message}`);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Handle PDF export button click
694
+ */
695
+ function handlePdfExport() {
696
+ try {
697
+ if (!researchData) {
698
+ throw new Error('No research data available');
699
+ }
700
+
701
+ console.log('PDF export initiated for research ID:', researchId);
702
+
703
+ // Show loading indicator
704
+ pdfBtn.disabled = true;
705
+ pdfBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating PDF...';
706
+
707
+ // Get metadata from DOM (which should be populated correctly by now)
708
+ const title = document.getElementById('result-query')?.textContent || `Research ${researchId}`;
709
+ console.log('Using title for PDF:', title);
710
+
711
+ // Check if PDF service is available
712
+ if (window.pdfService && window.pdfService.downloadPdf) {
713
+ console.log('PDF service available, calling downloadPdf');
714
+
715
+ // Add the metadata to the researchData for PDF generation
716
+ const pdfData = {
717
+ ...researchData,
718
+ title: title,
719
+ query: title,
720
+ metadata: {
721
+ title: title,
722
+ date: document.getElementById('result-date')?.textContent || 'Unknown date',
723
+ mode: document.getElementById('result-mode')?.textContent || 'Standard'
724
+ }
725
+ };
726
+
727
+ // Use the PDF service to generate and download the PDF
728
+ window.pdfService.downloadPdf(pdfData, researchId)
729
+ .then(() => {
730
+ console.log('PDF generated successfully');
731
+ // Reset button
732
+ pdfBtn.disabled = false;
733
+ pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
734
+ })
735
+ .catch(error => {
736
+ console.error('Error generating PDF:', error);
737
+ alert(`Error generating PDF: ${error.message || 'Unknown error'}`);
738
+
739
+ // Reset button
740
+ pdfBtn.disabled = false;
741
+ pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
742
+ });
743
+ } else {
744
+ console.error('PDF service not available');
745
+ throw new Error('PDF service not available');
746
+ }
747
+
748
+ } catch (error) {
749
+ console.error('Error exporting PDF:', error);
750
+ alert(`Error exporting PDF: ${error.message || 'Unknown error'}`);
751
+
752
+ // Reset button
753
+ if (pdfBtn) {
754
+ pdfBtn.disabled = false;
755
+ pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
756
+ }
757
+ }
758
+ }
759
+
760
+ // Initialize on DOM content loaded
761
+ if (document.readyState === 'loading') {
762
+ document.addEventListener('DOMContentLoaded', initializeResults);
763
+ } else {
764
+ initializeResults();
765
+ }
766
+ })();