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,622 @@
1
+ /**
2
+ * PDF Service
3
+ * Handles generation of PDFs from research results
4
+ */
5
+
6
+ /**
7
+ * Generate a PDF from the research content
8
+ * @param {string|any} title - The research title/query
9
+ * @param {string|any} content - The markdown content of the research
10
+ * @param {Object} metadata - Additional metadata for the PDF
11
+ * @returns {Promise<Blob>} A promise resolving to the generated PDF blob
12
+ */
13
+ async function generatePdf(title, content, metadata = {}) {
14
+ // Check if necessary libraries are loaded
15
+ if (typeof jspdf === 'undefined') {
16
+ throw new Error('PDF generation libraries not loaded (jsPDF missing)');
17
+ }
18
+
19
+ if (typeof html2canvas === 'undefined') {
20
+ throw new Error('PDF generation libraries not loaded (html2canvas missing)');
21
+ }
22
+
23
+ // Ensure title and content are strings
24
+ title = String(title || 'Research Report');
25
+ content = String(content || '');
26
+
27
+ // Create a temporary container to render the markdown
28
+ const tempContainer = document.createElement('div');
29
+ tempContainer.style.position = 'absolute';
30
+ tempContainer.style.left = '-9999px';
31
+ tempContainer.style.top = '-9999px';
32
+ tempContainer.style.width = '8.5in'; // US Letter width
33
+ tempContainer.className = 'pdf-content';
34
+
35
+ // Add PDF-specific styles
36
+ tempContainer.innerHTML = `
37
+ <style>
38
+ .pdf-content {
39
+ font-family: Arial, sans-serif;
40
+ color: #333;
41
+ line-height: 1.5;
42
+ padding: 20px;
43
+ background-color: #ffffff;
44
+ }
45
+ .pdf-content h1 {
46
+ font-size: 24px;
47
+ color: #000;
48
+ margin-bottom: 12px;
49
+ }
50
+ .pdf-content h2 {
51
+ font-size: 20px;
52
+ color: #000;
53
+ margin-top: 20px;
54
+ margin-bottom: 10px;
55
+ }
56
+ .pdf-content h3 {
57
+ font-size: 16px;
58
+ color: #000;
59
+ margin-top: 16px;
60
+ margin-bottom: 8px;
61
+ }
62
+ .pdf-content p {
63
+ margin-bottom: 10px;
64
+ }
65
+ .pdf-content ul, .pdf-content ol {
66
+ margin-left: 20px;
67
+ margin-bottom: 10px;
68
+ }
69
+ .pdf-content li {
70
+ margin-bottom: 5px;
71
+ }
72
+ .pdf-content pre {
73
+ background-color: #f5f5f5;
74
+ padding: 10px;
75
+ border-radius: 4px;
76
+ overflow-x: auto;
77
+ font-family: monospace;
78
+ font-size: 12px;
79
+ margin-bottom: 10px;
80
+ }
81
+ .pdf-content code {
82
+ font-family: monospace;
83
+ background-color: #f5f5f5;
84
+ padding: 2px 4px;
85
+ border-radius: 2px;
86
+ font-size: 12px;
87
+ }
88
+ .pdf-content blockquote {
89
+ border-left: 4px solid #ddd;
90
+ padding-left: 15px;
91
+ margin-left: 0;
92
+ color: #666;
93
+ }
94
+ .pdf-content table {
95
+ border-collapse: collapse;
96
+ width: 100%;
97
+ margin-bottom: 15px;
98
+ }
99
+ .pdf-content table, .pdf-content th, .pdf-content td {
100
+ border: 1px solid #ddd;
101
+ }
102
+ .pdf-content th, .pdf-content td {
103
+ padding: 8px;
104
+ text-align: left;
105
+ }
106
+ .pdf-content th {
107
+ background-color: #f5f5f5;
108
+ }
109
+ .pdf-metadata {
110
+ color: #666;
111
+ font-size: 12px;
112
+ margin-bottom: 20px;
113
+ border-bottom: 1px solid #ddd;
114
+ padding-bottom: 10px;
115
+ }
116
+ .pdf-header {
117
+ text-align: center;
118
+ border-bottom: 2px solid #333;
119
+ padding-bottom: 10px;
120
+ margin-bottom: 20px;
121
+ }
122
+ .pdf-footer {
123
+ border-top: 1px solid #ddd;
124
+ padding-top: 10px;
125
+ margin-top: 20px;
126
+ font-size: 12px;
127
+ color: #666;
128
+ }
129
+ </style>
130
+ <div class="pdf-header">
131
+ <h1>${title}</h1>
132
+ </div>
133
+ <div class="pdf-metadata">
134
+ <p>Generated: ${new Date().toLocaleString()}</p>
135
+ ${metadata.mode ? `<p>Mode: ${metadata.mode}</p>` : ''}
136
+ ${metadata.iterations ? `<p>Search iterations: ${metadata.iterations}</p>` : ''}
137
+ ${metadata.id ? `<p>Research ID: ${metadata.id}</p>` : ''}
138
+ ${metadata.timestamp ? `<p>Research Date: ${new Date(metadata.timestamp).toLocaleString()}</p>` : ''}
139
+ </div>
140
+ <div class="pdf-body">
141
+ ${window.marked ? window.marked.parse(content) : content}
142
+ </div>
143
+ <div class="pdf-footer">
144
+ <p>Generated by Deep Research System</p>
145
+ </div>
146
+ `;
147
+
148
+ // Add to the document body temporarily
149
+ document.body.appendChild(tempContainer);
150
+
151
+ try {
152
+ console.log("Starting PDF generation...");
153
+
154
+ // Use the jsPDF library from the window object
155
+ const { jsPDF } = window.jspdf;
156
+
157
+ // Create a new PDF document
158
+ const pdf = new jsPDF('p', 'pt', 'letter');
159
+ const pdfWidth = pdf.internal.pageSize.getWidth();
160
+ const pdfHeight = pdf.internal.pageSize.getHeight();
161
+ const margin = 40;
162
+ const contentWidth = pdfWidth - 2 * margin;
163
+
164
+ // Function to add page with header
165
+ const addPageWithHeader = (pageNum) => {
166
+ if (pageNum > 1) {
167
+ pdf.addPage();
168
+ }
169
+ pdf.setFontSize(8);
170
+ pdf.setTextColor(100, 100, 100);
171
+ pdf.text(`Deep Research - ${title} - Page ${pageNum}`, margin, pdfHeight - 20);
172
+ };
173
+
174
+ // Process each element with a more optimized approach
175
+ const contentDiv = tempContainer.querySelector('.pdf-body');
176
+ if (!contentDiv) {
177
+ throw new Error('PDF body container not found');
178
+ }
179
+
180
+ // Create a more efficient PDF generation approach that keeps text selectable
181
+ const elements = Array.from(contentDiv.children);
182
+ let currentY = margin;
183
+ let pageNum = 1;
184
+
185
+ // Add the first page with header
186
+ addPageWithHeader(pageNum);
187
+
188
+ // Process each element
189
+ for (const element of elements) {
190
+ // Skip style element
191
+ if (element.tagName === 'STYLE') continue;
192
+
193
+ try {
194
+ // Simple text content - handled directly by jsPDF for better text selection
195
+ if ((element.tagName === 'P' || element.tagName === 'DIV' || element.tagName === 'H1' ||
196
+ element.tagName === 'H2' || element.tagName === 'H3') &&
197
+ !element.querySelector('img, canvas, svg, table') &&
198
+ !element.innerHTML.includes('<')) {
199
+
200
+ pdf.setFont('helvetica', element.tagName.startsWith('H') ? 'bold' : 'normal');
201
+ pdf.setFontSize(element.tagName === 'H1' ? 18 : (element.tagName === 'H2' ? 16 : (element.tagName === 'H3' ? 14 : 11)));
202
+ pdf.setTextColor(0, 0, 0);
203
+
204
+ const text = element.textContent.trim();
205
+ if (!text) continue; // Skip empty text
206
+
207
+ const textLines = pdf.splitTextToSize(text, contentWidth);
208
+
209
+ // Check if we need a new page
210
+ if (currentY + (textLines.length * 14) > pdfHeight - margin) {
211
+ pageNum++;
212
+ addPageWithHeader(pageNum);
213
+ currentY = margin;
214
+ }
215
+
216
+ pdf.text(textLines, margin, currentY + 12);
217
+ currentY += (textLines.length * 14) + 10;
218
+ }
219
+ // List elements - handle bullets and numbering
220
+ else if (element.tagName === 'UL' || element.tagName === 'OL') {
221
+ const listItems = Array.from(element.querySelectorAll('li'));
222
+ for (let i = 0; i < listItems.length; i++) {
223
+ const item = listItems[i];
224
+ const itemText = item.textContent.trim();
225
+ if (!itemText) continue;
226
+
227
+ pdf.setFont('helvetica', 'normal');
228
+ pdf.setFontSize(11);
229
+ pdf.setTextColor(0, 0, 0);
230
+
231
+ const bulletPoint = element.tagName === 'UL' ? '•' : `${i + 1}.`;
232
+ const textWithBullet = `${bulletPoint} ${itemText}`;
233
+ const textLines = pdf.splitTextToSize(textWithBullet, contentWidth - 10);
234
+
235
+ // Check if we need a new page
236
+ if (currentY + (textLines.length * 14) > pdfHeight - margin) {
237
+ pageNum++;
238
+ addPageWithHeader(pageNum);
239
+ currentY = margin;
240
+ }
241
+
242
+ pdf.text(textLines, margin, currentY + 12);
243
+ currentY += (textLines.length * 14) + 5;
244
+ }
245
+ currentY += 5; // Add some space after the list
246
+ }
247
+ // Tables - render with proper cells
248
+ else if (element.tagName === 'TABLE') {
249
+ const rows = Array.from(element.querySelectorAll('tr'));
250
+ if (rows.length === 0) continue;
251
+
252
+ // Get header cells
253
+ const headerCells = Array.from(rows[0].querySelectorAll('th, td'));
254
+ const colCount = headerCells.length || 1;
255
+ const colWidth = contentWidth / colCount;
256
+
257
+ // Start table at current position
258
+ let tableY = currentY;
259
+
260
+ // Check if we need a new page
261
+ if (tableY + 20 > pdfHeight - margin) {
262
+ pageNum++;
263
+ addPageWithHeader(pageNum);
264
+ tableY = margin;
265
+ }
266
+
267
+ // Draw header background
268
+ pdf.setFillColor(240, 240, 240);
269
+ pdf.rect(margin, tableY, contentWidth, 20, 'F');
270
+
271
+ // Draw header text
272
+ pdf.setFont("helvetica", "bold");
273
+ pdf.setFontSize(10);
274
+ pdf.setTextColor(0, 0, 0);
275
+
276
+ headerCells.forEach((cell, index) => {
277
+ const text = cell.textContent.trim();
278
+ const x = margin + (index * colWidth) + 5;
279
+ pdf.text(text, x, tableY + 13);
280
+ });
281
+
282
+ // Draw horizontal line after header
283
+ pdf.setDrawColor(200, 200, 200);
284
+ pdf.setLineWidth(0.5);
285
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
286
+
287
+ tableY += 20;
288
+
289
+ // Draw table rows
290
+ pdf.setFont("helvetica", "normal");
291
+ for (let i = 1; i < rows.length; i++) {
292
+ // Check if we need a new page
293
+ if (tableY + 20 > pdfHeight - margin) {
294
+ pageNum++;
295
+ addPageWithHeader(pageNum);
296
+ tableY = margin;
297
+
298
+ // Redraw header on new page
299
+ pdf.setFillColor(240, 240, 240);
300
+ pdf.rect(margin, tableY, contentWidth, 20, 'F');
301
+
302
+ pdf.setFont("helvetica", "bold");
303
+ headerCells.forEach((cell, index) => {
304
+ const text = cell.textContent.trim();
305
+ const x = margin + (index * colWidth) + 5;
306
+ pdf.text(text, x, tableY + 13);
307
+ });
308
+
309
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
310
+ tableY += 20;
311
+ pdf.setFont("helvetica", "normal");
312
+ }
313
+
314
+ // Get cells for this row
315
+ const cells = Array.from(rows[i].querySelectorAll('td, th'));
316
+
317
+ // Draw cell content
318
+ cells.forEach((cell, index) => {
319
+ const text = cell.textContent.trim();
320
+ if (!text) return;
321
+
322
+ const x = margin + (index * colWidth) + 5;
323
+ pdf.text(text, x, tableY + 13);
324
+ });
325
+
326
+ // Draw horizontal line after row
327
+ pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
328
+ tableY += 20;
329
+ }
330
+
331
+ // Draw vertical lines
332
+ for (let i = 0; i <= colCount; i++) {
333
+ const x = margin + (i * colWidth);
334
+ pdf.line(x, currentY, x, tableY);
335
+ }
336
+
337
+ // Update current position to after the table
338
+ currentY = tableY + 10;
339
+ }
340
+ // Code blocks - render with monospace font and background
341
+ else if (element.tagName === 'PRE' || element.querySelector('pre')) {
342
+ const preElement = element.tagName === 'PRE' ? element : element.querySelector('pre');
343
+ const codeText = preElement.textContent.trim();
344
+ if (!codeText) continue;
345
+
346
+ pdf.setFont("courier", "normal"); // Use monospace font for code
347
+ pdf.setFontSize(9);
348
+ pdf.setTextColor(0, 0, 0);
349
+
350
+ // Split code into lines, respecting line breaks
351
+ const codeLines = codeText.split(/\r?\n/);
352
+ const wrappedLines = [];
353
+
354
+ codeLines.forEach(line => {
355
+ // Wrap long lines
356
+ const wrappedLine = pdf.splitTextToSize(line, contentWidth - 20);
357
+ wrappedLines.push(...wrappedLine);
358
+ });
359
+
360
+ // Calculate code block height
361
+ const lineHeight = 12;
362
+ const codeHeight = wrappedLines.length * lineHeight + 20; // 10px padding top and bottom
363
+
364
+ // Check if we need a new page
365
+ if (currentY + codeHeight > pdfHeight - margin) {
366
+ pageNum++;
367
+ addPageWithHeader(pageNum);
368
+ currentY = margin;
369
+ }
370
+
371
+ // Draw code block background
372
+ pdf.setFillColor(245, 245, 245);
373
+ pdf.rect(margin, currentY, contentWidth, codeHeight, 'F');
374
+
375
+ // Draw code content
376
+ pdf.setTextColor(0, 0, 0);
377
+ wrappedLines.forEach((line, index) => {
378
+ pdf.text(line, margin + 10, currentY + 15 + (index * lineHeight));
379
+ });
380
+
381
+ // Update position
382
+ currentY += codeHeight + 10;
383
+ }
384
+ // Images - render as images
385
+ else if (element.tagName === 'IMG' || element.querySelector('img')) {
386
+ const imgElement = element.tagName === 'IMG' ? element : element.querySelector('img');
387
+
388
+ if (!imgElement || !imgElement.src) continue;
389
+
390
+ try {
391
+ // Create a new image to get dimensions
392
+ const img = new Image();
393
+ img.src = imgElement.src;
394
+
395
+ // Calculate dimensions
396
+ const imgWidth = contentWidth;
397
+ const imgHeight = img.height * (contentWidth / img.width);
398
+
399
+ // Check if we need a new page
400
+ if (currentY + imgHeight > pdfHeight - margin) {
401
+ pageNum++;
402
+ addPageWithHeader(pageNum);
403
+ currentY = margin;
404
+ }
405
+
406
+ // Add image to PDF
407
+ pdf.addImage(img.src, 'JPEG', margin, currentY, imgWidth, imgHeight);
408
+ currentY += imgHeight + 10;
409
+ } catch (imgError) {
410
+ console.error('Error adding image:', imgError);
411
+ pdf.text("[Image could not be rendered]", margin, currentY + 12);
412
+ currentY += 20;
413
+ }
414
+ }
415
+ // Fallback for moderately complex elements - try to extract text first before using canvas
416
+ else {
417
+ // Try to extract text content and render it directly first
418
+ const textContent = element.textContent.trim();
419
+
420
+ if (textContent) {
421
+ pdf.setFont('helvetica', 'normal');
422
+ pdf.setFontSize(11);
423
+ pdf.setTextColor(0, 0, 0);
424
+
425
+ const textLines = pdf.splitTextToSize(textContent, contentWidth);
426
+
427
+ // Check if we need a new page
428
+ if (currentY + (textLines.length * 14) > pdfHeight - margin) {
429
+ pageNum++;
430
+ addPageWithHeader(pageNum);
431
+ currentY = margin;
432
+ }
433
+
434
+ pdf.text(textLines, margin, currentY + 12);
435
+ currentY += (textLines.length * 14) + 10;
436
+ } else {
437
+ // Only use html2canvas as a last resort for elements with no text content
438
+ try {
439
+ const canvas = await html2canvas(element, {
440
+ scale: 2,
441
+ useCORS: true,
442
+ logging: false,
443
+ backgroundColor: '#FFFFFF'
444
+ });
445
+
446
+ const imgData = canvas.toDataURL('image/png');
447
+ const imgWidth = contentWidth;
448
+ const imgHeight = (canvas.height * contentWidth) / canvas.width;
449
+
450
+ if (currentY + imgHeight > pdfHeight - margin) {
451
+ pageNum++;
452
+ addPageWithHeader(pageNum);
453
+ currentY = margin;
454
+ }
455
+
456
+ pdf.addImage(imgData, 'PNG', margin, currentY, imgWidth, imgHeight);
457
+ currentY += imgHeight + 10;
458
+ } catch (canvasError) {
459
+ console.error('Error rendering complex element:', canvasError);
460
+ pdf.text("[Content could not be rendered]", margin, currentY + 12);
461
+ currentY += 20;
462
+ }
463
+ }
464
+ }
465
+ } catch (elementError) {
466
+ console.error('Error processing element:', elementError);
467
+ pdf.text("[Error rendering content]", margin, currentY + 12);
468
+ currentY += 20;
469
+ }
470
+ }
471
+
472
+ // Generate the PDF blob
473
+ const blob = pdf.output('blob');
474
+ console.log("PDF generation completed successfully");
475
+ return blob;
476
+ } catch (error) {
477
+ console.error('Error in PDF generation:', error);
478
+ throw error;
479
+ } finally {
480
+ // Clean up
481
+ if (document.body.contains(tempContainer)) {
482
+ document.body.removeChild(tempContainer);
483
+ }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * Generate and download a PDF from research content
489
+ * @param {Object|string} titleOrData - Either the research title or the entire research data object
490
+ * @param {string|null} content - The markdown content of the research, or research ID if first param is data object
491
+ * @param {Object} metadata - Additional metadata for the PDF
492
+ * @returns {Promise<void>} A promise that resolves when the PDF has been downloaded
493
+ */
494
+ async function downloadPdf(titleOrData, content, metadata = {}) {
495
+ try {
496
+ console.log("Starting PDF download process...");
497
+
498
+ let title, pdfContent, pdfMetadata;
499
+
500
+ // Determine if we're being passed a research data object or direct parameters
501
+ if (typeof titleOrData === 'object' && titleOrData !== null) {
502
+ // We were passed a research data object
503
+ const researchData = titleOrData;
504
+ const researchId = content; // Second parameter is research ID in this case
505
+
506
+ console.log("Processing research data object", { researchId });
507
+
508
+ // Extract title from research data
509
+ title = researchData.query || researchData.title || researchData.prompt || `Research ${researchId}`;
510
+
511
+ // Extract content - try all possible locations based on how data might be structured
512
+ if (researchData.markdown) {
513
+ pdfContent = researchData.markdown;
514
+ } else if (researchData.content) {
515
+ pdfContent = researchData.content;
516
+ } else if (researchData.text) {
517
+ pdfContent = researchData.text;
518
+ } else if (researchData.summary) {
519
+ pdfContent = researchData.summary;
520
+ } else if (researchData.results && Array.isArray(researchData.results)) {
521
+ pdfContent = researchData.results.join('\n\n');
522
+ } else if (researchData.report) {
523
+ pdfContent = researchData.report;
524
+ } else if (researchData.research && researchData.research.content) {
525
+ pdfContent = researchData.research.content;
526
+ } else if (researchData.html) {
527
+ // If we have HTML, convert it to a reasonable markdown-like format
528
+ pdfContent = researchData.html
529
+ .replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
530
+ .replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
531
+ .replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
532
+ .replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
533
+ .replace(/<li[^>]*>(.*?)<\/li>/gi, '- $1\n')
534
+ .replace(/<br\s*\/?>/gi, '\n')
535
+ .replace(/<(?:.|\n)*?>/gm, '') // Remove remaining HTML tags
536
+ .replace(/&nbsp;/gi, ' ')
537
+ .replace(/&lt;/gi, '<')
538
+ .replace(/&gt;/gi, '>')
539
+ .replace(/&amp;/gi, '&')
540
+ .replace(/&quot;/gi, '"')
541
+ .replace(/&apos;/gi, "'")
542
+ .replace(/\n{3,}/g, '\n\n'); // Normalize excessive newlines
543
+ } else {
544
+ // Last resort: stringify the entire object
545
+ console.warn("Could not find content in research data, using JSON stringification");
546
+ pdfContent = JSON.stringify(researchData, null, 2);
547
+ }
548
+
549
+ // Extract metadata
550
+ pdfMetadata = {
551
+ mode: researchData.mode,
552
+ iterations: researchData.iterations,
553
+ timestamp: researchData.timestamp || researchData.created_at || new Date().toISOString(),
554
+ id: researchId
555
+ };
556
+ } else {
557
+ // We were passed direct parameters
558
+ title = titleOrData || 'Research Report';
559
+ pdfContent = content || '';
560
+ pdfMetadata = metadata || {};
561
+ }
562
+
563
+ // Ensure title is a string
564
+ title = String(title || 'Research Report');
565
+
566
+ console.log("PDF parameters prepared", { titleLength: title.length, contentLength: pdfContent?.length });
567
+
568
+ // Show loading indicator
569
+ const loadingIndicator = document.createElement('div');
570
+ loadingIndicator.className = 'loading-indicator';
571
+ loadingIndicator.innerHTML = '<div class="spinner"></div><div>Generating PDF...</div>';
572
+ loadingIndicator.style.position = 'fixed';
573
+ loadingIndicator.style.top = '50%';
574
+ loadingIndicator.style.left = '50%';
575
+ loadingIndicator.style.transform = 'translate(-50%, -50%)';
576
+ loadingIndicator.style.zIndex = '9999';
577
+ loadingIndicator.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
578
+ loadingIndicator.style.color = 'white';
579
+ loadingIndicator.style.padding = '20px';
580
+ loadingIndicator.style.borderRadius = '5px';
581
+ document.body.appendChild(loadingIndicator);
582
+
583
+ // Generate the PDF
584
+ const blob = await generatePdf(title, pdfContent, pdfMetadata);
585
+
586
+ // Create a download link
587
+ const url = URL.createObjectURL(blob);
588
+ const downloadLink = document.createElement('a');
589
+ downloadLink.href = url;
590
+
591
+ // Generate a filename based on the title
592
+ const safeTitle = title.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 30);
593
+ downloadLink.download = `${safeTitle}_research.pdf`;
594
+
595
+ // Trigger the download
596
+ document.body.appendChild(downloadLink);
597
+ downloadLink.click();
598
+
599
+ // Clean up
600
+ document.body.removeChild(downloadLink);
601
+ URL.revokeObjectURL(url);
602
+
603
+ console.log("PDF download process completed");
604
+ return true;
605
+ } catch (error) {
606
+ console.error('Error generating PDF:', error);
607
+ alert('Error generating PDF: ' + (error.message || 'Unknown error'));
608
+ throw error;
609
+ } finally {
610
+ // Remove loading indicator
611
+ const loadingIndicator = document.querySelector('.loading-indicator');
612
+ if (loadingIndicator) {
613
+ document.body.removeChild(loadingIndicator);
614
+ }
615
+ }
616
+ }
617
+
618
+ // Export PDF functions
619
+ window.pdfService = {
620
+ generatePdf,
621
+ downloadPdf
622
+ };