local-deep-research 0.1.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.
- local_deep_research/__init__.py +24 -0
- local_deep_research/citation_handler.py +113 -0
- local_deep_research/config.py +166 -0
- local_deep_research/defaults/__init__.py +44 -0
- local_deep_research/defaults/llm_config.py +269 -0
- local_deep_research/defaults/local_collections.toml +47 -0
- local_deep_research/defaults/main.toml +57 -0
- local_deep_research/defaults/search_engines.toml +244 -0
- local_deep_research/local_collections.py +141 -0
- local_deep_research/main.py +113 -0
- local_deep_research/report_generator.py +206 -0
- local_deep_research/search_system.py +241 -0
- local_deep_research/utilties/__init__.py +0 -0
- local_deep_research/utilties/enums.py +9 -0
- local_deep_research/utilties/llm_utils.py +116 -0
- local_deep_research/utilties/search_utilities.py +115 -0
- local_deep_research/utilties/setup_utils.py +6 -0
- local_deep_research/web/__init__.py +2 -0
- local_deep_research/web/app.py +1209 -0
- local_deep_research/web/static/css/styles.css +1008 -0
- local_deep_research/web/static/js/app.js +2078 -0
- local_deep_research/web/templates/api_keys_config.html +82 -0
- local_deep_research/web/templates/collections_config.html +90 -0
- local_deep_research/web/templates/index.html +312 -0
- local_deep_research/web/templates/llm_config.html +120 -0
- local_deep_research/web/templates/main_config.html +89 -0
- local_deep_research/web/templates/search_engines_config.html +154 -0
- local_deep_research/web/templates/settings.html +519 -0
- local_deep_research/web/templates/settings_dashboard.html +207 -0
- local_deep_research/web_search_engines/__init__.py +0 -0
- local_deep_research/web_search_engines/engines/__init__.py +0 -0
- local_deep_research/web_search_engines/engines/full_search.py +128 -0
- local_deep_research/web_search_engines/engines/meta_search_engine.py +274 -0
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +367 -0
- local_deep_research/web_search_engines/engines/search_engine_brave.py +245 -0
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +123 -0
- local_deep_research/web_search_engines/engines/search_engine_github.py +663 -0
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +283 -0
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +337 -0
- local_deep_research/web_search_engines/engines/search_engine_local.py +901 -0
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +153 -0
- local_deep_research/web_search_engines/engines/search_engine_medrxiv.py +623 -0
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +992 -0
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +230 -0
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +474 -0
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +242 -0
- local_deep_research/web_search_engines/full_search.py +254 -0
- local_deep_research/web_search_engines/search_engine_base.py +197 -0
- local_deep_research/web_search_engines/search_engine_factory.py +233 -0
- local_deep_research/web_search_engines/search_engines_config.py +54 -0
- local_deep_research-0.1.0.dist-info/LICENSE +21 -0
- local_deep_research-0.1.0.dist-info/METADATA +328 -0
- local_deep_research-0.1.0.dist-info/RECORD +56 -0
- local_deep_research-0.1.0.dist-info/WHEEL +5 -0
- local_deep_research-0.1.0.dist-info/entry_points.txt +3 -0
- local_deep_research-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2078 @@
|
|
1
|
+
// Main application functionality
|
2
|
+
document.addEventListener('DOMContentLoaded', () => {
|
3
|
+
// Global socket variable - initialize as null
|
4
|
+
let socket = null;
|
5
|
+
let socketConnected = false;
|
6
|
+
|
7
|
+
// Global state variables
|
8
|
+
let isResearchInProgress = false;
|
9
|
+
let currentResearchId = null;
|
10
|
+
window.currentResearchId = null;
|
11
|
+
|
12
|
+
// Polling interval for research status
|
13
|
+
let pollingInterval = null;
|
14
|
+
|
15
|
+
// Sound notification variables
|
16
|
+
let successSound = null;
|
17
|
+
let errorSound = null;
|
18
|
+
let notificationsEnabled = true;
|
19
|
+
|
20
|
+
// Initialize notification sounds
|
21
|
+
function initializeSounds() {
|
22
|
+
successSound = new Audio('/research/static/sounds/success.mp3');
|
23
|
+
errorSound = new Audio('/research/static/sounds/error.mp3');
|
24
|
+
successSound.volume = 0.7;
|
25
|
+
errorSound.volume = 0.7;
|
26
|
+
}
|
27
|
+
|
28
|
+
// Function to play a notification sound
|
29
|
+
function playNotificationSound(type) {
|
30
|
+
console.log(`Attempting to play ${type} notification sound`);
|
31
|
+
if (!notificationsEnabled) {
|
32
|
+
console.log('Notifications are disabled');
|
33
|
+
return;
|
34
|
+
}
|
35
|
+
|
36
|
+
// Play sounds regardless of tab focus
|
37
|
+
if (type === 'success' && successSound) {
|
38
|
+
console.log('Playing success sound');
|
39
|
+
successSound.play().catch(err => console.error('Error playing success sound:', err));
|
40
|
+
} else if (type === 'error' && errorSound) {
|
41
|
+
console.log('Playing error sound');
|
42
|
+
errorSound.play().catch(err => console.error('Error playing error sound:', err));
|
43
|
+
} else {
|
44
|
+
console.warn(`Unknown sound type or sound not initialized: ${type}`);
|
45
|
+
}
|
46
|
+
}
|
47
|
+
|
48
|
+
// Initialize socket only when needed with a timeout for safety
|
49
|
+
function initializeSocket() {
|
50
|
+
if (socket) return socket; // Return existing socket if already initialized
|
51
|
+
|
52
|
+
console.log('Initializing socket connection...');
|
53
|
+
// Create new socket connection with optimized settings for threading mode
|
54
|
+
socket = io({
|
55
|
+
path: '/research/socket.io',
|
56
|
+
transports: ['websocket', 'polling'],
|
57
|
+
reconnection: true,
|
58
|
+
reconnectionAttempts: 3,
|
59
|
+
reconnectionDelay: 1000,
|
60
|
+
timeout: 5000,
|
61
|
+
autoConnect: true,
|
62
|
+
forceNew: true
|
63
|
+
});
|
64
|
+
|
65
|
+
// Add event handlers
|
66
|
+
socket.on('connect', () => {
|
67
|
+
console.log('Socket connected');
|
68
|
+
socketConnected = true;
|
69
|
+
});
|
70
|
+
|
71
|
+
socket.on('disconnect', () => {
|
72
|
+
console.log('Socket disconnected');
|
73
|
+
socketConnected = false;
|
74
|
+
});
|
75
|
+
|
76
|
+
socket.on('connect_error', (error) => {
|
77
|
+
console.error('Socket connection error:', error);
|
78
|
+
socketConnected = false;
|
79
|
+
});
|
80
|
+
|
81
|
+
// Set a timeout to detect hanging connections
|
82
|
+
setTimeout(() => {
|
83
|
+
if (!socketConnected) {
|
84
|
+
console.log('Socket connection timeout - forcing reconnect');
|
85
|
+
try {
|
86
|
+
socket.disconnect();
|
87
|
+
socket.connect();
|
88
|
+
} catch (e) {
|
89
|
+
console.error('Error during forced reconnect:', e);
|
90
|
+
}
|
91
|
+
}
|
92
|
+
}, 5000);
|
93
|
+
|
94
|
+
return socket;
|
95
|
+
}
|
96
|
+
|
97
|
+
// Function to safely disconnect socket
|
98
|
+
window.disconnectSocket = function() {
|
99
|
+
try {
|
100
|
+
if (socket) {
|
101
|
+
console.log('Manually disconnecting socket');
|
102
|
+
socket.removeAllListeners();
|
103
|
+
socket.disconnect();
|
104
|
+
socket = null;
|
105
|
+
socketConnected = false;
|
106
|
+
}
|
107
|
+
} catch (e) {
|
108
|
+
console.error('Error disconnecting socket:', e);
|
109
|
+
}
|
110
|
+
};
|
111
|
+
|
112
|
+
// Helper function to connect to research updates
|
113
|
+
window.connectToResearchSocket = function(researchId) {
|
114
|
+
try {
|
115
|
+
// Initialize socket if needed
|
116
|
+
if (!socket) {
|
117
|
+
socket = initializeSocket();
|
118
|
+
}
|
119
|
+
|
120
|
+
// Subscribe to research updates
|
121
|
+
socket.emit('subscribe_to_research', { research_id: researchId });
|
122
|
+
|
123
|
+
// Set up event listener for research progress
|
124
|
+
const progressEventName = `research_progress_${researchId}`;
|
125
|
+
|
126
|
+
// Remove existing listeners to prevent duplicates
|
127
|
+
socket.off(progressEventName);
|
128
|
+
|
129
|
+
// Add new listener
|
130
|
+
socket.on(progressEventName, (data) => {
|
131
|
+
console.log('Received research progress update:', data);
|
132
|
+
updateProgressUI(data.progress, data.status, data.message);
|
133
|
+
|
134
|
+
// If research is complete, show the completion buttons
|
135
|
+
if (data.status === 'completed' || data.status === 'terminated' || data.status === 'failed' || data.status === 'suspended') {
|
136
|
+
console.log(`Socket received research final state: ${data.status}`);
|
137
|
+
|
138
|
+
// Clear polling interval if it exists
|
139
|
+
if (pollingInterval) {
|
140
|
+
console.log('Clearing polling interval from socket event');
|
141
|
+
clearInterval(pollingInterval);
|
142
|
+
pollingInterval = null;
|
143
|
+
}
|
144
|
+
|
145
|
+
// Update navigation state
|
146
|
+
if (data.status === 'completed') {
|
147
|
+
isResearchInProgress = false;
|
148
|
+
}
|
149
|
+
|
150
|
+
// Update UI for completion
|
151
|
+
if (data.status === 'completed') {
|
152
|
+
console.log('Research completed via socket, loading results automatically');
|
153
|
+
|
154
|
+
// Hide terminate button
|
155
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
156
|
+
if (terminateBtn) {
|
157
|
+
terminateBtn.style.display = 'none';
|
158
|
+
}
|
159
|
+
|
160
|
+
// Auto-load the results
|
161
|
+
loadResearch(researchId);
|
162
|
+
} else if (data.status === 'failed' || data.status === 'suspended') {
|
163
|
+
console.log(`Showing error message for status: ${data.status} from socket event`);
|
164
|
+
const errorMessage = document.getElementById('error-message');
|
165
|
+
if (errorMessage) {
|
166
|
+
errorMessage.style.display = 'block';
|
167
|
+
errorMessage.textContent = data.status === 'failed' ?
|
168
|
+
(data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
|
169
|
+
'Research was suspended';
|
170
|
+
} else {
|
171
|
+
console.error('error-message element not found in socket handler');
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
updateNavigationBasedOnResearchStatus();
|
176
|
+
|
177
|
+
// Play notification sounds based on status
|
178
|
+
if (data.status === 'completed') {
|
179
|
+
console.log('Playing success notification sound from socket event');
|
180
|
+
playNotificationSound('success');
|
181
|
+
} else if (data.status === 'failed') {
|
182
|
+
console.log('Playing error notification sound from socket event');
|
183
|
+
playNotificationSound('error');
|
184
|
+
}
|
185
|
+
|
186
|
+
// Force the UI to update with a manual trigger
|
187
|
+
document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
|
188
|
+
}
|
189
|
+
|
190
|
+
// Update the detailed log if on details page
|
191
|
+
if (data.log_entry && document.getElementById('research-log')) {
|
192
|
+
updateDetailLogEntry(data.log_entry);
|
193
|
+
}
|
194
|
+
});
|
195
|
+
|
196
|
+
return true;
|
197
|
+
} catch (e) {
|
198
|
+
console.error('Error connecting to research socket:', e);
|
199
|
+
return false;
|
200
|
+
}
|
201
|
+
};
|
202
|
+
|
203
|
+
// Check for active research on page load
|
204
|
+
async function checkActiveResearch() {
|
205
|
+
try {
|
206
|
+
const response = await fetch(getApiUrl('/api/history'));
|
207
|
+
const history = await response.json();
|
208
|
+
|
209
|
+
// Find in-progress research
|
210
|
+
const activeResearch = history.find(item => item.status === 'in_progress');
|
211
|
+
|
212
|
+
if (activeResearch) {
|
213
|
+
isResearchInProgress = true;
|
214
|
+
currentResearchId = activeResearch.id;
|
215
|
+
window.currentResearchId = currentResearchId;
|
216
|
+
|
217
|
+
// Check if we're on the new research page and redirect to progress
|
218
|
+
const currentPage = document.querySelector('.page.active');
|
219
|
+
|
220
|
+
if (currentPage && currentPage.id === 'new-research') {
|
221
|
+
// Navigate to progress page
|
222
|
+
switchPage('research-progress');
|
223
|
+
|
224
|
+
// Connect to socket for this research
|
225
|
+
window.connectToResearchSocket(currentResearchId);
|
226
|
+
|
227
|
+
// Start polling for updates
|
228
|
+
pollResearchStatus(currentResearchId);
|
229
|
+
}
|
230
|
+
}
|
231
|
+
} catch (error) {
|
232
|
+
console.error('Error checking for active research:', error);
|
233
|
+
}
|
234
|
+
}
|
235
|
+
|
236
|
+
// Add unload event listener
|
237
|
+
window.addEventListener('beforeunload', function() {
|
238
|
+
window.disconnectSocket();
|
239
|
+
});
|
240
|
+
|
241
|
+
// Function to start research
|
242
|
+
async function startResearch(query, mode) {
|
243
|
+
// Check if research is already in progress
|
244
|
+
if (isResearchInProgress) {
|
245
|
+
alert('Another research is already in progress. Please wait for it to complete or check its status in the history tab.');
|
246
|
+
return;
|
247
|
+
}
|
248
|
+
|
249
|
+
// Get the start button
|
250
|
+
const startResearchBtn = document.getElementById('start-research-btn');
|
251
|
+
if (!startResearchBtn) return;
|
252
|
+
|
253
|
+
// Disable the start button while we attempt to start the research
|
254
|
+
startResearchBtn.disabled = true;
|
255
|
+
startResearchBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
|
256
|
+
|
257
|
+
try {
|
258
|
+
const response = await fetch(getApiUrl('/api/start_research'), {
|
259
|
+
method: 'POST',
|
260
|
+
headers: {
|
261
|
+
'Content-Type': 'application/json'
|
262
|
+
},
|
263
|
+
body: JSON.stringify({
|
264
|
+
query: query,
|
265
|
+
mode: mode
|
266
|
+
})
|
267
|
+
});
|
268
|
+
|
269
|
+
const data = await response.json();
|
270
|
+
|
271
|
+
if (data.status === 'success') {
|
272
|
+
isResearchInProgress = true;
|
273
|
+
currentResearchId = data.research_id;
|
274
|
+
|
275
|
+
// Also update the window object
|
276
|
+
window.currentResearchId = data.research_id;
|
277
|
+
|
278
|
+
// Update the navigation to show Research in Progress
|
279
|
+
updateNavigationBasedOnResearchStatus();
|
280
|
+
|
281
|
+
// Update progress page
|
282
|
+
document.getElementById('current-query').textContent = query;
|
283
|
+
document.getElementById('progress-fill').style.width = '0%';
|
284
|
+
document.getElementById('progress-percentage').textContent = '0%';
|
285
|
+
document.getElementById('progress-status').textContent = 'Initializing research process...';
|
286
|
+
|
287
|
+
// Navigate to progress page
|
288
|
+
switchPage('research-progress');
|
289
|
+
|
290
|
+
// Connect to socket for this research
|
291
|
+
window.connectToResearchSocket(data.research_id);
|
292
|
+
|
293
|
+
// Start polling for status
|
294
|
+
pollResearchStatus(data.research_id);
|
295
|
+
|
296
|
+
// Show the terminate button
|
297
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
298
|
+
if (terminateBtn) {
|
299
|
+
terminateBtn.style.display = 'inline-flex';
|
300
|
+
terminateBtn.disabled = false;
|
301
|
+
}
|
302
|
+
} else {
|
303
|
+
alert('Error starting research: ' + (data.message || 'Unknown error'));
|
304
|
+
startResearchBtn.disabled = false;
|
305
|
+
startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
306
|
+
}
|
307
|
+
} catch (error) {
|
308
|
+
console.error('Error:', error);
|
309
|
+
alert('An error occurred while starting the research. Please try again.');
|
310
|
+
startResearchBtn.disabled = false;
|
311
|
+
startResearchBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
// Function to poll research status (as a backup to socket.io)
|
316
|
+
function pollResearchStatus(researchId) {
|
317
|
+
console.log(`Polling research status for ID: ${researchId}`);
|
318
|
+
fetch(getApiUrl(`/api/research/${researchId}`))
|
319
|
+
.then(response => {
|
320
|
+
if (!response.ok) {
|
321
|
+
throw new Error(`Server returned ${response.status}`);
|
322
|
+
}
|
323
|
+
return response.json();
|
324
|
+
})
|
325
|
+
.then(data => {
|
326
|
+
console.log('Research status response:', data);
|
327
|
+
// Update the UI with the current progress
|
328
|
+
updateProgressUI(data.progress, data.status, data.message);
|
329
|
+
|
330
|
+
// If research is complete, show the completion buttons
|
331
|
+
if (data.status === 'completed' || data.status === 'failed' || data.status === 'suspended') {
|
332
|
+
console.log(`Research is in final state: ${data.status}`);
|
333
|
+
// Clear the polling interval
|
334
|
+
if (pollingInterval) {
|
335
|
+
console.log('Clearing polling interval');
|
336
|
+
clearInterval(pollingInterval);
|
337
|
+
pollingInterval = null;
|
338
|
+
}
|
339
|
+
|
340
|
+
// Update UI for completion
|
341
|
+
if (data.status === 'completed') {
|
342
|
+
console.log('Research completed, loading results automatically');
|
343
|
+
// Hide the terminate button
|
344
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
345
|
+
if (terminateBtn) {
|
346
|
+
terminateBtn.style.display = 'none';
|
347
|
+
}
|
348
|
+
|
349
|
+
// Auto-load the results instead of showing a button
|
350
|
+
loadResearch(researchId);
|
351
|
+
} else if (data.status === 'failed' || data.status === 'suspended') {
|
352
|
+
console.log(`Showing error message for status: ${data.status}`);
|
353
|
+
const errorMessage = document.getElementById('error-message');
|
354
|
+
if (errorMessage) {
|
355
|
+
errorMessage.style.display = 'block';
|
356
|
+
errorMessage.textContent = data.status === 'failed' ?
|
357
|
+
(data.metadata && data.metadata.error ? JSON.parse(data.metadata).error : 'Research failed') :
|
358
|
+
'Research was suspended';
|
359
|
+
} else {
|
360
|
+
console.error('error-message element not found');
|
361
|
+
}
|
362
|
+
}
|
363
|
+
|
364
|
+
// Play notification sound based on status
|
365
|
+
if (data.status === 'completed') {
|
366
|
+
console.log('Playing success notification sound');
|
367
|
+
playNotificationSound('success');
|
368
|
+
} else if (data.status === 'failed') {
|
369
|
+
console.log('Playing error notification sound');
|
370
|
+
playNotificationSound('error');
|
371
|
+
}
|
372
|
+
|
373
|
+
// Update the navigation
|
374
|
+
console.log('Updating navigation based on research status');
|
375
|
+
updateNavigationBasedOnResearchStatus();
|
376
|
+
|
377
|
+
// Force the UI to update with a manual trigger
|
378
|
+
document.dispatchEvent(new CustomEvent('research_completed', { detail: data }));
|
379
|
+
|
380
|
+
return;
|
381
|
+
}
|
382
|
+
|
383
|
+
// Continue polling if still in progress
|
384
|
+
if (data.status === 'in_progress') {
|
385
|
+
console.log('Research is still in progress, continuing polling');
|
386
|
+
if (!pollingInterval) {
|
387
|
+
console.log('Setting up polling interval');
|
388
|
+
pollingInterval = setInterval(() => {
|
389
|
+
pollResearchStatus(researchId);
|
390
|
+
}, 10000);
|
391
|
+
}
|
392
|
+
}
|
393
|
+
})
|
394
|
+
.catch(error => {
|
395
|
+
console.error('Error polling research status:', error);
|
396
|
+
});
|
397
|
+
}
|
398
|
+
|
399
|
+
// Main initialization function
|
400
|
+
function initializeApp() {
|
401
|
+
console.log('Initializing application...');
|
402
|
+
|
403
|
+
// Initialize the sounds
|
404
|
+
initializeSounds();
|
405
|
+
|
406
|
+
// Get navigation elements
|
407
|
+
const navItems = document.querySelectorAll('.sidebar-nav li');
|
408
|
+
const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
|
409
|
+
const pages = document.querySelectorAll('.page');
|
410
|
+
const mobileTabBar = document.querySelector('.mobile-tab-bar');
|
411
|
+
|
412
|
+
// Handle responsive navigation based on screen size
|
413
|
+
function handleResponsiveNavigation() {
|
414
|
+
// Mobile tab bar should only be visible on small screens
|
415
|
+
if (window.innerWidth <= 767) {
|
416
|
+
if (mobileTabBar) {
|
417
|
+
mobileTabBar.style.display = 'flex';
|
418
|
+
}
|
419
|
+
} else {
|
420
|
+
if (mobileTabBar) {
|
421
|
+
mobileTabBar.style.display = 'none';
|
422
|
+
}
|
423
|
+
}
|
424
|
+
}
|
425
|
+
|
426
|
+
// Call on initial load
|
427
|
+
handleResponsiveNavigation();
|
428
|
+
|
429
|
+
// Add resize listener for responsive design
|
430
|
+
window.addEventListener('resize', handleResponsiveNavigation);
|
431
|
+
|
432
|
+
// Setup navigation click handlers
|
433
|
+
navItems.forEach(item => {
|
434
|
+
if (!item.classList.contains('external-link')) {
|
435
|
+
item.addEventListener('click', function() {
|
436
|
+
const pageId = this.dataset.page;
|
437
|
+
if (pageId) {
|
438
|
+
switchPage(pageId);
|
439
|
+
}
|
440
|
+
});
|
441
|
+
}
|
442
|
+
});
|
443
|
+
|
444
|
+
mobileNavItems.forEach(item => {
|
445
|
+
if (!item.classList.contains('external-link')) {
|
446
|
+
item.addEventListener('click', function() {
|
447
|
+
const pageId = this.dataset.page;
|
448
|
+
if (pageId) {
|
449
|
+
switchPage(pageId);
|
450
|
+
}
|
451
|
+
});
|
452
|
+
}
|
453
|
+
});
|
454
|
+
|
455
|
+
// Setup form submission
|
456
|
+
const researchForm = document.getElementById('research-form');
|
457
|
+
if (researchForm) {
|
458
|
+
researchForm.addEventListener('submit', function(e) {
|
459
|
+
e.preventDefault();
|
460
|
+
const query = document.getElementById('query').value.trim();
|
461
|
+
if (!query) {
|
462
|
+
alert('Please enter a research query');
|
463
|
+
return;
|
464
|
+
}
|
465
|
+
|
466
|
+
const mode = document.querySelector('.mode-option.active')?.dataset.mode || 'quick';
|
467
|
+
startResearch(query, mode);
|
468
|
+
});
|
469
|
+
}
|
470
|
+
|
471
|
+
// Initialize research mode selection
|
472
|
+
const modeOptions = document.querySelectorAll('.mode-option');
|
473
|
+
modeOptions.forEach(option => {
|
474
|
+
option.addEventListener('click', function() {
|
475
|
+
modeOptions.forEach(opt => opt.classList.remove('active'));
|
476
|
+
this.classList.add('active');
|
477
|
+
});
|
478
|
+
});
|
479
|
+
|
480
|
+
// Load research history initially
|
481
|
+
if (document.getElementById('history-list')) {
|
482
|
+
loadResearchHistory();
|
483
|
+
}
|
484
|
+
|
485
|
+
// Check for active research
|
486
|
+
checkActiveResearch();
|
487
|
+
|
488
|
+
// Setup notification toggle and other form elements
|
489
|
+
setupResearchForm();
|
490
|
+
|
491
|
+
console.log('Application initialized');
|
492
|
+
}
|
493
|
+
|
494
|
+
// Initialize the app
|
495
|
+
initializeApp();
|
496
|
+
|
497
|
+
// Function to switch between pages
|
498
|
+
function switchPage(pageId) {
|
499
|
+
// Get elements directly from the DOM
|
500
|
+
const pages = document.querySelectorAll('.page');
|
501
|
+
const navItems = document.querySelectorAll('.sidebar-nav li');
|
502
|
+
const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
|
503
|
+
|
504
|
+
// Remove active class from all pages
|
505
|
+
pages.forEach(page => {
|
506
|
+
page.classList.remove('active');
|
507
|
+
});
|
508
|
+
|
509
|
+
// Add active class to target page
|
510
|
+
const targetPage = document.getElementById(pageId);
|
511
|
+
if (targetPage) {
|
512
|
+
targetPage.classList.add('active');
|
513
|
+
}
|
514
|
+
|
515
|
+
// Update sidebar navigation active states
|
516
|
+
navItems.forEach(item => {
|
517
|
+
if (item.getAttribute('data-page') === pageId) {
|
518
|
+
item.classList.add('active');
|
519
|
+
} else {
|
520
|
+
item.classList.remove('active');
|
521
|
+
}
|
522
|
+
});
|
523
|
+
|
524
|
+
// Update mobile tab bar active states
|
525
|
+
mobileNavItems.forEach(item => {
|
526
|
+
if (item.getAttribute('data-page') === pageId) {
|
527
|
+
item.classList.add('active');
|
528
|
+
} else {
|
529
|
+
item.classList.remove('active');
|
530
|
+
}
|
531
|
+
});
|
532
|
+
|
533
|
+
// Special handling for history page
|
534
|
+
if (pageId === 'history') {
|
535
|
+
loadResearchHistory();
|
536
|
+
}
|
537
|
+
}
|
538
|
+
|
539
|
+
// Track termination status
|
540
|
+
let isTerminating = false;
|
541
|
+
|
542
|
+
// Check if we're on the history page and load history if needed
|
543
|
+
const historyPage = document.getElementById('history');
|
544
|
+
if (historyPage && historyPage.classList.contains('active')) {
|
545
|
+
// Use setTimeout to ensure the DOM is fully loaded
|
546
|
+
setTimeout(() => loadResearchHistory(), 100);
|
547
|
+
}
|
548
|
+
|
549
|
+
// Add a prefix helper function at the top of the file
|
550
|
+
function getApiUrl(path) {
|
551
|
+
// This function adds the /research prefix to all API URLs
|
552
|
+
return `/research${path}`;
|
553
|
+
}
|
554
|
+
|
555
|
+
// Function to terminate research - exposed to window object
|
556
|
+
async function terminateResearch(researchId) {
|
557
|
+
if (!researchId) {
|
558
|
+
console.error('No research ID provided for termination');
|
559
|
+
return;
|
560
|
+
}
|
561
|
+
|
562
|
+
// Prevent multiple termination requests
|
563
|
+
if (isTerminating) {
|
564
|
+
console.log('Termination already in progress');
|
565
|
+
return;
|
566
|
+
}
|
567
|
+
|
568
|
+
// Confirm with the user
|
569
|
+
if (!confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
|
570
|
+
return;
|
571
|
+
}
|
572
|
+
|
573
|
+
try {
|
574
|
+
// Set terminating flag
|
575
|
+
isTerminating = true;
|
576
|
+
|
577
|
+
// Update UI to show we're processing
|
578
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
579
|
+
if (terminateBtn) {
|
580
|
+
terminateBtn.disabled = true;
|
581
|
+
terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
582
|
+
}
|
583
|
+
|
584
|
+
// Find all terminate buttons in history items and disable them
|
585
|
+
const allTerminateBtns = document.querySelectorAll('.terminate-btn');
|
586
|
+
allTerminateBtns.forEach(btn => {
|
587
|
+
if (btn !== terminateBtn) {
|
588
|
+
btn.disabled = true;
|
589
|
+
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
590
|
+
}
|
591
|
+
});
|
592
|
+
|
593
|
+
// Call the terminate API
|
594
|
+
const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
|
595
|
+
method: 'POST'
|
596
|
+
});
|
597
|
+
|
598
|
+
const data = await response.json();
|
599
|
+
|
600
|
+
if (data.status === 'success') {
|
601
|
+
// The UI will be updated via socket events or polling
|
602
|
+
console.log('Termination request sent successfully');
|
603
|
+
|
604
|
+
// If we're on the history page, update the status of this item
|
605
|
+
if (document.getElementById('history').classList.contains('active')) {
|
606
|
+
updateHistoryItemStatus(researchId, 'terminating', 'Terminating...');
|
607
|
+
}
|
608
|
+
} else {
|
609
|
+
console.error('Termination request failed:', data.message);
|
610
|
+
alert(`Failed to terminate research: ${data.message}`);
|
611
|
+
|
612
|
+
// Reset the terminating flag
|
613
|
+
isTerminating = false;
|
614
|
+
|
615
|
+
// Reset the button
|
616
|
+
if (terminateBtn) {
|
617
|
+
terminateBtn.disabled = false;
|
618
|
+
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
619
|
+
}
|
620
|
+
|
621
|
+
// Reset history button states
|
622
|
+
const allTerminateBtns = document.querySelectorAll('.terminate-btn');
|
623
|
+
allTerminateBtns.forEach(btn => {
|
624
|
+
if (btn !== terminateBtn) {
|
625
|
+
btn.disabled = false;
|
626
|
+
btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
|
627
|
+
}
|
628
|
+
});
|
629
|
+
}
|
630
|
+
} catch (error) {
|
631
|
+
console.error('Error terminating research:', error);
|
632
|
+
alert('An error occurred while trying to terminate the research.');
|
633
|
+
|
634
|
+
// Reset the terminating flag
|
635
|
+
isTerminating = false;
|
636
|
+
|
637
|
+
// Reset the button
|
638
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
639
|
+
if (terminateBtn) {
|
640
|
+
terminateBtn.disabled = false;
|
641
|
+
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
642
|
+
}
|
643
|
+
|
644
|
+
// Reset history button states
|
645
|
+
const allTerminateBtns = document.querySelectorAll('.terminate-btn');
|
646
|
+
allTerminateBtns.forEach(btn => {
|
647
|
+
if (btn !== terminateBtn) {
|
648
|
+
btn.disabled = false;
|
649
|
+
btn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate';
|
650
|
+
}
|
651
|
+
});
|
652
|
+
}
|
653
|
+
}
|
654
|
+
|
655
|
+
// Expose the terminate function to the window object
|
656
|
+
window.terminateResearch = terminateResearch;
|
657
|
+
|
658
|
+
// Function to update the progress UI
|
659
|
+
function updateProgressUI(progress, status, message) {
|
660
|
+
const progressFill = document.getElementById('progress-fill');
|
661
|
+
const progressPercentage = document.getElementById('progress-percentage');
|
662
|
+
const progressStatus = document.getElementById('progress-status');
|
663
|
+
|
664
|
+
if (progressFill && progressPercentage) {
|
665
|
+
progressFill.style.width = `${progress}%`;
|
666
|
+
progressPercentage.textContent = `${progress}%`;
|
667
|
+
}
|
668
|
+
|
669
|
+
if (progressStatus && message) {
|
670
|
+
progressStatus.textContent = message;
|
671
|
+
|
672
|
+
// Update status class
|
673
|
+
progressStatus.className = 'progress-status';
|
674
|
+
if (status) {
|
675
|
+
progressStatus.classList.add(`status-${status}`);
|
676
|
+
}
|
677
|
+
}
|
678
|
+
|
679
|
+
// Show/hide terminate button based on status
|
680
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
681
|
+
if (terminateBtn) {
|
682
|
+
if (status === 'in_progress') {
|
683
|
+
terminateBtn.style.display = 'inline-flex';
|
684
|
+
terminateBtn.disabled = false;
|
685
|
+
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
686
|
+
} else if (status === 'terminating') {
|
687
|
+
terminateBtn.style.display = 'inline-flex';
|
688
|
+
terminateBtn.disabled = true;
|
689
|
+
terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
690
|
+
} else {
|
691
|
+
terminateBtn.style.display = 'none';
|
692
|
+
}
|
693
|
+
}
|
694
|
+
}
|
695
|
+
|
696
|
+
// Completely rewritten function to ensure reliable history loading
|
697
|
+
async function loadResearchHistory() {
|
698
|
+
const historyList = document.getElementById('history-list');
|
699
|
+
|
700
|
+
// Make sure we have the history list element
|
701
|
+
if (!historyList) {
|
702
|
+
console.error('History list element not found');
|
703
|
+
return;
|
704
|
+
}
|
705
|
+
|
706
|
+
historyList.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
|
707
|
+
|
708
|
+
try {
|
709
|
+
const response = await fetch(getApiUrl('/api/history'));
|
710
|
+
|
711
|
+
if (!response.ok) {
|
712
|
+
throw new Error(`Server returned ${response.status}`);
|
713
|
+
}
|
714
|
+
|
715
|
+
const data = await response.json();
|
716
|
+
|
717
|
+
// Clear the loading spinner
|
718
|
+
historyList.innerHTML = '';
|
719
|
+
|
720
|
+
// Handle empty data
|
721
|
+
if (!data || !Array.isArray(data) || data.length === 0) {
|
722
|
+
historyList.innerHTML = '<div class="empty-state">No research history found. Start a new research project!</div>';
|
723
|
+
return;
|
724
|
+
}
|
725
|
+
|
726
|
+
// Check if any research is in progress
|
727
|
+
const inProgressResearch = data.find(item => item.status === 'in_progress');
|
728
|
+
|
729
|
+
// Get the start research button
|
730
|
+
const startResearchBtn = document.getElementById('start-research-btn');
|
731
|
+
|
732
|
+
if (inProgressResearch) {
|
733
|
+
isResearchInProgress = true;
|
734
|
+
currentResearchId = inProgressResearch.id;
|
735
|
+
if (startResearchBtn) {
|
736
|
+
startResearchBtn.disabled = true;
|
737
|
+
}
|
738
|
+
} else {
|
739
|
+
isResearchInProgress = false;
|
740
|
+
if (startResearchBtn) {
|
741
|
+
startResearchBtn.disabled = false;
|
742
|
+
}
|
743
|
+
}
|
744
|
+
|
745
|
+
// Display each history item
|
746
|
+
data.forEach(item => {
|
747
|
+
try {
|
748
|
+
// Skip if item is invalid
|
749
|
+
if (!item || !item.id) {
|
750
|
+
return;
|
751
|
+
}
|
752
|
+
|
753
|
+
// Create container
|
754
|
+
const historyItem = document.createElement('div');
|
755
|
+
historyItem.className = 'history-item';
|
756
|
+
historyItem.dataset.researchId = item.id;
|
757
|
+
|
758
|
+
// Create header with title and status
|
759
|
+
const header = document.createElement('div');
|
760
|
+
header.className = 'history-item-header';
|
761
|
+
|
762
|
+
const title = document.createElement('div');
|
763
|
+
title.className = 'history-item-title';
|
764
|
+
title.textContent = item.query || 'Untitled Research';
|
765
|
+
|
766
|
+
const status = document.createElement('div');
|
767
|
+
status.className = `history-item-status status-${item.status || 'unknown'}`;
|
768
|
+
status.textContent = item.status ? (item.status.charAt(0).toUpperCase() + item.status.slice(1)) : 'Unknown';
|
769
|
+
|
770
|
+
header.appendChild(title);
|
771
|
+
header.appendChild(status);
|
772
|
+
historyItem.appendChild(header);
|
773
|
+
|
774
|
+
// Create meta section
|
775
|
+
const meta = document.createElement('div');
|
776
|
+
meta.className = 'history-item-meta';
|
777
|
+
|
778
|
+
const date = document.createElement('div');
|
779
|
+
date.className = 'history-item-date';
|
780
|
+
try {
|
781
|
+
// Use completed_at if available, fall back to created_at if not
|
782
|
+
const dateToUse = item.completed_at || item.created_at;
|
783
|
+
date.textContent = dateToUse ? formatDate(new Date(dateToUse)) : 'Unknown date';
|
784
|
+
} catch (e) {
|
785
|
+
date.textContent = item.completed_at || item.created_at || 'Unknown date';
|
786
|
+
}
|
787
|
+
|
788
|
+
const mode = document.createElement('div');
|
789
|
+
mode.className = 'history-item-mode';
|
790
|
+
const modeIcon = item.mode === 'quick' ? 'bolt' : 'microscope';
|
791
|
+
const modeText = item.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
|
792
|
+
mode.innerHTML = `<i class="fas fa-${modeIcon}"></i> ${modeText}`;
|
793
|
+
|
794
|
+
meta.appendChild(date);
|
795
|
+
meta.appendChild(mode);
|
796
|
+
historyItem.appendChild(meta);
|
797
|
+
|
798
|
+
// Create actions section
|
799
|
+
const actions = document.createElement('div');
|
800
|
+
actions.className = 'history-item-actions';
|
801
|
+
|
802
|
+
// View button
|
803
|
+
const viewBtn = document.createElement('button');
|
804
|
+
viewBtn.className = 'btn btn-sm btn-outline view-btn';
|
805
|
+
|
806
|
+
if (item.status === 'completed') {
|
807
|
+
viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
808
|
+
viewBtn.addEventListener('click', (e) => {
|
809
|
+
e.stopPropagation();
|
810
|
+
loadResearch(item.id);
|
811
|
+
});
|
812
|
+
|
813
|
+
// PDF button for completed research
|
814
|
+
const pdfBtn = document.createElement('button');
|
815
|
+
pdfBtn.className = 'btn btn-sm btn-outline pdf-btn';
|
816
|
+
pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> PDF';
|
817
|
+
pdfBtn.addEventListener('click', (e) => {
|
818
|
+
e.stopPropagation();
|
819
|
+
generatePdfFromResearch(item.id);
|
820
|
+
});
|
821
|
+
actions.appendChild(pdfBtn);
|
822
|
+
} else if (item.status === 'in_progress') {
|
823
|
+
viewBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> In Progress';
|
824
|
+
viewBtn.addEventListener('click', (e) => {
|
825
|
+
e.stopPropagation();
|
826
|
+
navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
|
827
|
+
});
|
828
|
+
} else {
|
829
|
+
viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
830
|
+
viewBtn.disabled = true;
|
831
|
+
}
|
832
|
+
|
833
|
+
actions.appendChild(viewBtn);
|
834
|
+
|
835
|
+
// Delete button
|
836
|
+
const deleteBtn = document.createElement('button');
|
837
|
+
deleteBtn.className = 'btn btn-sm btn-outline delete-btn';
|
838
|
+
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
|
839
|
+
deleteBtn.addEventListener('click', (e) => {
|
840
|
+
e.stopPropagation();
|
841
|
+
if (confirm(`Are you sure you want to delete this research: "${item.query}"?`)) {
|
842
|
+
deleteResearch(item.id);
|
843
|
+
}
|
844
|
+
});
|
845
|
+
actions.appendChild(deleteBtn);
|
846
|
+
|
847
|
+
historyItem.appendChild(actions);
|
848
|
+
|
849
|
+
// Add click handler for the entire item
|
850
|
+
historyItem.addEventListener('click', (e) => {
|
851
|
+
// Skip if clicking on a button
|
852
|
+
if (e.target.closest('button')) {
|
853
|
+
return;
|
854
|
+
}
|
855
|
+
|
856
|
+
if (item.status === 'completed') {
|
857
|
+
loadResearch(item.id);
|
858
|
+
} else if (item.status === 'in_progress') {
|
859
|
+
navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
|
860
|
+
}
|
861
|
+
});
|
862
|
+
|
863
|
+
// Add to the history list
|
864
|
+
historyList.appendChild(historyItem);
|
865
|
+
} catch (itemError) {
|
866
|
+
console.error('Error processing history item:', itemError, item);
|
867
|
+
}
|
868
|
+
});
|
869
|
+
|
870
|
+
} catch (error) {
|
871
|
+
console.error('Error loading history:', error);
|
872
|
+
historyList.innerHTML = `
|
873
|
+
<div class="error-message">
|
874
|
+
Error loading history: ${error.message}
|
875
|
+
</div>
|
876
|
+
<div style="text-align: center; margin-top: 1rem;">
|
877
|
+
<button id="retry-history-btn" class="btn btn-primary">
|
878
|
+
<i class="fas fa-sync"></i> Retry
|
879
|
+
</button>
|
880
|
+
</div>`;
|
881
|
+
|
882
|
+
const retryBtn = document.getElementById('retry-history-btn');
|
883
|
+
if (retryBtn) {
|
884
|
+
retryBtn.addEventListener('click', () => {
|
885
|
+
loadResearchHistory();
|
886
|
+
});
|
887
|
+
}
|
888
|
+
}
|
889
|
+
|
890
|
+
// Add a fallback in case something goes wrong and the history list is still empty
|
891
|
+
setTimeout(() => {
|
892
|
+
if (historyList.innerHTML === '' || historyList.innerHTML.includes('loading-spinner')) {
|
893
|
+
console.warn('History list is still empty or showing spinner after load attempt - applying fallback');
|
894
|
+
historyList.innerHTML = `
|
895
|
+
<div class="error-message">
|
896
|
+
Something went wrong while loading the history.
|
897
|
+
</div>
|
898
|
+
<div style="text-align: center; margin-top: 1rem;">
|
899
|
+
<button id="fallback-retry-btn" class="btn btn-primary">
|
900
|
+
<i class="fas fa-sync"></i> Retry
|
901
|
+
</button>
|
902
|
+
</div>`;
|
903
|
+
|
904
|
+
const fallbackRetryBtn = document.getElementById('fallback-retry-btn');
|
905
|
+
if (fallbackRetryBtn) {
|
906
|
+
fallbackRetryBtn.addEventListener('click', () => {
|
907
|
+
loadResearchHistory();
|
908
|
+
});
|
909
|
+
}
|
910
|
+
}
|
911
|
+
}, 5000); // Check after 5 seconds
|
912
|
+
}
|
913
|
+
|
914
|
+
// Function to navigate to research progress
|
915
|
+
function navigateToResearchProgress(research) {
|
916
|
+
document.getElementById('current-query').textContent = research.query;
|
917
|
+
document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
|
918
|
+
document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
|
919
|
+
|
920
|
+
// Navigate to progress page
|
921
|
+
switchPage('research-progress');
|
922
|
+
|
923
|
+
// Connect to socket for this research
|
924
|
+
window.connectToResearchSocket(research.id);
|
925
|
+
|
926
|
+
// Start polling for status
|
927
|
+
pollResearchStatus(research.id);
|
928
|
+
}
|
929
|
+
|
930
|
+
// Function to delete a research record
|
931
|
+
async function deleteResearch(researchId) {
|
932
|
+
try {
|
933
|
+
const response = await fetch(getApiUrl(`/api/research/${researchId}/delete`), {
|
934
|
+
method: 'DELETE'
|
935
|
+
});
|
936
|
+
|
937
|
+
if (response.ok) {
|
938
|
+
// Reload the history
|
939
|
+
loadResearchHistory();
|
940
|
+
} else {
|
941
|
+
alert('Failed to delete research. Please try again.');
|
942
|
+
}
|
943
|
+
} catch (error) {
|
944
|
+
console.error('Error deleting research:', error);
|
945
|
+
alert('An error occurred while deleting the research.');
|
946
|
+
}
|
947
|
+
}
|
948
|
+
|
949
|
+
// Function to load a specific research result
|
950
|
+
async function loadResearch(researchId) {
|
951
|
+
// Navigate to results page
|
952
|
+
switchPage('research-results');
|
953
|
+
|
954
|
+
const resultsContent = document.getElementById('results-content');
|
955
|
+
resultsContent.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
|
956
|
+
|
957
|
+
try {
|
958
|
+
// Load research details
|
959
|
+
const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
|
960
|
+
const details = await detailsResponse.json();
|
961
|
+
|
962
|
+
// Display metadata
|
963
|
+
document.getElementById('result-query').textContent = details.query;
|
964
|
+
|
965
|
+
// Format date with duration if available
|
966
|
+
let dateText = formatDate(new Date(details.completed_at || details.created_at));
|
967
|
+
if (details.duration_seconds) {
|
968
|
+
// Format duration
|
969
|
+
let durationText = '';
|
970
|
+
const duration = parseInt(details.duration_seconds);
|
971
|
+
|
972
|
+
if (duration < 60) { // less than a minute
|
973
|
+
durationText = `${duration}s`;
|
974
|
+
} else if (duration < 3600) { // less than an hour
|
975
|
+
durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
976
|
+
} else { // hours
|
977
|
+
durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
|
978
|
+
}
|
979
|
+
|
980
|
+
dateText += ` (Duration: ${durationText})`;
|
981
|
+
}
|
982
|
+
document.getElementById('result-date').textContent = dateText;
|
983
|
+
|
984
|
+
document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
|
985
|
+
|
986
|
+
// Load the report content
|
987
|
+
const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
|
988
|
+
const reportData = await reportResponse.json();
|
989
|
+
|
990
|
+
if (reportData.status === 'success') {
|
991
|
+
// Render markdown
|
992
|
+
const renderedContent = marked.parse(reportData.content);
|
993
|
+
resultsContent.innerHTML = renderedContent;
|
994
|
+
|
995
|
+
// Apply syntax highlighting
|
996
|
+
document.querySelectorAll('pre code').forEach((block) => {
|
997
|
+
hljs.highlightElement(block);
|
998
|
+
});
|
999
|
+
} else {
|
1000
|
+
resultsContent.innerHTML = '<div class="error-message">Error loading report. Please try again later.</div>';
|
1001
|
+
}
|
1002
|
+
} catch (error) {
|
1003
|
+
console.error('Error loading research:', error);
|
1004
|
+
resultsContent.innerHTML = '<div class="error-message">Error loading research results. Please try again later.</div>';
|
1005
|
+
}
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
// Function to load research details page
|
1009
|
+
async function loadResearchDetails(researchId) {
|
1010
|
+
// Navigate to details page
|
1011
|
+
switchPage('research-details');
|
1012
|
+
|
1013
|
+
// Initialize the research log area
|
1014
|
+
const researchLog = document.getElementById('research-log');
|
1015
|
+
researchLog.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
|
1016
|
+
|
1017
|
+
try {
|
1018
|
+
// Load research details
|
1019
|
+
const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
|
1020
|
+
console.log('Research details API response status:', response.status);
|
1021
|
+
|
1022
|
+
if (!response.ok) {
|
1023
|
+
console.error('API error:', response.status, response.statusText);
|
1024
|
+
researchLog.innerHTML = `<div class="error-message">Error loading research details. Status: ${response.status}</div>`;
|
1025
|
+
return;
|
1026
|
+
}
|
1027
|
+
|
1028
|
+
const data = await response.json();
|
1029
|
+
console.log('Research details data:', data);
|
1030
|
+
|
1031
|
+
if (data.status !== 'success') {
|
1032
|
+
researchLog.innerHTML = `<div class="error-message">Error loading research details: ${data.message || 'Unknown error'}</div>`;
|
1033
|
+
return;
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
// Display metadata
|
1037
|
+
document.getElementById('detail-query').textContent = data.query || 'N/A';
|
1038
|
+
document.getElementById('detail-status').textContent = capitalizeFirstLetter(data.status || 'unknown');
|
1039
|
+
document.getElementById('detail-status').className = `metadata-value status-${data.status || 'unknown'}`;
|
1040
|
+
document.getElementById('detail-mode').textContent = (data.mode === 'quick' ? 'Quick Summary' : 'Detailed Report') || 'N/A';
|
1041
|
+
|
1042
|
+
// Update progress bar
|
1043
|
+
const progress = data.progress || 0;
|
1044
|
+
document.getElementById('detail-progress-fill').style.width = `${progress}%`;
|
1045
|
+
document.getElementById('detail-progress-percentage').textContent = `${progress}%`;
|
1046
|
+
|
1047
|
+
// Render log entries
|
1048
|
+
renderLogEntries(data.log || []);
|
1049
|
+
|
1050
|
+
// Connect to socket for real-time updates
|
1051
|
+
window.connectToResearchSocket(researchId);
|
1052
|
+
|
1053
|
+
// Add appropriate actions based on research status
|
1054
|
+
const detailActions = document.getElementById('detail-actions');
|
1055
|
+
detailActions.innerHTML = '';
|
1056
|
+
|
1057
|
+
if (data.status === 'completed') {
|
1058
|
+
const viewResultsBtn = document.createElement('button');
|
1059
|
+
viewResultsBtn.className = 'btn btn-primary';
|
1060
|
+
viewResultsBtn.innerHTML = '<i class="fas fa-eye"></i> View Results';
|
1061
|
+
viewResultsBtn.addEventListener('click', () => loadResearch(researchId));
|
1062
|
+
detailActions.appendChild(viewResultsBtn);
|
1063
|
+
|
1064
|
+
// Add download PDF button
|
1065
|
+
const downloadPdfBtn = document.createElement('button');
|
1066
|
+
downloadPdfBtn.className = 'btn btn-outline';
|
1067
|
+
downloadPdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> Download PDF';
|
1068
|
+
downloadPdfBtn.addEventListener('click', () => generatePdfFromResearch(researchId));
|
1069
|
+
detailActions.appendChild(downloadPdfBtn);
|
1070
|
+
} else if (data.status === 'in_progress') {
|
1071
|
+
const viewProgressBtn = document.createElement('button');
|
1072
|
+
viewProgressBtn.className = 'btn btn-primary';
|
1073
|
+
viewProgressBtn.innerHTML = '<i class="fas fa-sync"></i> View Live Progress';
|
1074
|
+
viewProgressBtn.addEventListener('click', () => {
|
1075
|
+
document.getElementById('current-query').textContent = data.query || '';
|
1076
|
+
|
1077
|
+
// Navigate to progress page
|
1078
|
+
switchPage('research-progress');
|
1079
|
+
|
1080
|
+
// Connect to socket
|
1081
|
+
window.connectToResearchSocket(researchId);
|
1082
|
+
});
|
1083
|
+
detailActions.appendChild(viewProgressBtn);
|
1084
|
+
}
|
1085
|
+
} catch (error) {
|
1086
|
+
console.error('Error loading research details:', error);
|
1087
|
+
researchLog.innerHTML = `<div class="error-message">Error loading research details: ${error.message}</div>`;
|
1088
|
+
}
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
// Function to render log entries
|
1092
|
+
function renderLogEntries(logEntries) {
|
1093
|
+
const researchLog = document.getElementById('research-log');
|
1094
|
+
researchLog.innerHTML = '';
|
1095
|
+
|
1096
|
+
if (!logEntries || logEntries.length === 0) {
|
1097
|
+
researchLog.innerHTML = '<div class="empty-state">No log entries available.</div>';
|
1098
|
+
return;
|
1099
|
+
}
|
1100
|
+
|
1101
|
+
try {
|
1102
|
+
// Use a document fragment for better performance
|
1103
|
+
const fragment = document.createDocumentFragment();
|
1104
|
+
const template = document.getElementById('log-entry-template');
|
1105
|
+
|
1106
|
+
if (!template) {
|
1107
|
+
console.error('Log entry template not found');
|
1108
|
+
researchLog.innerHTML = '<div class="error-message">Error rendering log entries: Template not found</div>';
|
1109
|
+
return;
|
1110
|
+
}
|
1111
|
+
|
1112
|
+
logEntries.forEach(entry => {
|
1113
|
+
if (!entry) return; // Skip invalid entries
|
1114
|
+
|
1115
|
+
try {
|
1116
|
+
const clone = document.importNode(template.content, true);
|
1117
|
+
|
1118
|
+
// Format the timestamp
|
1119
|
+
let timeStr = 'N/A';
|
1120
|
+
try {
|
1121
|
+
if (entry.time) {
|
1122
|
+
const time = new Date(entry.time);
|
1123
|
+
timeStr = time.toLocaleTimeString();
|
1124
|
+
}
|
1125
|
+
} catch (timeErr) {
|
1126
|
+
console.warn('Error formatting time:', timeErr);
|
1127
|
+
}
|
1128
|
+
|
1129
|
+
const timeEl = clone.querySelector('.log-entry-time');
|
1130
|
+
if (timeEl) timeEl.textContent = timeStr;
|
1131
|
+
|
1132
|
+
// Add message with phase highlighting if available
|
1133
|
+
const messageEl = clone.querySelector('.log-entry-message');
|
1134
|
+
if (messageEl) {
|
1135
|
+
let phaseClass = '';
|
1136
|
+
if (entry.metadata && entry.metadata.phase) {
|
1137
|
+
phaseClass = `phase-${entry.metadata.phase}`;
|
1138
|
+
}
|
1139
|
+
messageEl.textContent = entry.message || 'No message';
|
1140
|
+
messageEl.classList.add(phaseClass);
|
1141
|
+
}
|
1142
|
+
|
1143
|
+
// Add progress information if available
|
1144
|
+
const progressEl = clone.querySelector('.log-entry-progress');
|
1145
|
+
if (progressEl) {
|
1146
|
+
if (entry.progress !== null && entry.progress !== undefined) {
|
1147
|
+
progressEl.textContent = `Progress: ${entry.progress}%`;
|
1148
|
+
} else {
|
1149
|
+
progressEl.textContent = '';
|
1150
|
+
}
|
1151
|
+
}
|
1152
|
+
|
1153
|
+
fragment.appendChild(clone);
|
1154
|
+
} catch (entryError) {
|
1155
|
+
console.error('Error processing log entry:', entryError, entry);
|
1156
|
+
// Continue with other entries
|
1157
|
+
}
|
1158
|
+
});
|
1159
|
+
|
1160
|
+
researchLog.appendChild(fragment);
|
1161
|
+
|
1162
|
+
// Scroll to the bottom
|
1163
|
+
researchLog.scrollTop = researchLog.scrollHeight;
|
1164
|
+
} catch (error) {
|
1165
|
+
console.error('Error rendering log entries:', error);
|
1166
|
+
researchLog.innerHTML = '<div class="error-message">Error rendering log entries. Please try again later.</div>';
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
// Connect to socket for updates if this is an in-progress research
|
1170
|
+
if (research && research.status === 'in_progress') {
|
1171
|
+
window.connectToResearchSocket(researchId);
|
1172
|
+
}
|
1173
|
+
}
|
1174
|
+
|
1175
|
+
// Function to update detail log with a new entry
|
1176
|
+
function updateDetailLogEntry(logEntry) {
|
1177
|
+
if (!logEntry || !document.getElementById('research-details').classList.contains('active')) {
|
1178
|
+
return;
|
1179
|
+
}
|
1180
|
+
|
1181
|
+
const researchLog = document.getElementById('research-log');
|
1182
|
+
const template = document.getElementById('log-entry-template');
|
1183
|
+
const clone = document.importNode(template.content, true);
|
1184
|
+
|
1185
|
+
// Format the timestamp
|
1186
|
+
const time = new Date(logEntry.time);
|
1187
|
+
clone.querySelector('.log-entry-time').textContent = time.toLocaleTimeString();
|
1188
|
+
|
1189
|
+
// Add message with phase highlighting if available
|
1190
|
+
const messageEl = clone.querySelector('.log-entry-message');
|
1191
|
+
let phaseClass = '';
|
1192
|
+
if (logEntry.metadata && logEntry.metadata.phase) {
|
1193
|
+
phaseClass = `phase-${logEntry.metadata.phase}`;
|
1194
|
+
}
|
1195
|
+
messageEl.textContent = logEntry.message;
|
1196
|
+
messageEl.classList.add(phaseClass);
|
1197
|
+
|
1198
|
+
// Add progress information if available
|
1199
|
+
const progressEl = clone.querySelector('.log-entry-progress');
|
1200
|
+
if (logEntry.progress !== null && logEntry.progress !== undefined) {
|
1201
|
+
progressEl.textContent = `Progress: ${logEntry.progress}%`;
|
1202
|
+
|
1203
|
+
// Also update the progress bar in the details view
|
1204
|
+
document.getElementById('detail-progress-fill').style.width = `${logEntry.progress}%`;
|
1205
|
+
document.getElementById('detail-progress-percentage').textContent = `${logEntry.progress}%`;
|
1206
|
+
} else {
|
1207
|
+
progressEl.textContent = '';
|
1208
|
+
}
|
1209
|
+
|
1210
|
+
researchLog.appendChild(clone);
|
1211
|
+
|
1212
|
+
// Scroll to the bottom
|
1213
|
+
researchLog.scrollTop = researchLog.scrollHeight;
|
1214
|
+
}
|
1215
|
+
|
1216
|
+
// Back to history button handlers
|
1217
|
+
document.getElementById('back-to-history').addEventListener('click', () => {
|
1218
|
+
const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
|
1219
|
+
historyNav.click();
|
1220
|
+
});
|
1221
|
+
|
1222
|
+
document.getElementById('back-to-history-from-details').addEventListener('click', () => {
|
1223
|
+
const historyNav = Array.from(navItems).find(item => item.getAttribute('data-page') === 'history');
|
1224
|
+
historyNav.click();
|
1225
|
+
});
|
1226
|
+
|
1227
|
+
// Helper functions
|
1228
|
+
function capitalizeFirstLetter(string) {
|
1229
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
1230
|
+
}
|
1231
|
+
|
1232
|
+
function formatDate(date) {
|
1233
|
+
// Ensure we're handling the date properly
|
1234
|
+
if (!(date instanceof Date) || isNaN(date)) {
|
1235
|
+
console.warn('Invalid date provided to formatDate:', date);
|
1236
|
+
return 'Invalid date';
|
1237
|
+
}
|
1238
|
+
|
1239
|
+
// Get current year to compare with date year
|
1240
|
+
const currentYear = new Date().getFullYear();
|
1241
|
+
const dateYear = date.getFullYear();
|
1242
|
+
|
1243
|
+
// Get month name, day, and time
|
1244
|
+
const month = date.toLocaleString('en-US', { month: 'short' });
|
1245
|
+
const day = date.getDate();
|
1246
|
+
const hours = date.getHours().toString().padStart(2, '0');
|
1247
|
+
const minutes = date.getMinutes().toString().padStart(2, '0');
|
1248
|
+
|
1249
|
+
// Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
|
1250
|
+
if (dateYear === currentYear) {
|
1251
|
+
return `${month} ${day}, ${hours}:${minutes}`;
|
1252
|
+
} else {
|
1253
|
+
return `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
|
1254
|
+
}
|
1255
|
+
}
|
1256
|
+
|
1257
|
+
// Function to update progress UI from current research
|
1258
|
+
function updateProgressFromCurrentResearch() {
|
1259
|
+
if (!isResearchInProgress || !currentResearchId) return;
|
1260
|
+
|
1261
|
+
// Fetch current status
|
1262
|
+
fetch(getApiUrl(`/api/research/${currentResearchId}`))
|
1263
|
+
.then(response => response.json())
|
1264
|
+
.then(data => {
|
1265
|
+
document.getElementById('current-query').textContent = data.query || '';
|
1266
|
+
document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
|
1267
|
+
document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
|
1268
|
+
|
1269
|
+
// Connect to socket for this research
|
1270
|
+
window.connectToResearchSocket(currentResearchId);
|
1271
|
+
})
|
1272
|
+
.catch(error => {
|
1273
|
+
console.error('Error fetching research status:', error);
|
1274
|
+
});
|
1275
|
+
}
|
1276
|
+
|
1277
|
+
// Function to update the sidebar navigation based on research status
|
1278
|
+
function updateNavigationBasedOnResearchStatus() {
|
1279
|
+
console.log("Updating navigation based on research status");
|
1280
|
+
console.log("isResearchInProgress:", isResearchInProgress);
|
1281
|
+
console.log("currentResearchId:", currentResearchId);
|
1282
|
+
|
1283
|
+
// Get nav items for each update to ensure we have fresh references
|
1284
|
+
const navItems = document.querySelectorAll('.sidebar-nav li');
|
1285
|
+
const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
|
1286
|
+
// Get all pages
|
1287
|
+
const pages = document.querySelectorAll('.page');
|
1288
|
+
|
1289
|
+
const newResearchNav = Array.from(navItems).find(item =>
|
1290
|
+
item.getAttribute('data-page') === 'new-research' ||
|
1291
|
+
(item.getAttribute('data-original-page') === 'new-research' &&
|
1292
|
+
item.getAttribute('data-page') === 'research-progress')
|
1293
|
+
);
|
1294
|
+
|
1295
|
+
if (newResearchNav) {
|
1296
|
+
if (isResearchInProgress) {
|
1297
|
+
console.log("Research is in progress, updating navigation");
|
1298
|
+
// Change text to "Research in Progress"
|
1299
|
+
newResearchNav.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Research in Progress';
|
1300
|
+
|
1301
|
+
// Also update the listener to navigate to progress page
|
1302
|
+
if (newResearchNav.getAttribute('data-page') !== 'research-progress') {
|
1303
|
+
newResearchNav.setAttribute('data-original-page', 'new-research');
|
1304
|
+
newResearchNav.setAttribute('data-page', 'research-progress');
|
1305
|
+
}
|
1306
|
+
|
1307
|
+
// If on new-research page, redirect to research-progress
|
1308
|
+
if (document.getElementById('new-research').classList.contains('active')) {
|
1309
|
+
pages.forEach(page => page.classList.remove('active'));
|
1310
|
+
document.getElementById('research-progress').classList.add('active');
|
1311
|
+
|
1312
|
+
// Update the research progress page
|
1313
|
+
updateProgressFromCurrentResearch();
|
1314
|
+
}
|
1315
|
+
} else {
|
1316
|
+
console.log("Research is not in progress, resetting navigation");
|
1317
|
+
// Reset to "New Research" if there's no active research
|
1318
|
+
newResearchNav.innerHTML = '<i class="fas fa-search"></i> New Research';
|
1319
|
+
|
1320
|
+
// Reset the listener
|
1321
|
+
if (newResearchNav.hasAttribute('data-original-page')) {
|
1322
|
+
newResearchNav.setAttribute('data-page', newResearchNav.getAttribute('data-original-page'));
|
1323
|
+
newResearchNav.removeAttribute('data-original-page');
|
1324
|
+
}
|
1325
|
+
|
1326
|
+
// If the terminate button is visible, hide it
|
1327
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
1328
|
+
if (terminateBtn) {
|
1329
|
+
terminateBtn.style.display = 'none';
|
1330
|
+
terminateBtn.disabled = false;
|
1331
|
+
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
1332
|
+
}
|
1333
|
+
}
|
1334
|
+
|
1335
|
+
// Make sure the navigation highlights the correct item
|
1336
|
+
navItems.forEach(item => {
|
1337
|
+
if (item === newResearchNav) {
|
1338
|
+
if (isResearchInProgress) {
|
1339
|
+
if (document.getElementById('research-progress').classList.contains('active')) {
|
1340
|
+
item.classList.add('active');
|
1341
|
+
}
|
1342
|
+
} else if (document.getElementById('new-research').classList.contains('active')) {
|
1343
|
+
item.classList.add('active');
|
1344
|
+
}
|
1345
|
+
}
|
1346
|
+
});
|
1347
|
+
}
|
1348
|
+
|
1349
|
+
// Connect to socket for updates
|
1350
|
+
if (currentResearchId) {
|
1351
|
+
window.connectToResearchSocket(currentResearchId);
|
1352
|
+
}
|
1353
|
+
}
|
1354
|
+
|
1355
|
+
// Function to update the research progress page from nav click
|
1356
|
+
function updateProgressPage() {
|
1357
|
+
if (!isResearchInProgress || !currentResearchId) {
|
1358
|
+
return;
|
1359
|
+
}
|
1360
|
+
|
1361
|
+
// Update the progress page
|
1362
|
+
fetch(getApiUrl(`/api/research/${currentResearchId}`))
|
1363
|
+
.then(response => response.json())
|
1364
|
+
.then(data => {
|
1365
|
+
// Update the query display
|
1366
|
+
document.getElementById('current-query').textContent = data.query;
|
1367
|
+
|
1368
|
+
// Update the progress bar
|
1369
|
+
updateProgressUI(data.progress || 0, data.status);
|
1370
|
+
|
1371
|
+
// Connect to socket for live updates
|
1372
|
+
window.connectToResearchSocket(currentResearchId);
|
1373
|
+
|
1374
|
+
// Check if we need to show the terminate button
|
1375
|
+
if (data.status === 'in_progress') {
|
1376
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
1377
|
+
if (terminateBtn) {
|
1378
|
+
terminateBtn.style.display = 'inline-flex';
|
1379
|
+
terminateBtn.disabled = false;
|
1380
|
+
}
|
1381
|
+
}
|
1382
|
+
})
|
1383
|
+
.catch(error => {
|
1384
|
+
console.error('Error fetching research status:', error);
|
1385
|
+
});
|
1386
|
+
}
|
1387
|
+
|
1388
|
+
// Function to update a specific history item without reloading the whole list
|
1389
|
+
function updateHistoryItemStatus(researchId, status, statusText) {
|
1390
|
+
const historyList = document.getElementById('history-list');
|
1391
|
+
|
1392
|
+
// Look for the item in the active research banner
|
1393
|
+
const activeBanner = historyList.querySelector(`.active-research-banner[data-research-id="${researchId}"]`);
|
1394
|
+
if (activeBanner) {
|
1395
|
+
const statusEl = activeBanner.querySelector('.history-item-status');
|
1396
|
+
if (statusEl) {
|
1397
|
+
statusEl.textContent = statusText || capitalizeFirstLetter(status);
|
1398
|
+
statusEl.className = 'history-item-status';
|
1399
|
+
statusEl.classList.add(`status-${status}`);
|
1400
|
+
}
|
1401
|
+
|
1402
|
+
// Update buttons
|
1403
|
+
const terminateBtn = activeBanner.querySelector('.terminate-btn');
|
1404
|
+
if (terminateBtn) {
|
1405
|
+
terminateBtn.style.display = 'none';
|
1406
|
+
}
|
1407
|
+
|
1408
|
+
const viewProgressBtn = activeBanner.querySelector('.view-progress-btn');
|
1409
|
+
if (viewProgressBtn) {
|
1410
|
+
if (status === 'suspended') {
|
1411
|
+
viewProgressBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
|
1412
|
+
} else if (status === 'failed') {
|
1413
|
+
viewProgressBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
|
1414
|
+
}
|
1415
|
+
viewProgressBtn.disabled = true;
|
1416
|
+
}
|
1417
|
+
|
1418
|
+
return;
|
1419
|
+
}
|
1420
|
+
|
1421
|
+
// Look for the item in the regular list
|
1422
|
+
const historyItem = historyList.querySelector(`.history-item[data-research-id="${researchId}"]`);
|
1423
|
+
if (historyItem) {
|
1424
|
+
const statusEl = historyItem.querySelector('.history-item-status');
|
1425
|
+
if (statusEl) {
|
1426
|
+
statusEl.textContent = statusText || capitalizeFirstLetter(status);
|
1427
|
+
statusEl.className = 'history-item-status';
|
1428
|
+
statusEl.classList.add(`status-${status}`);
|
1429
|
+
}
|
1430
|
+
|
1431
|
+
// Update view button
|
1432
|
+
const viewBtn = historyItem.querySelector('.view-btn');
|
1433
|
+
if (viewBtn) {
|
1434
|
+
if (status === 'suspended') {
|
1435
|
+
viewBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
|
1436
|
+
viewBtn.disabled = true;
|
1437
|
+
} else if (status === 'failed') {
|
1438
|
+
viewBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
|
1439
|
+
viewBtn.disabled = true;
|
1440
|
+
}
|
1441
|
+
}
|
1442
|
+
}
|
1443
|
+
}
|
1444
|
+
|
1445
|
+
// PDF Generation Functions
|
1446
|
+
function generatePdf() {
|
1447
|
+
const resultsContent = document.getElementById('results-content');
|
1448
|
+
const query = document.getElementById('result-query').textContent;
|
1449
|
+
const date = document.getElementById('result-date').textContent;
|
1450
|
+
const mode = document.getElementById('result-mode').textContent;
|
1451
|
+
|
1452
|
+
// Show loading indicator
|
1453
|
+
const loadingIndicator = document.createElement('div');
|
1454
|
+
loadingIndicator.className = 'loading-spinner centered';
|
1455
|
+
loadingIndicator.innerHTML = '<div class="spinner"></div><p style="margin-top: 10px;">Generating PDF...</p>';
|
1456
|
+
resultsContent.parentNode.insertBefore(loadingIndicator, resultsContent);
|
1457
|
+
resultsContent.style.display = 'none';
|
1458
|
+
|
1459
|
+
// Create a clone of the content for PDF generation
|
1460
|
+
const contentClone = resultsContent.cloneNode(true);
|
1461
|
+
contentClone.style.display = 'block';
|
1462
|
+
contentClone.style.position = 'absolute';
|
1463
|
+
contentClone.style.left = '-9999px';
|
1464
|
+
contentClone.style.width = '800px';
|
1465
|
+
|
1466
|
+
// Apply PDF-specific styling for better readability
|
1467
|
+
contentClone.style.background = '#ffffff';
|
1468
|
+
contentClone.style.color = '#333333';
|
1469
|
+
contentClone.style.padding = '20px';
|
1470
|
+
|
1471
|
+
// Improve visibility by adjusting styles specifically for PDF
|
1472
|
+
const applyPdfStyles = (element) => {
|
1473
|
+
// Set all text to dark color for better readability on white background
|
1474
|
+
element.querySelectorAll('*').forEach(el => {
|
1475
|
+
// Skip elements that already have inline color styles
|
1476
|
+
if (!el.style.color) {
|
1477
|
+
el.style.color = '#333333';
|
1478
|
+
}
|
1479
|
+
|
1480
|
+
// Fix background colors
|
1481
|
+
if (el.style.backgroundColor &&
|
1482
|
+
(el.style.backgroundColor.includes('var(--bg') ||
|
1483
|
+
el.style.backgroundColor.includes('#121212') ||
|
1484
|
+
el.style.backgroundColor.includes('#1e1e2d') ||
|
1485
|
+
el.style.backgroundColor.includes('#2a2a3a'))) {
|
1486
|
+
el.style.backgroundColor = '#f8f8f8';
|
1487
|
+
}
|
1488
|
+
|
1489
|
+
// Handle code blocks specifically
|
1490
|
+
if (el.tagName === 'PRE' || el.tagName === 'CODE') {
|
1491
|
+
el.style.backgroundColor = '#f5f5f5';
|
1492
|
+
el.style.border = '1px solid #e0e0e0';
|
1493
|
+
el.style.color = '#333333';
|
1494
|
+
}
|
1495
|
+
|
1496
|
+
// Make links visible
|
1497
|
+
if (el.tagName === 'A') {
|
1498
|
+
el.style.color = '#0066cc';
|
1499
|
+
el.style.textDecoration = 'underline';
|
1500
|
+
}
|
1501
|
+
});
|
1502
|
+
|
1503
|
+
// Fix specific syntax highlighting elements for PDF
|
1504
|
+
element.querySelectorAll('.hljs').forEach(hljs => {
|
1505
|
+
hljs.style.backgroundColor = '#f8f8f8';
|
1506
|
+
hljs.style.color = '#333333';
|
1507
|
+
|
1508
|
+
// Fix common syntax highlighting colors for PDF
|
1509
|
+
hljs.querySelectorAll('.hljs-keyword').forEach(el => el.style.color = '#0000cc');
|
1510
|
+
hljs.querySelectorAll('.hljs-string').forEach(el => el.style.color = '#008800');
|
1511
|
+
hljs.querySelectorAll('.hljs-number').forEach(el => el.style.color = '#aa0000');
|
1512
|
+
hljs.querySelectorAll('.hljs-comment').forEach(el => el.style.color = '#888888');
|
1513
|
+
hljs.querySelectorAll('.hljs-function').forEach(el => el.style.color = '#880000');
|
1514
|
+
});
|
1515
|
+
};
|
1516
|
+
|
1517
|
+
document.body.appendChild(contentClone);
|
1518
|
+
|
1519
|
+
// Apply PDF-specific styles
|
1520
|
+
applyPdfStyles(contentClone);
|
1521
|
+
|
1522
|
+
// Add title and metadata to the PDF content
|
1523
|
+
const headerDiv = document.createElement('div');
|
1524
|
+
headerDiv.innerHTML = `
|
1525
|
+
<h1 style="color: #6e4ff6; font-size: 24px; margin-bottom: 10px;">${query}</h1>
|
1526
|
+
<div style="margin-bottom: 20px; font-size: 14px; color: #666;">
|
1527
|
+
<p><strong>Generated:</strong> ${date}</p>
|
1528
|
+
<p><strong>Mode:</strong> ${mode}</p>
|
1529
|
+
<p><strong>Source:</strong> Deep Research Lab</p>
|
1530
|
+
</div>
|
1531
|
+
<hr style="margin-bottom: 20px; border: 1px solid #eee;">
|
1532
|
+
`;
|
1533
|
+
contentClone.insertBefore(headerDiv, contentClone.firstChild);
|
1534
|
+
|
1535
|
+
setTimeout(() => {
|
1536
|
+
try {
|
1537
|
+
// Use window.jspdf which is from the UMD bundle
|
1538
|
+
const { jsPDF } = window.jspdf;
|
1539
|
+
const pdf = new jsPDF('p', 'pt', 'a4');
|
1540
|
+
const pdfWidth = pdf.internal.pageSize.getWidth();
|
1541
|
+
const pdfHeight = pdf.internal.pageSize.getHeight();
|
1542
|
+
const margin = 40;
|
1543
|
+
const contentWidth = pdfWidth - 2 * margin;
|
1544
|
+
|
1545
|
+
// Create a more efficient PDF generation approach that keeps text selectable
|
1546
|
+
const generateTextBasedPDF = async () => {
|
1547
|
+
try {
|
1548
|
+
// Get all text elements and handle them differently than images and special content
|
1549
|
+
const elements = Array.from(contentClone.children);
|
1550
|
+
let currentY = margin;
|
1551
|
+
let pageNum = 1;
|
1552
|
+
|
1553
|
+
// Function to add a page with header
|
1554
|
+
const addPageWithHeader = (pageNum) => {
|
1555
|
+
if (pageNum > 1) {
|
1556
|
+
pdf.addPage();
|
1557
|
+
}
|
1558
|
+
pdf.setFontSize(8);
|
1559
|
+
pdf.setTextColor(100, 100, 100);
|
1560
|
+
pdf.text(`Deep Research - ${query} - Page ${pageNum}`, margin, pdfHeight - 20);
|
1561
|
+
};
|
1562
|
+
|
1563
|
+
addPageWithHeader(pageNum);
|
1564
|
+
|
1565
|
+
// Process each element
|
1566
|
+
for (const element of elements) {
|
1567
|
+
// Simple text content - handled directly by jsPDF
|
1568
|
+
if ((element.tagName === 'P' || element.tagName === 'DIV') &&
|
1569
|
+
!element.querySelector('img, canvas, svg') &&
|
1570
|
+
element.children.length === 0) {
|
1571
|
+
|
1572
|
+
pdf.setFontSize(11);
|
1573
|
+
pdf.setTextColor(0, 0, 0);
|
1574
|
+
|
1575
|
+
const text = element.textContent.trim();
|
1576
|
+
if (!text) continue; // Skip empty text
|
1577
|
+
|
1578
|
+
const textLines = pdf.splitTextToSize(text, contentWidth);
|
1579
|
+
|
1580
|
+
// Check if we need a new page
|
1581
|
+
if (currentY + (textLines.length * 14) > pdfHeight - margin) {
|
1582
|
+
pageNum++;
|
1583
|
+
addPageWithHeader(pageNum);
|
1584
|
+
currentY = margin;
|
1585
|
+
}
|
1586
|
+
|
1587
|
+
pdf.text(textLines, margin, currentY + 12);
|
1588
|
+
currentY += (textLines.length * 14) + 10;
|
1589
|
+
}
|
1590
|
+
// Handle headings
|
1591
|
+
else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
|
1592
|
+
const fontSize = {
|
1593
|
+
'H1': 24,
|
1594
|
+
'H2': 20,
|
1595
|
+
'H3': 16,
|
1596
|
+
'H4': 14,
|
1597
|
+
'H5': 12,
|
1598
|
+
'H6': 11
|
1599
|
+
}[element.tagName];
|
1600
|
+
|
1601
|
+
// Add heading text as native PDF text
|
1602
|
+
pdf.setFontSize(fontSize);
|
1603
|
+
|
1604
|
+
// Use a different color for headings to match styling
|
1605
|
+
if (element.tagName === 'H1') {
|
1606
|
+
pdf.setTextColor(110, 79, 246); // Purple for main headers
|
1607
|
+
} else if (element.tagName === 'H2') {
|
1608
|
+
pdf.setTextColor(70, 90, 150); // Darker blue for H2
|
1609
|
+
} else {
|
1610
|
+
pdf.setTextColor(0, 0, 0); // Black for other headings
|
1611
|
+
}
|
1612
|
+
|
1613
|
+
const text = element.textContent.trim();
|
1614
|
+
if (!text) continue; // Skip empty headings
|
1615
|
+
|
1616
|
+
const textLines = pdf.splitTextToSize(text, contentWidth);
|
1617
|
+
|
1618
|
+
// Check if we need a new page
|
1619
|
+
if (currentY + (textLines.length * (fontSize + 4)) > pdfHeight - margin) {
|
1620
|
+
pageNum++;
|
1621
|
+
addPageWithHeader(pageNum);
|
1622
|
+
currentY = margin + 40; // Reset Y position after header
|
1623
|
+
}
|
1624
|
+
|
1625
|
+
pdf.text(textLines, margin, currentY + fontSize);
|
1626
|
+
currentY += (textLines.length * (fontSize + 4)) + 10;
|
1627
|
+
|
1628
|
+
// Add a subtle underline for H1 and H2
|
1629
|
+
if (element.tagName === 'H1' || element.tagName === 'H2') {
|
1630
|
+
pdf.setDrawColor(110, 79, 246, 0.5);
|
1631
|
+
pdf.setLineWidth(0.5);
|
1632
|
+
pdf.line(
|
1633
|
+
margin,
|
1634
|
+
currentY - 5,
|
1635
|
+
margin + Math.min(contentWidth, pdf.getTextWidth(text) * 1.2),
|
1636
|
+
currentY - 5
|
1637
|
+
);
|
1638
|
+
currentY += 5; // Add a bit more space after underlined headings
|
1639
|
+
}
|
1640
|
+
}
|
1641
|
+
// Handle lists
|
1642
|
+
else if (element.tagName === 'UL' || element.tagName === 'OL') {
|
1643
|
+
pdf.setFontSize(11);
|
1644
|
+
pdf.setTextColor(0, 0, 0);
|
1645
|
+
|
1646
|
+
const listItems = element.querySelectorAll('li');
|
1647
|
+
let itemNumber = 1;
|
1648
|
+
|
1649
|
+
for (const item of listItems) {
|
1650
|
+
const prefix = element.tagName === 'UL' ? '• ' : `${itemNumber}. `;
|
1651
|
+
const text = item.textContent.trim();
|
1652
|
+
|
1653
|
+
if (!text) continue; // Skip empty list items
|
1654
|
+
|
1655
|
+
// Split text to fit width, accounting for bullet/number indent
|
1656
|
+
const textLines = pdf.splitTextToSize(text, contentWidth - 15);
|
1657
|
+
|
1658
|
+
// Check if we need a new page
|
1659
|
+
if (currentY + (textLines.length * 14) > pdfHeight - margin) {
|
1660
|
+
pageNum++;
|
1661
|
+
addPageWithHeader(pageNum);
|
1662
|
+
currentY = margin;
|
1663
|
+
}
|
1664
|
+
|
1665
|
+
// Add the bullet/number
|
1666
|
+
pdf.text(prefix, margin, currentY + 12);
|
1667
|
+
|
1668
|
+
// Add the text with indent
|
1669
|
+
pdf.text(textLines, margin + 15, currentY + 12);
|
1670
|
+
currentY += (textLines.length * 14) + 5;
|
1671
|
+
|
1672
|
+
if (element.tagName === 'OL') itemNumber++;
|
1673
|
+
}
|
1674
|
+
|
1675
|
+
currentY += 5; // Extra space after list
|
1676
|
+
}
|
1677
|
+
// Handle code blocks as text
|
1678
|
+
else if (element.tagName === 'PRE' || element.querySelector('pre')) {
|
1679
|
+
const codeElement = element.tagName === 'PRE' ? element : element.querySelector('pre');
|
1680
|
+
const codeText = codeElement.textContent.trim();
|
1681
|
+
|
1682
|
+
if (!codeText) continue; // Skip empty code blocks
|
1683
|
+
|
1684
|
+
// Use monospace font for code
|
1685
|
+
pdf.setFont("courier", "normal");
|
1686
|
+
pdf.setFontSize(9); // Smaller font for code
|
1687
|
+
|
1688
|
+
// Calculate code block size
|
1689
|
+
const codeLines = codeText.split('\n');
|
1690
|
+
const lineHeight = 10; // Smaller line height for code
|
1691
|
+
const codeBlockHeight = (codeLines.length * lineHeight) + 20; // Add padding
|
1692
|
+
|
1693
|
+
// Add a background for the code block
|
1694
|
+
if (currentY + codeBlockHeight > pdfHeight - margin) {
|
1695
|
+
pageNum++;
|
1696
|
+
addPageWithHeader(pageNum);
|
1697
|
+
currentY = margin;
|
1698
|
+
}
|
1699
|
+
|
1700
|
+
// Draw code block background
|
1701
|
+
pdf.setFillColor(245, 245, 245); // Light gray background
|
1702
|
+
pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'F');
|
1703
|
+
|
1704
|
+
// Draw a border
|
1705
|
+
pdf.setDrawColor(220, 220, 220);
|
1706
|
+
pdf.setLineWidth(0.5);
|
1707
|
+
pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'S');
|
1708
|
+
|
1709
|
+
// Add the code text
|
1710
|
+
pdf.setTextColor(0, 0, 0);
|
1711
|
+
currentY += 10; // Add padding at top
|
1712
|
+
|
1713
|
+
codeLines.forEach(line => {
|
1714
|
+
// Handle indentation by preserving leading spaces
|
1715
|
+
const spacePadding = line.match(/^(\s*)/)[0].length;
|
1716
|
+
const visibleLine = line.trimLeft();
|
1717
|
+
|
1718
|
+
// Calculate width of space character
|
1719
|
+
const spaceWidth = pdf.getStringUnitWidth(' ') * 9 / pdf.internal.scaleFactor;
|
1720
|
+
|
1721
|
+
pdf.text(visibleLine, margin + (spacePadding * spaceWidth), currentY);
|
1722
|
+
currentY += lineHeight;
|
1723
|
+
});
|
1724
|
+
|
1725
|
+
currentY += 10; // Add padding at bottom
|
1726
|
+
|
1727
|
+
// Reset to normal font
|
1728
|
+
pdf.setFont("helvetica", "normal");
|
1729
|
+
pdf.setFontSize(11);
|
1730
|
+
}
|
1731
|
+
// Handle tables as text
|
1732
|
+
else if (element.tagName === 'TABLE' || element.querySelector('table')) {
|
1733
|
+
const tableElement = element.tagName === 'TABLE' ? element : element.querySelector('table');
|
1734
|
+
|
1735
|
+
if (!tableElement) continue;
|
1736
|
+
|
1737
|
+
// Get table rows
|
1738
|
+
const rows = Array.from(tableElement.querySelectorAll('tr'));
|
1739
|
+
if (rows.length === 0) continue;
|
1740
|
+
|
1741
|
+
// Calculate column widths
|
1742
|
+
const headerCells = Array.from(rows[0].querySelectorAll('th, td'));
|
1743
|
+
const numColumns = headerCells.length;
|
1744
|
+
|
1745
|
+
if (numColumns === 0) continue;
|
1746
|
+
|
1747
|
+
// Default column width distribution (equal)
|
1748
|
+
const colWidth = contentWidth / numColumns;
|
1749
|
+
|
1750
|
+
// Start drawing table
|
1751
|
+
let tableY = currentY + 10;
|
1752
|
+
|
1753
|
+
// Check if we need a new page
|
1754
|
+
if (tableY + (rows.length * 20) > pdfHeight - margin) {
|
1755
|
+
pageNum++;
|
1756
|
+
addPageWithHeader(pageNum);
|
1757
|
+
tableY = margin + 10;
|
1758
|
+
currentY = margin;
|
1759
|
+
}
|
1760
|
+
|
1761
|
+
// Draw table header
|
1762
|
+
pdf.setFillColor(240, 240, 240);
|
1763
|
+
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
1764
|
+
|
1765
|
+
pdf.setFont("helvetica", "bold");
|
1766
|
+
pdf.setFontSize(10);
|
1767
|
+
pdf.setTextColor(0, 0, 0);
|
1768
|
+
|
1769
|
+
headerCells.forEach((cell, index) => {
|
1770
|
+
const text = cell.textContent.trim();
|
1771
|
+
const x = margin + (index * colWidth) + 5;
|
1772
|
+
pdf.text(text, x, tableY + 13);
|
1773
|
+
});
|
1774
|
+
|
1775
|
+
// Draw horizontal line after header
|
1776
|
+
pdf.setDrawColor(200, 200, 200);
|
1777
|
+
pdf.setLineWidth(0.5);
|
1778
|
+
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
1779
|
+
|
1780
|
+
tableY += 20;
|
1781
|
+
|
1782
|
+
// Draw table rows
|
1783
|
+
pdf.setFont("helvetica", "normal");
|
1784
|
+
for (let i = 1; i < rows.length; i++) {
|
1785
|
+
// Check if we need a new page
|
1786
|
+
if (tableY + 20 > pdfHeight - margin) {
|
1787
|
+
// Draw bottom border for last row on current page
|
1788
|
+
pdf.line(margin, tableY, margin + contentWidth, tableY);
|
1789
|
+
|
1790
|
+
// Add new page
|
1791
|
+
pageNum++;
|
1792
|
+
addPageWithHeader(pageNum);
|
1793
|
+
tableY = margin + 10;
|
1794
|
+
|
1795
|
+
// Redraw header on new page
|
1796
|
+
pdf.setFillColor(240, 240, 240);
|
1797
|
+
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
1798
|
+
|
1799
|
+
pdf.setFont("helvetica", "bold");
|
1800
|
+
headerCells.forEach((cell, index) => {
|
1801
|
+
const text = cell.textContent.trim();
|
1802
|
+
const x = margin + (index * colWidth) + 5;
|
1803
|
+
pdf.text(text, x, tableY + 13);
|
1804
|
+
});
|
1805
|
+
|
1806
|
+
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
1807
|
+
tableY += 20;
|
1808
|
+
pdf.setFont("helvetica", "normal");
|
1809
|
+
}
|
1810
|
+
|
1811
|
+
// Get cells for this row
|
1812
|
+
const cells = Array.from(rows[i].querySelectorAll('td, th'));
|
1813
|
+
|
1814
|
+
// Alternate row background for better readability
|
1815
|
+
if (i % 2 === 0) {
|
1816
|
+
pdf.setFillColor(250, 250, 250);
|
1817
|
+
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
1818
|
+
}
|
1819
|
+
|
1820
|
+
// Add cell content
|
1821
|
+
cells.forEach((cell, index) => {
|
1822
|
+
const text = cell.textContent.trim();
|
1823
|
+
const x = margin + (index * colWidth) + 5;
|
1824
|
+
pdf.text(text, x, tableY + 13);
|
1825
|
+
});
|
1826
|
+
|
1827
|
+
// Draw horizontal line after row
|
1828
|
+
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
1829
|
+
tableY += 20;
|
1830
|
+
}
|
1831
|
+
|
1832
|
+
// Draw vertical lines for columns
|
1833
|
+
for (let i = 0; i <= numColumns; i++) {
|
1834
|
+
const x = margin + (i * colWidth);
|
1835
|
+
pdf.line(x, currentY + 10, x, tableY);
|
1836
|
+
}
|
1837
|
+
|
1838
|
+
currentY = tableY + 10;
|
1839
|
+
}
|
1840
|
+
// Images still need to be handled as images
|
1841
|
+
else if (element.tagName === 'IMG' || element.querySelector('img')) {
|
1842
|
+
const imgElement = element.tagName === 'IMG' ? element : element.querySelector('img');
|
1843
|
+
|
1844
|
+
if (!imgElement || !imgElement.src) continue;
|
1845
|
+
|
1846
|
+
try {
|
1847
|
+
// Create a new image to get dimensions
|
1848
|
+
const img = new Image();
|
1849
|
+
img.src = imgElement.src;
|
1850
|
+
|
1851
|
+
// Calculate dimensions
|
1852
|
+
const imgWidth = contentWidth;
|
1853
|
+
const imgHeight = img.height * (contentWidth / img.width);
|
1854
|
+
|
1855
|
+
// Check if we need a new page
|
1856
|
+
if (currentY + imgHeight > pdfHeight - margin) {
|
1857
|
+
pageNum++;
|
1858
|
+
addPageWithHeader(pageNum);
|
1859
|
+
currentY = margin;
|
1860
|
+
}
|
1861
|
+
|
1862
|
+
// Add image to PDF
|
1863
|
+
pdf.addImage(img.src, 'JPEG', margin, currentY, imgWidth, imgHeight);
|
1864
|
+
currentY += imgHeight + 10;
|
1865
|
+
} catch (imgError) {
|
1866
|
+
console.error('Error adding image:', imgError);
|
1867
|
+
pdf.text("[Image could not be rendered]", margin, currentY + 12);
|
1868
|
+
currentY += 20;
|
1869
|
+
}
|
1870
|
+
}
|
1871
|
+
// Other complex elements still use html2canvas as fallback
|
1872
|
+
else {
|
1873
|
+
try {
|
1874
|
+
const canvas = await html2canvas(element, {
|
1875
|
+
scale: 2,
|
1876
|
+
useCORS: true,
|
1877
|
+
logging: false,
|
1878
|
+
backgroundColor: '#FFFFFF'
|
1879
|
+
});
|
1880
|
+
|
1881
|
+
const imgData = canvas.toDataURL('image/png');
|
1882
|
+
const imgWidth = contentWidth;
|
1883
|
+
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
1884
|
+
|
1885
|
+
if (currentY + imgHeight > pdfHeight - margin) {
|
1886
|
+
pageNum++;
|
1887
|
+
addPageWithHeader(pageNum);
|
1888
|
+
currentY = margin;
|
1889
|
+
}
|
1890
|
+
|
1891
|
+
pdf.addImage(imgData, 'PNG', margin, currentY, imgWidth, imgHeight);
|
1892
|
+
currentY += imgHeight + 10;
|
1893
|
+
} catch (canvasError) {
|
1894
|
+
console.error('Error rendering complex element:', canvasError);
|
1895
|
+
pdf.text("[Complex content could not be rendered]", margin, currentY + 12);
|
1896
|
+
currentY += 20;
|
1897
|
+
}
|
1898
|
+
}
|
1899
|
+
}
|
1900
|
+
|
1901
|
+
// Download the PDF
|
1902
|
+
const filename = `${query.replace(/[^a-z0-9]/gi, '_').substring(0, 30).toLowerCase()}_research.pdf`;
|
1903
|
+
pdf.save(filename);
|
1904
|
+
|
1905
|
+
// Clean up
|
1906
|
+
document.body.removeChild(contentClone);
|
1907
|
+
resultsContent.style.display = 'block';
|
1908
|
+
loadingIndicator.remove();
|
1909
|
+
} catch (error) {
|
1910
|
+
console.error('Error generating PDF:', error);
|
1911
|
+
alert('An error occurred while generating the PDF. Please try again.');
|
1912
|
+
document.body.removeChild(contentClone);
|
1913
|
+
resultsContent.style.display = 'block';
|
1914
|
+
loadingIndicator.remove();
|
1915
|
+
}
|
1916
|
+
};
|
1917
|
+
|
1918
|
+
generateTextBasedPDF();
|
1919
|
+
} catch (error) {
|
1920
|
+
console.error('Error initializing PDF generation:', error);
|
1921
|
+
alert('An error occurred while preparing the PDF. Please try again.');
|
1922
|
+
document.body.removeChild(contentClone);
|
1923
|
+
resultsContent.style.display = 'block';
|
1924
|
+
loadingIndicator.remove();
|
1925
|
+
}
|
1926
|
+
}, 100);
|
1927
|
+
}
|
1928
|
+
|
1929
|
+
// Function to generate PDF from a specific research ID
|
1930
|
+
async function generatePdfFromResearch(researchId) {
|
1931
|
+
try {
|
1932
|
+
// Load research details
|
1933
|
+
const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
|
1934
|
+
const details = await detailsResponse.json();
|
1935
|
+
|
1936
|
+
// Load the report content
|
1937
|
+
const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
|
1938
|
+
const reportData = await reportResponse.json();
|
1939
|
+
|
1940
|
+
if (reportData.status === 'success') {
|
1941
|
+
// Create a temporary container to render the content
|
1942
|
+
const tempContainer = document.createElement('div');
|
1943
|
+
tempContainer.className = 'results-content pdf-optimized';
|
1944
|
+
tempContainer.style.display = 'none';
|
1945
|
+
document.body.appendChild(tempContainer);
|
1946
|
+
|
1947
|
+
// Render markdown with optimized styles for PDF
|
1948
|
+
const renderedContent = marked.parse(reportData.content);
|
1949
|
+
tempContainer.innerHTML = renderedContent;
|
1950
|
+
|
1951
|
+
// Apply syntax highlighting
|
1952
|
+
tempContainer.querySelectorAll('pre code').forEach((block) => {
|
1953
|
+
hljs.highlightElement(block);
|
1954
|
+
});
|
1955
|
+
|
1956
|
+
// Format date
|
1957
|
+
let dateText = formatDate(new Date(details.completed_at || details.created_at));
|
1958
|
+
if (details.duration_seconds) {
|
1959
|
+
let durationText = '';
|
1960
|
+
const duration = parseInt(details.duration_seconds);
|
1961
|
+
|
1962
|
+
if (duration < 60) {
|
1963
|
+
durationText = `${duration}s`;
|
1964
|
+
} else if (duration < 3600) {
|
1965
|
+
durationText = `${Math.floor(duration / 60)}m ${duration % 60}s`;
|
1966
|
+
} else {
|
1967
|
+
durationText = `${Math.floor(duration / 3600)}h ${Math.floor((duration % 3600) / 60)}m`;
|
1968
|
+
}
|
1969
|
+
|
1970
|
+
dateText += ` (Duration: ${durationText})`;
|
1971
|
+
}
|
1972
|
+
|
1973
|
+
// Set up data for PDF generation
|
1974
|
+
document.getElementById('result-query').textContent = details.query;
|
1975
|
+
document.getElementById('result-date').textContent = dateText;
|
1976
|
+
document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
|
1977
|
+
|
1978
|
+
// Replace the current content with our temporary content
|
1979
|
+
const resultsContent = document.getElementById('results-content');
|
1980
|
+
const originalContent = resultsContent.innerHTML;
|
1981
|
+
resultsContent.innerHTML = tempContainer.innerHTML;
|
1982
|
+
|
1983
|
+
// Generate the PDF
|
1984
|
+
generatePdf();
|
1985
|
+
|
1986
|
+
// Restore original content if we're not on the results page
|
1987
|
+
setTimeout(() => {
|
1988
|
+
if (!document.getElementById('research-results').classList.contains('active')) {
|
1989
|
+
resultsContent.innerHTML = originalContent;
|
1990
|
+
}
|
1991
|
+
document.body.removeChild(tempContainer);
|
1992
|
+
}, 500);
|
1993
|
+
|
1994
|
+
} else {
|
1995
|
+
alert('Error loading report. Could not generate PDF.');
|
1996
|
+
}
|
1997
|
+
} catch (error) {
|
1998
|
+
console.error('Error generating PDF:', error);
|
1999
|
+
alert('An error occurred while generating the PDF. Please try again.');
|
2000
|
+
}
|
2001
|
+
}
|
2002
|
+
|
2003
|
+
// Initialize the terminate button event listener
|
2004
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
2005
|
+
if (terminateBtn) {
|
2006
|
+
terminateBtn.addEventListener('click', () => {
|
2007
|
+
if (confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
|
2008
|
+
if (currentResearchId) {
|
2009
|
+
terminateResearch(currentResearchId);
|
2010
|
+
}
|
2011
|
+
}
|
2012
|
+
});
|
2013
|
+
}
|
2014
|
+
|
2015
|
+
// Initialize PDF download button
|
2016
|
+
const downloadPdfBtn = document.getElementById('download-pdf-btn');
|
2017
|
+
if (downloadPdfBtn) {
|
2018
|
+
downloadPdfBtn.addEventListener('click', generatePdf);
|
2019
|
+
}
|
2020
|
+
|
2021
|
+
// Function to set up the research form
|
2022
|
+
function setupResearchForm() {
|
2023
|
+
const researchForm = document.getElementById('research-form');
|
2024
|
+
const notificationToggle = document.getElementById('notification-toggle');
|
2025
|
+
|
2026
|
+
// Set notification state from toggle
|
2027
|
+
if (notificationToggle) {
|
2028
|
+
notificationsEnabled = notificationToggle.checked;
|
2029
|
+
|
2030
|
+
// Listen for changes to the toggle
|
2031
|
+
notificationToggle.addEventListener('change', function() {
|
2032
|
+
notificationsEnabled = this.checked;
|
2033
|
+
// Store preference in localStorage for persistence
|
2034
|
+
localStorage.setItem('notificationsEnabled', notificationsEnabled);
|
2035
|
+
});
|
2036
|
+
|
2037
|
+
// Load saved preference from localStorage
|
2038
|
+
const savedPref = localStorage.getItem('notificationsEnabled');
|
2039
|
+
if (savedPref !== null) {
|
2040
|
+
notificationsEnabled = savedPref === 'true';
|
2041
|
+
notificationToggle.checked = notificationsEnabled;
|
2042
|
+
}
|
2043
|
+
}
|
2044
|
+
|
2045
|
+
// ... existing form setup ...
|
2046
|
+
}
|
2047
|
+
|
2048
|
+
// Add event listener for view results button
|
2049
|
+
const viewResultsBtn = document.getElementById('view-results-btn');
|
2050
|
+
if (viewResultsBtn) {
|
2051
|
+
viewResultsBtn.addEventListener('click', () => {
|
2052
|
+
console.log('View results button clicked');
|
2053
|
+
if (currentResearchId) {
|
2054
|
+
loadResearch(currentResearchId);
|
2055
|
+
} else {
|
2056
|
+
console.error('No research ID available');
|
2057
|
+
}
|
2058
|
+
});
|
2059
|
+
}
|
2060
|
+
|
2061
|
+
// Add listener for research_completed custom event
|
2062
|
+
document.addEventListener('research_completed', (event) => {
|
2063
|
+
console.log('Research completed event received:', event.detail);
|
2064
|
+
const data = event.detail;
|
2065
|
+
|
2066
|
+
// Mark research as no longer in progress
|
2067
|
+
isResearchInProgress = false;
|
2068
|
+
|
2069
|
+
// Hide terminate button
|
2070
|
+
const terminateBtn = document.getElementById('terminate-research-btn');
|
2071
|
+
if (terminateBtn) {
|
2072
|
+
terminateBtn.style.display = 'none';
|
2073
|
+
}
|
2074
|
+
|
2075
|
+
// Update navigation
|
2076
|
+
updateNavigationBasedOnResearchStatus();
|
2077
|
+
});
|
2078
|
+
});
|