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.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +96 -84
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +72 -44
- local_deep_research/search_system.py +147 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1592 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
- local_deep_research-0.2.0.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,3763 +0,0 @@
|
|
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
|
-
// Add function to cleanup research resources globally
|
21
|
-
window.cleanupResearchResources = function() {
|
22
|
-
console.log('Cleaning up research resources');
|
23
|
-
|
24
|
-
// Disconnect any active sockets
|
25
|
-
disconnectAllSockets();
|
26
|
-
|
27
|
-
// Remove any active research data
|
28
|
-
if (window.currentResearchId) {
|
29
|
-
console.log(`Cleaning up research ID: ${window.currentResearchId}`);
|
30
|
-
window.currentResearchId = null;
|
31
|
-
}
|
32
|
-
|
33
|
-
// Reset any active timers
|
34
|
-
if (pollingInterval) {
|
35
|
-
clearInterval(pollingInterval);
|
36
|
-
pollingInterval = null;
|
37
|
-
console.log('Cleared polling interval during cleanup');
|
38
|
-
}
|
39
|
-
|
40
|
-
// Reset research state flags
|
41
|
-
isResearchInProgress = false;
|
42
|
-
};
|
43
|
-
|
44
|
-
// Initialize notification sounds
|
45
|
-
function initializeSounds() {
|
46
|
-
successSound = new Audio('/research/static/sounds/success.mp3');
|
47
|
-
errorSound = new Audio('/research/static/sounds/error.mp3');
|
48
|
-
successSound.volume = 0.7;
|
49
|
-
errorSound.volume = 0.7;
|
50
|
-
}
|
51
|
-
|
52
|
-
// Function to play a notification sound
|
53
|
-
function playNotificationSound(type) {
|
54
|
-
console.log(`Attempting to play ${type} notification sound`);
|
55
|
-
if (!notificationsEnabled) {
|
56
|
-
console.log('Notifications are disabled');
|
57
|
-
return;
|
58
|
-
}
|
59
|
-
|
60
|
-
// Play sounds regardless of tab focus
|
61
|
-
if (type === 'success' && successSound) {
|
62
|
-
console.log('Playing success sound');
|
63
|
-
successSound.play().catch(err => console.error('Error playing success sound:', err));
|
64
|
-
} else if (type === 'error' && errorSound) {
|
65
|
-
console.log('Playing error sound');
|
66
|
-
errorSound.play().catch(err => console.error('Error playing error sound:', err));
|
67
|
-
} else {
|
68
|
-
console.warn(`Unknown sound type or sound not initialized: ${type}`);
|
69
|
-
}
|
70
|
-
}
|
71
|
-
|
72
|
-
// Initialize socket only when needed with a timeout for safety
|
73
|
-
function initializeSocket() {
|
74
|
-
if (socket) {
|
75
|
-
// If we already have a socket but it's disconnected, reconnect
|
76
|
-
if (!socketConnected) {
|
77
|
-
try {
|
78
|
-
console.log('Socket disconnected, reconnecting...');
|
79
|
-
socket.connect();
|
80
|
-
} catch (e) {
|
81
|
-
console.error('Error reconnecting socket:', e);
|
82
|
-
// Create a new socket
|
83
|
-
socket = null;
|
84
|
-
return initializeSocket();
|
85
|
-
}
|
86
|
-
}
|
87
|
-
return socket;
|
88
|
-
}
|
89
|
-
|
90
|
-
console.log('Initializing socket connection...');
|
91
|
-
// Create new socket connection with optimized settings for threading mode
|
92
|
-
socket = io({
|
93
|
-
path: '/research/socket.io',
|
94
|
-
transports: ['polling', 'websocket'], // Try polling first, then websocket
|
95
|
-
reconnection: true,
|
96
|
-
reconnectionAttempts: 10,
|
97
|
-
reconnectionDelay: 1000,
|
98
|
-
timeout: 25000, // Increase timeout further
|
99
|
-
autoConnect: true,
|
100
|
-
forceNew: true,
|
101
|
-
upgrade: false // Disable automatic transport upgrade for more stability
|
102
|
-
});
|
103
|
-
|
104
|
-
// Add event handlers
|
105
|
-
socket.on('connect', () => {
|
106
|
-
console.log('Socket connected');
|
107
|
-
socketConnected = true;
|
108
|
-
|
109
|
-
// If we're reconnecting and have a current research, resubscribe
|
110
|
-
if (currentResearchId) {
|
111
|
-
console.log(`Reconnected, resubscribing to research ${currentResearchId}`);
|
112
|
-
socket.emit('subscribe_to_research', { research_id: currentResearchId });
|
113
|
-
}
|
114
|
-
});
|
115
|
-
|
116
|
-
socket.on('disconnect', () => {
|
117
|
-
console.log('Socket disconnected');
|
118
|
-
socketConnected = false;
|
119
|
-
});
|
120
|
-
|
121
|
-
socket.on('connect_error', (error) => {
|
122
|
-
console.error('Socket connection error:', error);
|
123
|
-
socketConnected = false;
|
124
|
-
});
|
125
|
-
|
126
|
-
socket.on('error', (error) => {
|
127
|
-
console.error('Socket error:', error);
|
128
|
-
});
|
129
|
-
|
130
|
-
// Set a timeout to detect hanging connections
|
131
|
-
let connectionTimeoutId = setTimeout(() => {
|
132
|
-
if (!socketConnected) {
|
133
|
-
console.log('Socket connection timeout - forcing reconnect');
|
134
|
-
try {
|
135
|
-
if (socket) {
|
136
|
-
// First try to disconnect cleanly
|
137
|
-
try {
|
138
|
-
socket.disconnect();
|
139
|
-
} catch (disconnectErr) {
|
140
|
-
console.warn('Error disconnecting socket during timeout:', disconnectErr);
|
141
|
-
}
|
142
|
-
|
143
|
-
// Then try to reconnect
|
144
|
-
try {
|
145
|
-
socket.connect();
|
146
|
-
} catch (connectErr) {
|
147
|
-
console.warn('Error reconnecting socket during timeout:', connectErr);
|
148
|
-
// Create a new socket only if connect fails
|
149
|
-
socket = null;
|
150
|
-
socket = initializeSocket();
|
151
|
-
}
|
152
|
-
} else {
|
153
|
-
// Socket is already null, just create a new one
|
154
|
-
socket = initializeSocket();
|
155
|
-
}
|
156
|
-
} catch (e) {
|
157
|
-
console.error('Error during forced reconnect:', e);
|
158
|
-
// Force create a new socket
|
159
|
-
socket = null;
|
160
|
-
socket = initializeSocket();
|
161
|
-
}
|
162
|
-
}
|
163
|
-
}, 15000); // Longer timeout before forcing reconnection
|
164
|
-
|
165
|
-
// Clean up timeout if socket connects
|
166
|
-
socket.on('connect', () => {
|
167
|
-
if (connectionTimeoutId) {
|
168
|
-
clearTimeout(connectionTimeoutId);
|
169
|
-
connectionTimeoutId = null;
|
170
|
-
}
|
171
|
-
});
|
172
|
-
|
173
|
-
return socket;
|
174
|
-
}
|
175
|
-
|
176
|
-
// Function to safely disconnect socket
|
177
|
-
window.disconnectSocket = function() {
|
178
|
-
try {
|
179
|
-
if (socket) {
|
180
|
-
console.log('Manually disconnecting socket');
|
181
|
-
try {
|
182
|
-
// First remove all listeners
|
183
|
-
socket.removeAllListeners();
|
184
|
-
} catch (listenerErr) {
|
185
|
-
console.warn('Error removing socket listeners:', listenerErr);
|
186
|
-
}
|
187
|
-
|
188
|
-
try {
|
189
|
-
// Then disconnect
|
190
|
-
socket.disconnect();
|
191
|
-
} catch (disconnectErr) {
|
192
|
-
console.warn('Error during socket disconnect:', disconnectErr);
|
193
|
-
}
|
194
|
-
|
195
|
-
// Always set to null to allow garbage collection
|
196
|
-
socket = null;
|
197
|
-
socketConnected = false;
|
198
|
-
}
|
199
|
-
} catch (e) {
|
200
|
-
console.error('Error disconnecting socket:', e);
|
201
|
-
// Ensure socket is nullified even if errors occur
|
202
|
-
socket = null;
|
203
|
-
socketConnected = false;
|
204
|
-
}
|
205
|
-
};
|
206
|
-
|
207
|
-
// Function to connect to socket for a research
|
208
|
-
window.connectToResearchSocket = async function(researchId) {
|
209
|
-
if (!researchId) {
|
210
|
-
console.error('No research ID provided for socket connection');
|
211
|
-
return;
|
212
|
-
}
|
213
|
-
|
214
|
-
try {
|
215
|
-
// Check if research is terminated/suspended before connecting
|
216
|
-
const response = await fetch(getApiUrl(`/api/research/${researchId}`));
|
217
|
-
const data = await response.json();
|
218
|
-
|
219
|
-
// Don't connect to socket for terminated or suspended research
|
220
|
-
if (data.status === 'suspended' || data.status === 'failed') {
|
221
|
-
console.log(`Not connecting socket for ${data.status} research ${researchId}`);
|
222
|
-
|
223
|
-
// Make sure UI reflects the suspended state
|
224
|
-
updateTerminationUIState('suspended', `Research was ${data.status}`);
|
225
|
-
return;
|
226
|
-
}
|
227
|
-
// Don't connect to completed research
|
228
|
-
else if (data.status === 'completed') {
|
229
|
-
console.log(`Not connecting socket for completed research ${researchId}`);
|
230
|
-
return;
|
231
|
-
}
|
232
|
-
|
233
|
-
console.log(`Connecting to socket for research ${researchId} (status: ${data.status})`);
|
234
|
-
|
235
|
-
// Initialize socket if it doesn't exist
|
236
|
-
if (!socket) {
|
237
|
-
initializeSocket();
|
238
|
-
}
|
239
|
-
|
240
|
-
// Subscribe to the research channel
|
241
|
-
if (socket && socket.connected) {
|
242
|
-
socket.emit('subscribe_to_research', { research_id: researchId });
|
243
|
-
console.log(`Subscribed to research ${researchId}`);
|
244
|
-
} else {
|
245
|
-
console.warn('Socket not connected, waiting for connection...');
|
246
|
-
// Wait for socket to connect
|
247
|
-
const maxAttempts = 5;
|
248
|
-
let attempts = 0;
|
249
|
-
|
250
|
-
const socketConnectInterval = setInterval(() => {
|
251
|
-
attempts++;
|
252
|
-
if (socket && socket.connected) {
|
253
|
-
socket.emit('subscribe_to_research', { research_id: researchId });
|
254
|
-
console.log(`Subscribed to research ${researchId} after ${attempts} attempts`);
|
255
|
-
clearInterval(socketConnectInterval);
|
256
|
-
} else if (attempts >= maxAttempts) {
|
257
|
-
console.error(`Failed to connect to socket after ${maxAttempts} attempts`);
|
258
|
-
clearInterval(socketConnectInterval);
|
259
|
-
addConsoleLog('Failed to connect to real-time updates', 'error');
|
260
|
-
}
|
261
|
-
}, 1000);
|
262
|
-
}
|
263
|
-
} catch (error) {
|
264
|
-
console.error(`Error connecting to socket for research ${researchId}:`, error);
|
265
|
-
}
|
266
|
-
};
|
267
|
-
|
268
|
-
// Format the research status for display
|
269
|
-
function formatStatus(status) {
|
270
|
-
if (!status) return 'Unknown';
|
271
|
-
|
272
|
-
// Handle in_progress specially
|
273
|
-
if (status === 'in_progress') return 'In Progress';
|
274
|
-
|
275
|
-
// Capitalize first letter for other statuses
|
276
|
-
return status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
277
|
-
}
|
278
|
-
|
279
|
-
// Format the research mode for display
|
280
|
-
function formatMode(mode) {
|
281
|
-
if (!mode) return 'Unknown';
|
282
|
-
|
283
|
-
return mode === 'detailed' ? 'Detailed Report' : 'Quick Summary';
|
284
|
-
}
|
285
|
-
|
286
|
-
// Format a date for display
|
287
|
-
function formatDate(date, duration = null) {
|
288
|
-
// Handle null/undefined gracefully
|
289
|
-
if (!date) return 'Unknown';
|
290
|
-
|
291
|
-
// Check if we have a date string instead of a Date object
|
292
|
-
if (typeof date === 'string') {
|
293
|
-
try {
|
294
|
-
// Handle ISO string with microseconds (which causes problems)
|
295
|
-
if (date.includes('.') && date.includes('T')) {
|
296
|
-
// Extract only up to milliseconds (3 digits after dot) or remove microseconds entirely
|
297
|
-
const parts = date.split('.');
|
298
|
-
if (parts.length > 1) {
|
299
|
-
// If there's a Z or + or - after microseconds, preserve it
|
300
|
-
let timezone = '';
|
301
|
-
const microsecondPart = parts[1];
|
302
|
-
const tzIndex = microsecondPart.search(/[Z+-]/);
|
303
|
-
|
304
|
-
if (tzIndex !== -1) {
|
305
|
-
timezone = microsecondPart.substring(tzIndex);
|
306
|
-
}
|
307
|
-
|
308
|
-
// Use only milliseconds (first 3 digits after dot) or none if format issues
|
309
|
-
const milliseconds = microsecondPart.substring(0, Math.min(3, tzIndex !== -1 ? tzIndex : microsecondPart.length));
|
310
|
-
|
311
|
-
// Reconstruct with controlled precision
|
312
|
-
const cleanedDateStr = parts[0] + (milliseconds.length > 0 ? '.' + milliseconds : '') + timezone;
|
313
|
-
date = new Date(cleanedDateStr);
|
314
|
-
} else {
|
315
|
-
date = new Date(date);
|
316
|
-
}
|
317
|
-
} else {
|
318
|
-
date = new Date(date);
|
319
|
-
}
|
320
|
-
} catch (e) {
|
321
|
-
console.warn('Error parsing date string:', e);
|
322
|
-
return 'Invalid date'; // Return error message if we can't parse
|
323
|
-
}
|
324
|
-
}
|
325
|
-
|
326
|
-
// Ensure we're handling the date properly
|
327
|
-
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
328
|
-
console.warn('Invalid date provided to formatDate:', date);
|
329
|
-
return 'Invalid date';
|
330
|
-
}
|
331
|
-
|
332
|
-
// Get current year to compare with date year
|
333
|
-
const currentYear = new Date().getFullYear();
|
334
|
-
const dateYear = date.getFullYear();
|
335
|
-
|
336
|
-
// Get month name, day, and time
|
337
|
-
const month = date.toLocaleString('en-US', { month: 'short' });
|
338
|
-
const day = date.getDate();
|
339
|
-
const hours = date.getHours().toString().padStart(2, '0');
|
340
|
-
const minutes = date.getMinutes().toString().padStart(2, '0');
|
341
|
-
|
342
|
-
// Format like "Feb 25, 08:09" or "Feb 25, 2022, 08:09" if not current year
|
343
|
-
let formattedDate;
|
344
|
-
if (dateYear === currentYear) {
|
345
|
-
formattedDate = `${month} ${day}, ${hours}:${minutes}`;
|
346
|
-
} else {
|
347
|
-
formattedDate = `${month} ${day}, ${dateYear}, ${hours}:${minutes}`;
|
348
|
-
}
|
349
|
-
|
350
|
-
// Add duration if provided
|
351
|
-
if (duration) {
|
352
|
-
let durationText = '';
|
353
|
-
const durationSec = typeof duration === 'number' ? duration : parseInt(duration);
|
354
|
-
|
355
|
-
if (durationSec < 60) {
|
356
|
-
durationText = `${durationSec}s`;
|
357
|
-
} else if (durationSec < 3600) {
|
358
|
-
durationText = `${Math.floor(durationSec / 60)}m ${durationSec % 60}s`;
|
359
|
-
} else {
|
360
|
-
durationText = `${Math.floor(durationSec / 3600)}h ${Math.floor((durationSec % 3600) / 60)}m`;
|
361
|
-
}
|
362
|
-
|
363
|
-
formattedDate += ` (Duration: ${durationText})`;
|
364
|
-
}
|
365
|
-
|
366
|
-
return formattedDate;
|
367
|
-
}
|
368
|
-
|
369
|
-
// Update the socket event handler to fix termination handling
|
370
|
-
window.handleResearchProgressEvent = function(data) {
|
371
|
-
console.log('Research progress update:', data);
|
372
|
-
|
373
|
-
// Extract research ID from the event
|
374
|
-
const eventResearchId = getActiveResearchId();
|
375
|
-
|
376
|
-
// Track processed messages to prevent duplicates
|
377
|
-
window.processedMessages = window.processedMessages || new Set();
|
378
|
-
|
379
|
-
// Add to console log if there's a message
|
380
|
-
if (data.message) {
|
381
|
-
let logType = 'info';
|
382
|
-
|
383
|
-
// Create a unique identifier for this message (message + timestamp if available)
|
384
|
-
const messageId = data.message + (data.log_entry?.time || '');
|
385
|
-
|
386
|
-
// Check if we've already processed this message
|
387
|
-
if (!window.processedMessages.has(messageId)) {
|
388
|
-
window.processedMessages.add(messageId);
|
389
|
-
|
390
|
-
// Determine log type based on status or message content
|
391
|
-
if (data.status === 'failed' || data.status === 'suspended' || data.status === 'terminating') {
|
392
|
-
logType = 'error';
|
393
|
-
} else if (isMilestoneLog(data.message, data.log_entry?.metadata)) {
|
394
|
-
logType = 'milestone';
|
395
|
-
}
|
396
|
-
|
397
|
-
// Store meaningful messages to avoid overwriting with generic messages
|
398
|
-
if (data.message && data.message !== 'Processing research...') {
|
399
|
-
window.lastMeaningfulStatusMessage = data.message;
|
400
|
-
}
|
401
|
-
|
402
|
-
// Extract metadata for search engine information
|
403
|
-
const metadata = data.log_entry?.metadata || null;
|
404
|
-
|
405
|
-
// Pass metadata to addConsoleLog for potential search engine info
|
406
|
-
// Pass the research ID to respect the current viewing context
|
407
|
-
addConsoleLog(data.message, logType, metadata, eventResearchId);
|
408
|
-
}
|
409
|
-
}
|
410
|
-
|
411
|
-
// Add error messages to log
|
412
|
-
if (data.error && !window.processedMessages.has('error:' + data.error)) {
|
413
|
-
window.processedMessages.add('error:' + data.error);
|
414
|
-
addConsoleLog(data.error, 'error', null, eventResearchId);
|
415
|
-
|
416
|
-
// Store error as last meaningful message
|
417
|
-
window.lastMeaningfulStatusMessage = data.error;
|
418
|
-
}
|
419
|
-
|
420
|
-
// Update progress UI if progress is provided
|
421
|
-
if (data.progress !== undefined) {
|
422
|
-
const displayMessage = data.message || window.lastMeaningfulStatusMessage || 'Processing research...';
|
423
|
-
updateProgressUI(data.progress, data.status, displayMessage);
|
424
|
-
}
|
425
|
-
|
426
|
-
// Update detail log if log_entry is provided
|
427
|
-
if (data.log_entry) {
|
428
|
-
updateDetailLogEntry(data.log_entry);
|
429
|
-
}
|
430
|
-
|
431
|
-
// Handle status changes
|
432
|
-
if (data.status) {
|
433
|
-
// Special handling for terminating status and handling already terminated research
|
434
|
-
if (data.status === 'terminating') {
|
435
|
-
// Immediately mark as suspended and update UI
|
436
|
-
isResearchInProgress = false;
|
437
|
-
|
438
|
-
// Update UI state
|
439
|
-
updateTerminationUIState('suspending', data.message || 'Terminating research...');
|
440
|
-
}
|
441
|
-
// Handle suspended research specifically
|
442
|
-
else if (data.status === 'suspended') {
|
443
|
-
console.log('Research was suspended, updating UI directly');
|
444
|
-
|
445
|
-
const researchId = getActiveResearchId();
|
446
|
-
if (!researchId) return;
|
447
|
-
|
448
|
-
// Mark research as not in progress
|
449
|
-
isResearchInProgress = false;
|
450
|
-
|
451
|
-
// Clear polling interval
|
452
|
-
if (pollingInterval) {
|
453
|
-
console.log('Clearing polling interval due to suspension');
|
454
|
-
clearInterval(pollingInterval);
|
455
|
-
pollingInterval = null;
|
456
|
-
}
|
457
|
-
|
458
|
-
// Play error notification sound
|
459
|
-
playNotificationSound('error');
|
460
|
-
|
461
|
-
// Update UI for suspended research
|
462
|
-
updateTerminationUIState('suspended', data.message || 'Research was suspended');
|
463
|
-
|
464
|
-
// Add console log
|
465
|
-
addConsoleLog('Research suspended', 'error');
|
466
|
-
|
467
|
-
// Reset lastMeaningfulStatusMessage for next research
|
468
|
-
window.lastMeaningfulStatusMessage = '';
|
469
|
-
|
470
|
-
// Reset current research ID
|
471
|
-
currentResearchId = null;
|
472
|
-
window.currentResearchId = null;
|
473
|
-
|
474
|
-
// Update navigation
|
475
|
-
updateNavigationBasedOnResearchStatus();
|
476
|
-
|
477
|
-
// Refresh history if on history page
|
478
|
-
if (document.getElementById('history').classList.contains('active')) {
|
479
|
-
loadResearchHistory();
|
480
|
-
}
|
481
|
-
}
|
482
|
-
// Handle completion states
|
483
|
-
else if (data.status === 'completed' || data.status === 'failed') {
|
484
|
-
const researchId = getActiveResearchId();
|
485
|
-
if (!researchId) return;
|
486
|
-
|
487
|
-
// Mark research as not in progress
|
488
|
-
isResearchInProgress = false;
|
489
|
-
|
490
|
-
// Clear polling interval
|
491
|
-
if (pollingInterval) {
|
492
|
-
console.log('Clearing polling interval from socket event');
|
493
|
-
clearInterval(pollingInterval);
|
494
|
-
pollingInterval = null;
|
495
|
-
}
|
496
|
-
|
497
|
-
if (data.status === 'completed') {
|
498
|
-
// Success sound and notification
|
499
|
-
playNotificationSound('success');
|
500
|
-
|
501
|
-
// Store the completed research ID for navigation
|
502
|
-
const completedResearchId = researchId;
|
503
|
-
|
504
|
-
// Reset current research ID
|
505
|
-
currentResearchId = null;
|
506
|
-
window.currentResearchId = null;
|
507
|
-
|
508
|
-
// Reset lastMeaningfulStatusMessage for next research
|
509
|
-
window.lastMeaningfulStatusMessage = '';
|
510
|
-
|
511
|
-
// Update navigation state
|
512
|
-
updateNavigationBasedOnResearchStatus();
|
513
|
-
|
514
|
-
// Navigate to results page with a slight delay to ensure all updates are processed
|
515
|
-
setTimeout(() => {
|
516
|
-
// Load the research results
|
517
|
-
loadResearch(completedResearchId);
|
518
|
-
}, 800);
|
519
|
-
} else {
|
520
|
-
// Error sound and notification
|
521
|
-
playNotificationSound('error');
|
522
|
-
|
523
|
-
// Use the updateTerminationUIState function for consistency
|
524
|
-
updateTerminationUIState('suspended', data.error || `Research was ${data.status}`);
|
525
|
-
|
526
|
-
// Reset lastMeaningfulStatusMessage for next research
|
527
|
-
window.lastMeaningfulStatusMessage = '';
|
528
|
-
|
529
|
-
// Reset current research ID
|
530
|
-
currentResearchId = null;
|
531
|
-
window.currentResearchId = null;
|
532
|
-
|
533
|
-
// Update navigation - important for correctly showing/hiding various elements
|
534
|
-
// based on the current research state
|
535
|
-
updateNavigationBasedOnResearchStatus();
|
536
|
-
}
|
537
|
-
|
538
|
-
// Refresh the history list to show the completed research
|
539
|
-
if (document.getElementById('history').classList.contains('active')) {
|
540
|
-
loadResearchHistory();
|
541
|
-
}
|
542
|
-
}
|
543
|
-
}
|
544
|
-
};
|
545
|
-
|
546
|
-
// Check for active research on page load
|
547
|
-
async function checkActiveResearch() {
|
548
|
-
try {
|
549
|
-
const response = await fetch(getApiUrl('/api/history'));
|
550
|
-
const history = await response.json();
|
551
|
-
|
552
|
-
// Find in-progress research
|
553
|
-
const activeResearch = history.find(item => item.status === 'in_progress');
|
554
|
-
|
555
|
-
if (activeResearch) {
|
556
|
-
// Verify the research is truly active by checking its details
|
557
|
-
try {
|
558
|
-
const detailsResponse = await fetch(getApiUrl(`/api/research/${activeResearch.id}`));
|
559
|
-
const details = await detailsResponse.json();
|
560
|
-
|
561
|
-
// If status is not in_progress in the details, it's stale
|
562
|
-
if (details.status !== 'in_progress') {
|
563
|
-
console.log(`Research ${activeResearch.id} is stale (status: ${details.status}), ignoring`);
|
564
|
-
return;
|
565
|
-
}
|
566
|
-
|
567
|
-
// Check when the research was started - if it's been more than 1 hour, it might be stale
|
568
|
-
if (details.created_at) {
|
569
|
-
const startTime = new Date(details.created_at);
|
570
|
-
const currentTime = new Date();
|
571
|
-
const hoursSinceStart = (currentTime - startTime) / (1000 * 60 * 60);
|
572
|
-
|
573
|
-
if (hoursSinceStart > 1) {
|
574
|
-
console.log(`Research ${activeResearch.id} has been running for ${hoursSinceStart.toFixed(2)} hours, which is unusually long. Checking for activity...`);
|
575
|
-
|
576
|
-
// Check if there has been log activity in the last 10 minutes
|
577
|
-
let recentActivity = false;
|
578
|
-
if (details.log && Array.isArray(details.log) && details.log.length > 0) {
|
579
|
-
const lastLogTime = new Date(details.log[details.log.length - 1].time);
|
580
|
-
const minutesSinceLastLog = (currentTime - lastLogTime) / (1000 * 60);
|
581
|
-
|
582
|
-
if (minutesSinceLastLog < 10) {
|
583
|
-
recentActivity = true;
|
584
|
-
} else {
|
585
|
-
console.log(`No recent activity for ${minutesSinceLastLog.toFixed(2)} minutes, treating as stale`);
|
586
|
-
return;
|
587
|
-
}
|
588
|
-
}
|
589
|
-
}
|
590
|
-
}
|
591
|
-
|
592
|
-
// If we get here, the research seems to be genuinely active
|
593
|
-
isResearchInProgress = true;
|
594
|
-
currentResearchId = activeResearch.id;
|
595
|
-
window.currentResearchId = currentResearchId;
|
596
|
-
|
597
|
-
// Check if we're on the new research page and redirect to progress
|
598
|
-
const currentPage = document.querySelector('.page.active');
|
599
|
-
|
600
|
-
if (currentPage && currentPage.id === 'new-research') {
|
601
|
-
// Navigate to progress page
|
602
|
-
switchPage('research-progress');
|
603
|
-
|
604
|
-
// Connect to socket for this research
|
605
|
-
window.connectToResearchSocket(currentResearchId);
|
606
|
-
|
607
|
-
// Start polling for updates
|
608
|
-
pollResearchStatus(currentResearchId);
|
609
|
-
}
|
610
|
-
} catch (detailsError) {
|
611
|
-
console.error('Error checking research details:', detailsError);
|
612
|
-
}
|
613
|
-
}
|
614
|
-
} catch (error) {
|
615
|
-
console.error('Error checking for active research:', error);
|
616
|
-
}
|
617
|
-
}
|
618
|
-
|
619
|
-
// Add unload event listener
|
620
|
-
window.addEventListener('beforeunload', function() {
|
621
|
-
window.disconnectSocket();
|
622
|
-
});
|
623
|
-
|
624
|
-
// Function to start research
|
625
|
-
async function startResearch(query, mode) {
|
626
|
-
// First validate that we have a query
|
627
|
-
if (!query || query.trim() === '') {
|
628
|
-
alert('Please enter a query');
|
629
|
-
return;
|
630
|
-
}
|
631
|
-
|
632
|
-
try {
|
633
|
-
// Update button state
|
634
|
-
const startBtn = document.getElementById('start-research-btn');
|
635
|
-
if (startBtn) {
|
636
|
-
startBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
|
637
|
-
startBtn.disabled = true;
|
638
|
-
}
|
639
|
-
|
640
|
-
// Clear any previous research
|
641
|
-
resetResearchState();
|
642
|
-
|
643
|
-
// Set favicon to loading
|
644
|
-
setFavicon('loading');
|
645
|
-
|
646
|
-
// Get the current query element
|
647
|
-
const currentQueryEl = document.getElementById('current-query');
|
648
|
-
if (currentQueryEl) {
|
649
|
-
currentQueryEl.textContent = query;
|
650
|
-
}
|
651
|
-
|
652
|
-
// Create payload
|
653
|
-
const payload = {
|
654
|
-
query: query,
|
655
|
-
mode: mode || 'quick'
|
656
|
-
};
|
657
|
-
|
658
|
-
// Call the API
|
659
|
-
const response = await fetch(getApiUrl('/api/start_research'), {
|
660
|
-
method: 'POST',
|
661
|
-
headers: {
|
662
|
-
'Content-Type': 'application/json'
|
663
|
-
},
|
664
|
-
body: JSON.stringify(payload)
|
665
|
-
});
|
666
|
-
|
667
|
-
// Parse the response
|
668
|
-
const result = await response.json();
|
669
|
-
|
670
|
-
if (result.status === 'success') {
|
671
|
-
// Update the current research ID
|
672
|
-
currentResearchId = result.research_id;
|
673
|
-
window.currentResearchId = result.research_id;
|
674
|
-
|
675
|
-
console.log(`Started research with ID: ${currentResearchId}`);
|
676
|
-
|
677
|
-
// Mark as in progress
|
678
|
-
isResearchInProgress = true;
|
679
|
-
|
680
|
-
// Hide the try again button if visible
|
681
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
682
|
-
if (tryAgainBtn) {
|
683
|
-
tryAgainBtn.style.display = 'none';
|
684
|
-
}
|
685
|
-
|
686
|
-
// Reset progress UI
|
687
|
-
updateProgressUI(0, 'in_progress', `Researching: ${query}`);
|
688
|
-
|
689
|
-
// Store query in case we need to display it again
|
690
|
-
window.currentResearchQuery = query;
|
691
|
-
|
692
|
-
// Update navigation
|
693
|
-
updateNavigationBasedOnResearchStatus();
|
694
|
-
|
695
|
-
// Navigate to the progress page
|
696
|
-
switchPage('research-progress');
|
697
|
-
|
698
|
-
// Connect to the socket for this research
|
699
|
-
window.connectToResearchSocket(currentResearchId);
|
700
|
-
|
701
|
-
// Start polling for status
|
702
|
-
pollResearchStatus(currentResearchId);
|
703
|
-
} else {
|
704
|
-
// Handle error
|
705
|
-
const errorMessage = result.message || 'Failed to start research';
|
706
|
-
console.error('Research start error:', errorMessage);
|
707
|
-
|
708
|
-
// Add error to log
|
709
|
-
addConsoleLog(`Error: ${errorMessage}`, 'error');
|
710
|
-
|
711
|
-
alert(errorMessage);
|
712
|
-
|
713
|
-
// Reset the favicon
|
714
|
-
setFavicon('default');
|
715
|
-
|
716
|
-
// Reset button state
|
717
|
-
if (startBtn) {
|
718
|
-
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
719
|
-
startBtn.disabled = false;
|
720
|
-
}
|
721
|
-
}
|
722
|
-
} catch (error) {
|
723
|
-
console.error('Error starting research:', error);
|
724
|
-
|
725
|
-
// Add error to log
|
726
|
-
addConsoleLog(`Error: ${error.message}`, 'error');
|
727
|
-
|
728
|
-
// Reset the favicon
|
729
|
-
setFavicon('default');
|
730
|
-
|
731
|
-
// Reset button state
|
732
|
-
const startBtn = document.getElementById('start-research-btn');
|
733
|
-
if (startBtn) {
|
734
|
-
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
735
|
-
startBtn.disabled = false;
|
736
|
-
}
|
737
|
-
|
738
|
-
alert('An error occurred while starting the research. Please try again.');
|
739
|
-
}
|
740
|
-
}
|
741
|
-
|
742
|
-
// Function to clear any existing polling interval
|
743
|
-
function clearPollingInterval() {
|
744
|
-
if (pollingInterval) {
|
745
|
-
console.log('Clearing existing polling interval');
|
746
|
-
clearInterval(pollingInterval);
|
747
|
-
pollingInterval = null;
|
748
|
-
}
|
749
|
-
}
|
750
|
-
|
751
|
-
// Update the polling function to prevent "Processing research..." from overwriting actual status messages
|
752
|
-
function pollResearchStatus(researchId) {
|
753
|
-
if (!researchId) {
|
754
|
-
console.error('No research ID provided for polling');
|
755
|
-
return;
|
756
|
-
}
|
757
|
-
|
758
|
-
// Reset message deduplication when starting a new poll
|
759
|
-
window.processedMessages = window.processedMessages || new Set();
|
760
|
-
|
761
|
-
// Don't set "Loading research..." here, as we'll set the actual query from the response
|
762
|
-
// Only set loading if currentQuery is empty
|
763
|
-
const currentQueryEl = document.getElementById('current-query');
|
764
|
-
if (currentQueryEl && (!currentQueryEl.textContent || currentQueryEl.textContent === 'Loading research...')) {
|
765
|
-
currentQueryEl.textContent = window.currentResearchQuery || 'Loading research...';
|
766
|
-
}
|
767
|
-
|
768
|
-
console.log(`Starting polling for research ${researchId}`);
|
769
|
-
|
770
|
-
// Store the last real message to avoid overwriting with generic messages
|
771
|
-
window.lastMeaningfulStatusMessage = window.lastMeaningfulStatusMessage || '';
|
772
|
-
|
773
|
-
// Ensure we have a socket connection
|
774
|
-
if (typeof window.connectToResearchSocket === 'function') {
|
775
|
-
window.connectToResearchSocket(researchId);
|
776
|
-
}
|
777
|
-
|
778
|
-
// Set polling interval for updates
|
779
|
-
if (pollingInterval) {
|
780
|
-
clearInterval(pollingInterval);
|
781
|
-
}
|
782
|
-
|
783
|
-
// Make an immediate request
|
784
|
-
checkResearchStatus(researchId);
|
785
|
-
|
786
|
-
// Then set up polling
|
787
|
-
pollingInterval = setInterval(() => {
|
788
|
-
checkResearchStatus(researchId);
|
789
|
-
}, 2000); // Poll every 2 seconds
|
790
|
-
|
791
|
-
// Function to check research status
|
792
|
-
function checkResearchStatus(researchId) {
|
793
|
-
fetch(getApiUrl(`/api/research/${researchId}/details`))
|
794
|
-
.then(response => response.json())
|
795
|
-
.then(data => {
|
796
|
-
// Update the current query with the actual query from the research
|
797
|
-
const currentQueryEl = document.getElementById('current-query');
|
798
|
-
if (currentQueryEl && data.query) {
|
799
|
-
currentQueryEl.textContent = data.query;
|
800
|
-
// Store the query in case we need it later
|
801
|
-
window.currentResearchQuery = data.query;
|
802
|
-
}
|
803
|
-
|
804
|
-
// Process status update
|
805
|
-
if (data && data.status !== 'error') {
|
806
|
-
// Update UI with progress
|
807
|
-
const progress = data.progress || 0;
|
808
|
-
const status = data.status || 'in_progress';
|
809
|
-
|
810
|
-
// Get most recent message
|
811
|
-
let message = '';
|
812
|
-
let foundNewMessage = false;
|
813
|
-
let latestMetadata = null;
|
814
|
-
|
815
|
-
if (data.log && data.log.length > 0) {
|
816
|
-
// Get the latest unique log entry
|
817
|
-
for (let i = data.log.length - 1; i >= 0; i--) {
|
818
|
-
const latestLog = data.log[i];
|
819
|
-
const messageId = latestLog.message + (latestLog.time || '');
|
820
|
-
|
821
|
-
if (!window.processedMessages.has(messageId)) {
|
822
|
-
window.processedMessages.add(messageId);
|
823
|
-
message = latestLog.message || '';
|
824
|
-
latestMetadata = latestLog.metadata || null;
|
825
|
-
|
826
|
-
// Only update the lastMeaningfulStatusMessage if we have a real message
|
827
|
-
if (message && message !== 'Processing research...') {
|
828
|
-
window.lastMeaningfulStatusMessage = message;
|
829
|
-
foundNewMessage = true;
|
830
|
-
}
|
831
|
-
|
832
|
-
// Add to console logs
|
833
|
-
if (message) {
|
834
|
-
let logType = 'info';
|
835
|
-
if (isMilestoneLog(message, latestLog.metadata)) {
|
836
|
-
logType = 'milestone';
|
837
|
-
} else if (latestLog.type === 'error' || (latestLog.metadata && latestLog.metadata.phase === 'error')) {
|
838
|
-
logType = 'error';
|
839
|
-
} else if (latestLog.type) {
|
840
|
-
logType = latestLog.type; // Use the type from the database if available
|
841
|
-
}
|
842
|
-
addConsoleLog(message, logType, latestMetadata);
|
843
|
-
}
|
844
|
-
|
845
|
-
break; // Only process one message per poll
|
846
|
-
}
|
847
|
-
}
|
848
|
-
}
|
849
|
-
|
850
|
-
// Use a meaningful message if available; otherwise, keep the last good one
|
851
|
-
const displayMessage = foundNewMessage ? message :
|
852
|
-
(window.lastMeaningfulStatusMessage || 'Processing research...');
|
853
|
-
|
854
|
-
// Update progress UI
|
855
|
-
updateProgressUI(progress, status, displayMessage);
|
856
|
-
|
857
|
-
// Update the UI based on research status
|
858
|
-
if (status === 'completed' || status === 'failed' || status === 'suspended') {
|
859
|
-
// Clear polling interval
|
860
|
-
if (pollingInterval) {
|
861
|
-
console.log('Clearing polling interval due to status change');
|
862
|
-
clearInterval(pollingInterval);
|
863
|
-
pollingInterval = null;
|
864
|
-
}
|
865
|
-
|
866
|
-
// Handle completion or failure
|
867
|
-
if (status === 'completed') {
|
868
|
-
addConsoleLog('Research completed successfully', 'milestone');
|
869
|
-
playNotificationSound('success');
|
870
|
-
|
871
|
-
// Store the completed research ID for navigation
|
872
|
-
const completedResearchId = researchId;
|
873
|
-
|
874
|
-
// Reset current research ID
|
875
|
-
currentResearchId = null;
|
876
|
-
window.currentResearchId = null;
|
877
|
-
|
878
|
-
// Update navigation state
|
879
|
-
isResearchInProgress = false;
|
880
|
-
updateNavigationBasedOnResearchStatus();
|
881
|
-
|
882
|
-
// Reset lastMeaningfulStatusMessage for next research
|
883
|
-
window.lastMeaningfulStatusMessage = '';
|
884
|
-
|
885
|
-
// Navigate to results page with a slight delay to ensure all updates are processed
|
886
|
-
setTimeout(() => {
|
887
|
-
// Load the research results
|
888
|
-
loadResearch(completedResearchId);
|
889
|
-
}, 800);
|
890
|
-
} else {
|
891
|
-
addConsoleLog(`Research ${status}`, 'error');
|
892
|
-
playNotificationSound('error');
|
893
|
-
|
894
|
-
// Show error message and Try Again button
|
895
|
-
const errorMessage = document.getElementById('error-message');
|
896
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
897
|
-
|
898
|
-
if (errorMessage) {
|
899
|
-
errorMessage.textContent = data.error || `Research was ${status}`;
|
900
|
-
errorMessage.style.display = 'block';
|
901
|
-
}
|
902
|
-
|
903
|
-
if (tryAgainBtn) {
|
904
|
-
tryAgainBtn.style.display = 'block';
|
905
|
-
}
|
906
|
-
|
907
|
-
// Reset lastMeaningfulStatusMessage for next research
|
908
|
-
window.lastMeaningfulStatusMessage = '';
|
909
|
-
|
910
|
-
// Update navigation
|
911
|
-
isResearchInProgress = false;
|
912
|
-
currentResearchId = null;
|
913
|
-
window.currentResearchId = null;
|
914
|
-
updateNavigationBasedOnResearchStatus();
|
915
|
-
}
|
916
|
-
} else {
|
917
|
-
// Research is still in progress
|
918
|
-
isResearchInProgress = true;
|
919
|
-
currentResearchId = researchId;
|
920
|
-
window.currentResearchId = researchId;
|
921
|
-
}
|
922
|
-
}
|
923
|
-
})
|
924
|
-
.catch(error => {
|
925
|
-
console.error('Error polling research status:', error);
|
926
|
-
// Don't clear the interval on error - just keep trying
|
927
|
-
});
|
928
|
-
}
|
929
|
-
}
|
930
|
-
|
931
|
-
// Function to reset the start research button to its default state
|
932
|
-
function resetStartResearchButton() {
|
933
|
-
const startBtn = document.getElementById('start-research-btn');
|
934
|
-
if (startBtn) {
|
935
|
-
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
936
|
-
startBtn.disabled = false;
|
937
|
-
}
|
938
|
-
}
|
939
|
-
|
940
|
-
// Main initialization function
|
941
|
-
function initializeApp() {
|
942
|
-
console.log('Initializing application...');
|
943
|
-
|
944
|
-
// Initialize the sounds
|
945
|
-
initializeSounds();
|
946
|
-
|
947
|
-
// Initialize socket connection
|
948
|
-
initializeSocket();
|
949
|
-
|
950
|
-
// Create a dynamic favicon with the lightning emoji by default
|
951
|
-
createDynamicFavicon('⚡');
|
952
|
-
|
953
|
-
// Add try again button handler
|
954
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
955
|
-
if (tryAgainBtn) {
|
956
|
-
tryAgainBtn.addEventListener('click', function() {
|
957
|
-
// Switch back to the new research page
|
958
|
-
switchPage('new-research');
|
959
|
-
|
960
|
-
// Reset the research state
|
961
|
-
resetResearchState();
|
962
|
-
|
963
|
-
// Reset the Start Research button
|
964
|
-
resetStartResearchButton();
|
965
|
-
});
|
966
|
-
}
|
967
|
-
|
968
|
-
// Get navigation elements
|
969
|
-
const navItems = document.querySelectorAll('.sidebar-nav li');
|
970
|
-
const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
|
971
|
-
const pages = document.querySelectorAll('.page');
|
972
|
-
const mobileTabBar = document.querySelector('.mobile-tab-bar');
|
973
|
-
const logo = document.getElementById('logo-link');
|
974
|
-
|
975
|
-
// Handle responsive navigation based on screen size
|
976
|
-
function handleResponsiveNavigation() {
|
977
|
-
// Mobile tab bar should only be visible on small screens
|
978
|
-
if (window.innerWidth <= 767) {
|
979
|
-
if (mobileTabBar) {
|
980
|
-
mobileTabBar.style.display = 'flex';
|
981
|
-
}
|
982
|
-
} else {
|
983
|
-
if (mobileTabBar) {
|
984
|
-
mobileTabBar.style.display = 'none';
|
985
|
-
}
|
986
|
-
}
|
987
|
-
}
|
988
|
-
|
989
|
-
// Call on initial load
|
990
|
-
handleResponsiveNavigation();
|
991
|
-
|
992
|
-
// Add resize listener for responsive design
|
993
|
-
window.addEventListener('resize', handleResponsiveNavigation);
|
994
|
-
|
995
|
-
// Handle logo click
|
996
|
-
if (logo) {
|
997
|
-
logo.addEventListener('click', () => {
|
998
|
-
switchPage('new-research');
|
999
|
-
resetStartResearchButton();
|
1000
|
-
});
|
1001
|
-
}
|
1002
|
-
|
1003
|
-
// Setup navigation click handlers
|
1004
|
-
navItems.forEach(item => {
|
1005
|
-
if (!item.classList.contains('external-link')) {
|
1006
|
-
item.addEventListener('click', function() {
|
1007
|
-
const pageId = this.dataset.page;
|
1008
|
-
if (pageId) {
|
1009
|
-
switchPage(pageId);
|
1010
|
-
// Reset Start Research button when returning to the form
|
1011
|
-
if (pageId === 'new-research') {
|
1012
|
-
resetStartResearchButton();
|
1013
|
-
}
|
1014
|
-
}
|
1015
|
-
});
|
1016
|
-
}
|
1017
|
-
});
|
1018
|
-
|
1019
|
-
mobileNavItems.forEach(item => {
|
1020
|
-
if (!item.classList.contains('external-link')) {
|
1021
|
-
item.addEventListener('click', function() {
|
1022
|
-
const pageId = this.dataset.page;
|
1023
|
-
if (pageId) {
|
1024
|
-
switchPage(pageId);
|
1025
|
-
// Reset Start Research button when returning to the form
|
1026
|
-
if (pageId === 'new-research') {
|
1027
|
-
resetStartResearchButton();
|
1028
|
-
}
|
1029
|
-
}
|
1030
|
-
});
|
1031
|
-
}
|
1032
|
-
});
|
1033
|
-
|
1034
|
-
// Setup form submission
|
1035
|
-
const researchForm = document.getElementById('research-form');
|
1036
|
-
if (researchForm) {
|
1037
|
-
researchForm.addEventListener('submit', function(e) {
|
1038
|
-
e.preventDefault();
|
1039
|
-
const query = document.getElementById('query').value.trim();
|
1040
|
-
if (!query) {
|
1041
|
-
alert('Please enter a research query');
|
1042
|
-
return;
|
1043
|
-
}
|
1044
|
-
|
1045
|
-
const mode = document.querySelector('.mode-option.active')?.dataset.mode || 'quick';
|
1046
|
-
startResearch(query, mode);
|
1047
|
-
});
|
1048
|
-
}
|
1049
|
-
|
1050
|
-
// Initialize research mode selection
|
1051
|
-
const modeOptions = document.querySelectorAll('.mode-option');
|
1052
|
-
modeOptions.forEach(option => {
|
1053
|
-
option.addEventListener('click', function() {
|
1054
|
-
modeOptions.forEach(opt => opt.classList.remove('active'));
|
1055
|
-
this.classList.add('active');
|
1056
|
-
|
1057
|
-
// Update favicon based on selected mode
|
1058
|
-
const mode = this.dataset.mode;
|
1059
|
-
setFavicon(mode);
|
1060
|
-
});
|
1061
|
-
});
|
1062
|
-
|
1063
|
-
// Load research history initially
|
1064
|
-
if (document.getElementById('history-list')) {
|
1065
|
-
loadResearchHistory();
|
1066
|
-
}
|
1067
|
-
|
1068
|
-
// Check for active research
|
1069
|
-
checkActiveResearch();
|
1070
|
-
|
1071
|
-
// Setup notification toggle and other form elements
|
1072
|
-
setupResearchForm();
|
1073
|
-
|
1074
|
-
console.log('Application initialized');
|
1075
|
-
}
|
1076
|
-
|
1077
|
-
// Initialize the app
|
1078
|
-
initializeApp();
|
1079
|
-
|
1080
|
-
// Function to switch between pages
|
1081
|
-
function switchPage(pageId) {
|
1082
|
-
// First hide all pages
|
1083
|
-
const pages = document.querySelectorAll('.page');
|
1084
|
-
pages.forEach(page => page.classList.remove('active'));
|
1085
|
-
|
1086
|
-
// Then activate the selected page
|
1087
|
-
const selectedPage = document.getElementById(pageId);
|
1088
|
-
if (selectedPage) {
|
1089
|
-
selectedPage.classList.add('active');
|
1090
|
-
}
|
1091
|
-
|
1092
|
-
// Update the URL hash
|
1093
|
-
window.location.hash = '#' + pageId;
|
1094
|
-
|
1095
|
-
// Update the navigation UI to highlight the active page
|
1096
|
-
updateNavigationUI(pageId);
|
1097
|
-
|
1098
|
-
// Clear console logs when switching to pages that don't show research details
|
1099
|
-
if (pageId === 'new-research' || pageId === 'history') {
|
1100
|
-
clearConsoleLogs();
|
1101
|
-
// Reset the viewing research ID when navigating to non-research pages
|
1102
|
-
viewingResearchId = null;
|
1103
|
-
}
|
1104
|
-
|
1105
|
-
// Special handling for history page
|
1106
|
-
if (pageId === 'history') {
|
1107
|
-
loadResearchHistory();
|
1108
|
-
}
|
1109
|
-
|
1110
|
-
// Reset scroll position for the newly activated page
|
1111
|
-
window.scrollTo(0, 0);
|
1112
|
-
|
1113
|
-
console.log(`Switched to page: ${pageId}`);
|
1114
|
-
|
1115
|
-
// Update the log panel visibility
|
1116
|
-
updateLogPanelVisibility(pageId);
|
1117
|
-
}
|
1118
|
-
|
1119
|
-
// Track termination status
|
1120
|
-
let isTerminating = false;
|
1121
|
-
|
1122
|
-
// Check if we're on the history page and load history if needed
|
1123
|
-
const historyPage = document.getElementById('history');
|
1124
|
-
if (historyPage && historyPage.classList.contains('active')) {
|
1125
|
-
// Use setTimeout to ensure the DOM is fully loaded
|
1126
|
-
setTimeout(() => loadResearchHistory(), 100);
|
1127
|
-
}
|
1128
|
-
|
1129
|
-
// Add a prefix helper function at the top of the file
|
1130
|
-
function getApiUrl(path) {
|
1131
|
-
// This function adds the /research prefix to all API URLs
|
1132
|
-
return `/research${path}`;
|
1133
|
-
}
|
1134
|
-
|
1135
|
-
// Function to properly disconnect all socket connections
|
1136
|
-
function disconnectAllSockets() {
|
1137
|
-
if (socket) {
|
1138
|
-
try {
|
1139
|
-
console.log('Disconnecting all socket connections');
|
1140
|
-
|
1141
|
-
// Get the active research ID
|
1142
|
-
const researchId = getActiveResearchId();
|
1143
|
-
|
1144
|
-
// If there's an active research, unsubscribe first
|
1145
|
-
if (researchId) {
|
1146
|
-
console.log(`Unsubscribing from research ${researchId}`);
|
1147
|
-
socket.emit('unsubscribe_from_research', { research_id: researchId });
|
1148
|
-
}
|
1149
|
-
|
1150
|
-
// Also attempt to disconnect the socket
|
1151
|
-
socket.disconnect();
|
1152
|
-
socket = null;
|
1153
|
-
|
1154
|
-
console.log('Socket disconnected successfully');
|
1155
|
-
} catch (error) {
|
1156
|
-
console.error('Error disconnecting socket:', error);
|
1157
|
-
}
|
1158
|
-
}
|
1159
|
-
}
|
1160
|
-
|
1161
|
-
// Update the terminateResearch function to handle termination more gracefully
|
1162
|
-
async function terminateResearch(researchId) {
|
1163
|
-
if (!researchId) {
|
1164
|
-
console.error('No research ID provided for termination');
|
1165
|
-
return;
|
1166
|
-
}
|
1167
|
-
|
1168
|
-
// Prevent multiple termination requests
|
1169
|
-
if (document.getElementById('terminate-research-btn')?.disabled) {
|
1170
|
-
console.log('Termination already in progress');
|
1171
|
-
return;
|
1172
|
-
}
|
1173
|
-
|
1174
|
-
// Confirm with the user
|
1175
|
-
if (!confirm('Are you sure you want to terminate this research? This action cannot be undone.')) {
|
1176
|
-
return;
|
1177
|
-
}
|
1178
|
-
|
1179
|
-
console.log(`Attempting to terminate research: ${researchId}`);
|
1180
|
-
|
1181
|
-
try {
|
1182
|
-
// Get the terminate button
|
1183
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
1184
|
-
if (terminateBtn) {
|
1185
|
-
// Disable the button to prevent multiple clicks
|
1186
|
-
terminateBtn.disabled = true;
|
1187
|
-
terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
1188
|
-
}
|
1189
|
-
|
1190
|
-
// Update UI to show terminating state immediately
|
1191
|
-
updateTerminationUIState('suspending', 'Terminating research...');
|
1192
|
-
|
1193
|
-
// Add a log entry
|
1194
|
-
addConsoleLog('Terminating research...', 'error');
|
1195
|
-
|
1196
|
-
// Immediately mark the research as terminated in our app state
|
1197
|
-
// so we don't reconnect to it
|
1198
|
-
isResearchInProgress = false;
|
1199
|
-
|
1200
|
-
// Disconnect all sockets to ensure we stop receiving updates
|
1201
|
-
disconnectAllSockets();
|
1202
|
-
|
1203
|
-
// Clear the polling interval immediately
|
1204
|
-
if (pollingInterval) {
|
1205
|
-
clearInterval(pollingInterval);
|
1206
|
-
pollingInterval = null;
|
1207
|
-
console.log('Cleared polling interval during termination');
|
1208
|
-
}
|
1209
|
-
|
1210
|
-
// Call the API to terminate
|
1211
|
-
const response = await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
|
1212
|
-
method: 'POST',
|
1213
|
-
headers: {
|
1214
|
-
'Content-Type': 'application/json'
|
1215
|
-
}
|
1216
|
-
});
|
1217
|
-
|
1218
|
-
const data = await response.json();
|
1219
|
-
|
1220
|
-
if (data.status === 'success') {
|
1221
|
-
console.log(`Research ${researchId} termination requested successfully`);
|
1222
|
-
|
1223
|
-
// Add termination log
|
1224
|
-
addConsoleLog('Research termination requested. Please wait...', 'error');
|
1225
|
-
|
1226
|
-
// Immediately update UI for better responsiveness
|
1227
|
-
updateTerminationUIState('suspended', 'Research was terminated');
|
1228
|
-
|
1229
|
-
// Start polling for the suspended status with a more reliable approach
|
1230
|
-
let checkAttempts = 0;
|
1231
|
-
const maxAttempts = 3; // Reduced from 5
|
1232
|
-
const checkInterval = setInterval(async () => {
|
1233
|
-
checkAttempts++;
|
1234
|
-
console.log(`Checking termination status (attempt ${checkAttempts}/${maxAttempts})...`);
|
1235
|
-
|
1236
|
-
try {
|
1237
|
-
const statusResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
|
1238
|
-
const statusData = await statusResponse.json();
|
1239
|
-
|
1240
|
-
// Check for actual termination status in the response
|
1241
|
-
if (statusData.status === 'suspended' || statusData.status === 'failed') {
|
1242
|
-
console.log(`Research is now ${statusData.status}, updating UI`);
|
1243
|
-
clearInterval(checkInterval);
|
1244
|
-
|
1245
|
-
// Reset research state
|
1246
|
-
currentResearchId = null;
|
1247
|
-
window.currentResearchId = null;
|
1248
|
-
|
1249
|
-
// Disconnect any remaining sockets again, just to be sure
|
1250
|
-
disconnectAllSockets();
|
1251
|
-
|
1252
|
-
// Update navigation
|
1253
|
-
updateNavigationBasedOnResearchStatus();
|
1254
|
-
|
1255
|
-
return;
|
1256
|
-
}
|
1257
|
-
|
1258
|
-
// If we reach the maximum attempts but status isn't updated yet
|
1259
|
-
if (checkAttempts >= maxAttempts) {
|
1260
|
-
console.log('Max termination check attempts reached, forcing UI update');
|
1261
|
-
clearInterval(checkInterval);
|
1262
|
-
|
1263
|
-
// Force update to suspended state even if backend hasn't caught up yet
|
1264
|
-
currentResearchId = null;
|
1265
|
-
window.currentResearchId = null;
|
1266
|
-
|
1267
|
-
// Disconnect any remaining sockets
|
1268
|
-
disconnectAllSockets();
|
1269
|
-
|
1270
|
-
// Update database status directly with a second termination request
|
1271
|
-
try {
|
1272
|
-
console.log('Sending second termination request to ensure completion');
|
1273
|
-
await fetch(getApiUrl(`/api/research/${researchId}/terminate`), {
|
1274
|
-
method: 'POST',
|
1275
|
-
headers: { 'Content-Type': 'application/json' }
|
1276
|
-
});
|
1277
|
-
} catch (secondError) {
|
1278
|
-
console.error('Error sending second termination request:', secondError);
|
1279
|
-
}
|
1280
|
-
|
1281
|
-
updateNavigationBasedOnResearchStatus();
|
1282
|
-
}
|
1283
|
-
} catch (checkError) {
|
1284
|
-
console.error(`Error checking termination status: ${checkError}`);
|
1285
|
-
if (checkAttempts >= maxAttempts) {
|
1286
|
-
clearInterval(checkInterval);
|
1287
|
-
updateTerminationUIState('error', 'Error checking termination status');
|
1288
|
-
}
|
1289
|
-
}
|
1290
|
-
}, 300); // Check faster for more responsive feedback
|
1291
|
-
|
1292
|
-
} else {
|
1293
|
-
console.error(`Error terminating research: ${data.message}`);
|
1294
|
-
addConsoleLog(`Error terminating research: ${data.message}`, 'error');
|
1295
|
-
|
1296
|
-
// Re-enable the button
|
1297
|
-
if (terminateBtn) {
|
1298
|
-
terminateBtn.disabled = false;
|
1299
|
-
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
1300
|
-
}
|
1301
|
-
}
|
1302
|
-
} catch (error) {
|
1303
|
-
console.error(`Error in terminate request: ${error}`);
|
1304
|
-
addConsoleLog(`Error in terminate request: ${error}`, 'error');
|
1305
|
-
|
1306
|
-
// Re-enable the button
|
1307
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
1308
|
-
if (terminateBtn) {
|
1309
|
-
terminateBtn.disabled = false;
|
1310
|
-
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
1311
|
-
}
|
1312
|
-
}
|
1313
|
-
}
|
1314
|
-
|
1315
|
-
// Helper function to update UI elements during termination
|
1316
|
-
function updateTerminationUIState(state, message) {
|
1317
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
1318
|
-
const errorMessage = document.getElementById('error-message');
|
1319
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
1320
|
-
const progressStatus = document.getElementById('progress-status');
|
1321
|
-
const progressBar = document.getElementById('progress-bar');
|
1322
|
-
|
1323
|
-
switch (state) {
|
1324
|
-
case 'suspending':
|
1325
|
-
if (progressStatus) {
|
1326
|
-
progressStatus.textContent = 'Terminating research...';
|
1327
|
-
progressStatus.className = 'progress-status status-terminating fade-in';
|
1328
|
-
}
|
1329
|
-
if (progressBar) {
|
1330
|
-
progressBar.classList.remove('suspended-status');
|
1331
|
-
}
|
1332
|
-
if (terminateBtn) {
|
1333
|
-
terminateBtn.disabled = true;
|
1334
|
-
terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
1335
|
-
}
|
1336
|
-
if (errorMessage) {
|
1337
|
-
errorMessage.style.display = 'none';
|
1338
|
-
}
|
1339
|
-
if (tryAgainBtn) {
|
1340
|
-
tryAgainBtn.style.display = 'none';
|
1341
|
-
}
|
1342
|
-
break;
|
1343
|
-
|
1344
|
-
case 'suspended':
|
1345
|
-
if (progressStatus) {
|
1346
|
-
progressStatus.innerHTML = '<i class="fas fa-exclamation-triangle termination-icon"></i> ' + (message || 'Research was suspended');
|
1347
|
-
progressStatus.className = 'progress-status status-failed fade-in';
|
1348
|
-
}
|
1349
|
-
if (progressBar) {
|
1350
|
-
progressBar.classList.add('suspended-status');
|
1351
|
-
}
|
1352
|
-
if (terminateBtn) {
|
1353
|
-
terminateBtn.style.display = 'none';
|
1354
|
-
}
|
1355
|
-
if (errorMessage) {
|
1356
|
-
// Hide the error message box completely
|
1357
|
-
errorMessage.style.display = 'none';
|
1358
|
-
}
|
1359
|
-
if (tryAgainBtn) {
|
1360
|
-
tryAgainBtn.style.display = 'block';
|
1361
|
-
// Update try again button to be more attractive
|
1362
|
-
tryAgainBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Try Again';
|
1363
|
-
tryAgainBtn.className = 'btn btn-primary fade-in';
|
1364
|
-
}
|
1365
|
-
|
1366
|
-
// Update page title to show suspension
|
1367
|
-
document.title = '⚠️ Research Suspended - Local Deep Research';
|
1368
|
-
|
1369
|
-
break;
|
1370
|
-
|
1371
|
-
case 'error':
|
1372
|
-
if (progressStatus) {
|
1373
|
-
progressStatus.innerHTML = '<i class="fas fa-exclamation-circle termination-icon"></i>' + (message || 'Error terminating research');
|
1374
|
-
progressStatus.className = 'progress-status status-failed fade-in';
|
1375
|
-
}
|
1376
|
-
if (progressBar) {
|
1377
|
-
progressBar.classList.remove('suspended-status');
|
1378
|
-
}
|
1379
|
-
if (terminateBtn) {
|
1380
|
-
terminateBtn.disabled = false;
|
1381
|
-
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
1382
|
-
}
|
1383
|
-
if (errorMessage) {
|
1384
|
-
errorMessage.innerHTML = '<i class="fas fa-exclamation-circle termination-icon"></i>' + (message || 'Error terminating research');
|
1385
|
-
errorMessage.style.display = 'block';
|
1386
|
-
errorMessage.className = 'error-message fade-in';
|
1387
|
-
}
|
1388
|
-
if (tryAgainBtn) {
|
1389
|
-
tryAgainBtn.style.display = 'none';
|
1390
|
-
}
|
1391
|
-
break;
|
1392
|
-
}
|
1393
|
-
}
|
1394
|
-
|
1395
|
-
// Expose the terminate function to the window object
|
1396
|
-
window.terminateResearch = terminateResearch;
|
1397
|
-
|
1398
|
-
// Function to update the progress UI
|
1399
|
-
function updateProgressUI(progress, status, message) {
|
1400
|
-
const progressFill = document.getElementById('progress-fill');
|
1401
|
-
const progressPercentage = document.getElementById('progress-percentage');
|
1402
|
-
const progressStatus = document.getElementById('progress-status');
|
1403
|
-
const errorMessage = document.getElementById('error-message');
|
1404
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
1405
|
-
|
1406
|
-
if (progressFill && progressPercentage) {
|
1407
|
-
progressFill.style.width = `${progress}%`;
|
1408
|
-
progressPercentage.textContent = `${progress}%`;
|
1409
|
-
}
|
1410
|
-
|
1411
|
-
if (progressStatus && message) {
|
1412
|
-
progressStatus.textContent = message;
|
1413
|
-
|
1414
|
-
// Update status class
|
1415
|
-
progressStatus.className = 'progress-status';
|
1416
|
-
if (status) {
|
1417
|
-
progressStatus.classList.add(`status-${status}`);
|
1418
|
-
}
|
1419
|
-
}
|
1420
|
-
|
1421
|
-
// Show/hide terminate button based on status
|
1422
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
1423
|
-
if (terminateBtn) {
|
1424
|
-
if (status === 'in_progress') {
|
1425
|
-
terminateBtn.style.display = 'inline-flex';
|
1426
|
-
terminateBtn.disabled = false;
|
1427
|
-
terminateBtn.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
1428
|
-
|
1429
|
-
// Hide try again button when in progress
|
1430
|
-
if (tryAgainBtn) {
|
1431
|
-
tryAgainBtn.style.display = 'none';
|
1432
|
-
}
|
1433
|
-
} else if (status === 'terminating') {
|
1434
|
-
terminateBtn.style.display = 'inline-flex';
|
1435
|
-
terminateBtn.disabled = true;
|
1436
|
-
terminateBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Terminating...';
|
1437
|
-
} else {
|
1438
|
-
terminateBtn.style.display = 'none';
|
1439
|
-
|
1440
|
-
// Show try again button for failed or suspended research
|
1441
|
-
if (tryAgainBtn && (status === 'failed' || status === 'suspended')) {
|
1442
|
-
tryAgainBtn.style.display = 'inline-flex';
|
1443
|
-
|
1444
|
-
// Get the current query for retry
|
1445
|
-
const currentQuery = document.getElementById('current-query');
|
1446
|
-
const queryText = currentQuery ? currentQuery.textContent : '';
|
1447
|
-
|
1448
|
-
// Add click event to try again button to go back to research form with the query preserved
|
1449
|
-
tryAgainBtn.onclick = function() {
|
1450
|
-
// Switch to the research form
|
1451
|
-
switchPage('new-research');
|
1452
|
-
|
1453
|
-
// Set the query text in the form
|
1454
|
-
const queryTextarea = document.getElementById('query');
|
1455
|
-
if (queryTextarea && queryText) {
|
1456
|
-
queryTextarea.value = queryText;
|
1457
|
-
}
|
1458
|
-
|
1459
|
-
// Clean up any remaining research state
|
1460
|
-
window.cleanupResearchResources();
|
1461
|
-
};
|
1462
|
-
}
|
1463
|
-
}
|
1464
|
-
}
|
1465
|
-
|
1466
|
-
// Show error message when there's an error
|
1467
|
-
if (errorMessage) {
|
1468
|
-
if (status === 'failed' || status === 'suspended') {
|
1469
|
-
errorMessage.style.display = 'block';
|
1470
|
-
errorMessage.textContent = message || (status === 'failed' ? 'Research failed' : 'Research was suspended');
|
1471
|
-
} else {
|
1472
|
-
errorMessage.style.display = 'none';
|
1473
|
-
}
|
1474
|
-
}
|
1475
|
-
}
|
1476
|
-
|
1477
|
-
// Completely rewritten function to ensure reliable history loading
|
1478
|
-
async function loadResearchHistory() {
|
1479
|
-
const historyList = document.getElementById('history-list');
|
1480
|
-
|
1481
|
-
// Make sure we have the history list element
|
1482
|
-
if (!historyList) {
|
1483
|
-
console.error('History list element not found');
|
1484
|
-
return;
|
1485
|
-
}
|
1486
|
-
|
1487
|
-
historyList.innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
|
1488
|
-
|
1489
|
-
try {
|
1490
|
-
const response = await fetch(getApiUrl('/api/history'));
|
1491
|
-
|
1492
|
-
if (!response.ok) {
|
1493
|
-
throw new Error(`Server returned ${response.status}`);
|
1494
|
-
}
|
1495
|
-
|
1496
|
-
const data = await response.json();
|
1497
|
-
|
1498
|
-
// Clear the loading spinner
|
1499
|
-
historyList.innerHTML = '';
|
1500
|
-
|
1501
|
-
// Handle empty data
|
1502
|
-
if (!data || !Array.isArray(data) || data.length === 0) {
|
1503
|
-
historyList.innerHTML = '<div class="empty-state">No research history found. Start a new research project!</div>';
|
1504
|
-
return;
|
1505
|
-
}
|
1506
|
-
|
1507
|
-
// Check if any research is in progress
|
1508
|
-
const inProgressResearch = data.find(item => item.status === 'in_progress');
|
1509
|
-
|
1510
|
-
// Get the start research button
|
1511
|
-
const startResearchBtn = document.getElementById('start-research-btn');
|
1512
|
-
|
1513
|
-
if (inProgressResearch) {
|
1514
|
-
isResearchInProgress = true;
|
1515
|
-
currentResearchId = inProgressResearch.id;
|
1516
|
-
if (startResearchBtn) {
|
1517
|
-
startResearchBtn.disabled = true;
|
1518
|
-
}
|
1519
|
-
} else {
|
1520
|
-
isResearchInProgress = false;
|
1521
|
-
if (startResearchBtn) {
|
1522
|
-
startResearchBtn.disabled = false;
|
1523
|
-
}
|
1524
|
-
}
|
1525
|
-
|
1526
|
-
// Display each history item
|
1527
|
-
data.forEach(item => {
|
1528
|
-
try {
|
1529
|
-
// Skip if item is invalid
|
1530
|
-
if (!item || !item.id) {
|
1531
|
-
return;
|
1532
|
-
}
|
1533
|
-
|
1534
|
-
// Create container
|
1535
|
-
const historyItem = document.createElement('div');
|
1536
|
-
historyItem.className = 'history-item';
|
1537
|
-
historyItem.dataset.researchId = item.id;
|
1538
|
-
|
1539
|
-
// Create header with title and status
|
1540
|
-
const header = document.createElement('div');
|
1541
|
-
header.className = 'history-item-header';
|
1542
|
-
|
1543
|
-
const title = document.createElement('div');
|
1544
|
-
title.className = 'history-item-title';
|
1545
|
-
title.textContent = item.query || 'Untitled Research';
|
1546
|
-
|
1547
|
-
const status = document.createElement('div');
|
1548
|
-
status.className = `history-item-status status-${item.status ? item.status.replace('_', '-') : 'unknown'}`;
|
1549
|
-
status.textContent = item.status ?
|
1550
|
-
(item.status === 'in_progress' ? 'In Progress' :
|
1551
|
-
item.status.charAt(0).toUpperCase() + item.status.slice(1)) :
|
1552
|
-
'Unknown';
|
1553
|
-
|
1554
|
-
header.appendChild(title);
|
1555
|
-
header.appendChild(status);
|
1556
|
-
historyItem.appendChild(header);
|
1557
|
-
|
1558
|
-
// Create meta section
|
1559
|
-
const meta = document.createElement('div');
|
1560
|
-
meta.className = 'history-item-meta';
|
1561
|
-
|
1562
|
-
const date = document.createElement('div');
|
1563
|
-
date.className = 'history-item-date';
|
1564
|
-
try {
|
1565
|
-
// Use completed_at if available, fall back to created_at if not
|
1566
|
-
const dateToUse = item.completed_at || item.created_at;
|
1567
|
-
date.textContent = dateToUse ? formatDate(new Date(dateToUse)) : 'Unknown date';
|
1568
|
-
} catch (e) {
|
1569
|
-
date.textContent = item.completed_at || item.created_at || 'Unknown date';
|
1570
|
-
}
|
1571
|
-
|
1572
|
-
const mode = document.createElement('div');
|
1573
|
-
mode.className = 'history-item-mode';
|
1574
|
-
const modeIcon = item.mode === 'quick' ? 'bolt' : 'microscope';
|
1575
|
-
const modeText = item.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
|
1576
|
-
mode.innerHTML = `<i class="fas fa-${modeIcon}"></i> ${modeText}`;
|
1577
|
-
|
1578
|
-
meta.appendChild(date);
|
1579
|
-
meta.appendChild(mode);
|
1580
|
-
historyItem.appendChild(meta);
|
1581
|
-
|
1582
|
-
// Create actions section
|
1583
|
-
const actions = document.createElement('div');
|
1584
|
-
actions.className = 'history-item-actions';
|
1585
|
-
|
1586
|
-
// View button
|
1587
|
-
const viewBtn = document.createElement('button');
|
1588
|
-
viewBtn.className = 'btn btn-sm btn-outline view-btn';
|
1589
|
-
|
1590
|
-
if (item.status === 'completed') {
|
1591
|
-
viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
1592
|
-
viewBtn.addEventListener('click', (e) => {
|
1593
|
-
e.stopPropagation();
|
1594
|
-
loadResearch(item.id);
|
1595
|
-
});
|
1596
|
-
|
1597
|
-
// PDF button for completed research
|
1598
|
-
const pdfBtn = document.createElement('button');
|
1599
|
-
pdfBtn.className = 'btn btn-sm btn-outline pdf-btn';
|
1600
|
-
pdfBtn.innerHTML = '<i class="fas fa-file-pdf"></i> PDF';
|
1601
|
-
pdfBtn.addEventListener('click', (e) => {
|
1602
|
-
e.stopPropagation();
|
1603
|
-
generatePdfFromResearch(item.id);
|
1604
|
-
});
|
1605
|
-
actions.appendChild(pdfBtn);
|
1606
|
-
} else if (item.status === 'in_progress') {
|
1607
|
-
viewBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> In Progress';
|
1608
|
-
viewBtn.addEventListener('click', (e) => {
|
1609
|
-
e.stopPropagation();
|
1610
|
-
navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
|
1611
|
-
});
|
1612
|
-
} else {
|
1613
|
-
viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
1614
|
-
viewBtn.disabled = true;
|
1615
|
-
}
|
1616
|
-
|
1617
|
-
actions.appendChild(viewBtn);
|
1618
|
-
|
1619
|
-
// Delete button
|
1620
|
-
const deleteBtn = document.createElement('button');
|
1621
|
-
deleteBtn.className = 'btn btn-sm btn-outline delete-btn';
|
1622
|
-
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
|
1623
|
-
deleteBtn.addEventListener('click', (e) => {
|
1624
|
-
e.stopPropagation();
|
1625
|
-
if (confirm(`Are you sure you want to delete this research: "${item.query}"?`)) {
|
1626
|
-
deleteResearch(item.id);
|
1627
|
-
}
|
1628
|
-
});
|
1629
|
-
actions.appendChild(deleteBtn);
|
1630
|
-
|
1631
|
-
historyItem.appendChild(actions);
|
1632
|
-
|
1633
|
-
// Add click handler for the entire item
|
1634
|
-
historyItem.addEventListener('click', (e) => {
|
1635
|
-
// Skip if clicking on a button
|
1636
|
-
if (e.target.closest('button')) {
|
1637
|
-
return;
|
1638
|
-
}
|
1639
|
-
|
1640
|
-
if (item.status === 'completed') {
|
1641
|
-
loadResearch(item.id);
|
1642
|
-
} else if (item.status === 'in_progress') {
|
1643
|
-
navigateToResearchProgress({id: item.id, query: item.query, progress: 0});
|
1644
|
-
}
|
1645
|
-
});
|
1646
|
-
|
1647
|
-
// Add to the history list
|
1648
|
-
historyList.appendChild(historyItem);
|
1649
|
-
} catch (itemError) {
|
1650
|
-
console.error('Error processing history item:', itemError, item);
|
1651
|
-
}
|
1652
|
-
});
|
1653
|
-
|
1654
|
-
} catch (error) {
|
1655
|
-
console.error('Error loading history:', error);
|
1656
|
-
historyList.innerHTML = `
|
1657
|
-
<div class="error-message">
|
1658
|
-
Error loading history: ${error.message}
|
1659
|
-
</div>
|
1660
|
-
<div style="text-align: center; margin-top: 1rem;">
|
1661
|
-
<button id="retry-history-btn" class="btn btn-primary">
|
1662
|
-
<i class="fas fa-sync"></i> Retry
|
1663
|
-
</button>
|
1664
|
-
</div>`;
|
1665
|
-
|
1666
|
-
const retryBtn = document.getElementById('retry-history-btn');
|
1667
|
-
if (retryBtn) {
|
1668
|
-
retryBtn.addEventListener('click', () => {
|
1669
|
-
loadResearchHistory();
|
1670
|
-
});
|
1671
|
-
}
|
1672
|
-
}
|
1673
|
-
|
1674
|
-
// Add a fallback in case something goes wrong and the history list is still empty
|
1675
|
-
setTimeout(() => {
|
1676
|
-
if (historyList.innerHTML === '' || historyList.innerHTML.includes('loading-spinner')) {
|
1677
|
-
console.warn('History list is still empty or showing spinner after load attempt - applying fallback');
|
1678
|
-
historyList.innerHTML = `
|
1679
|
-
<div class="error-message">
|
1680
|
-
Something went wrong while loading the history.
|
1681
|
-
</div>
|
1682
|
-
<div style="text-align: center; margin-top: 1rem;">
|
1683
|
-
<button id="fallback-retry-btn" class="btn btn-primary">
|
1684
|
-
<i class="fas fa-sync"></i> Retry
|
1685
|
-
</button>
|
1686
|
-
</div>`;
|
1687
|
-
|
1688
|
-
const fallbackRetryBtn = document.getElementById('fallback-retry-btn');
|
1689
|
-
if (fallbackRetryBtn) {
|
1690
|
-
fallbackRetryBtn.addEventListener('click', () => {
|
1691
|
-
loadResearchHistory();
|
1692
|
-
});
|
1693
|
-
}
|
1694
|
-
}
|
1695
|
-
}, 5000); // Check after 5 seconds
|
1696
|
-
}
|
1697
|
-
|
1698
|
-
// Function to navigate to research progress
|
1699
|
-
function navigateToResearchProgress(research) {
|
1700
|
-
// Set the current viewing research ID
|
1701
|
-
viewingResearchId = research.id;
|
1702
|
-
|
1703
|
-
// First check if the research is already terminated/suspended
|
1704
|
-
if (research.status === 'suspended' || research.status === 'failed') {
|
1705
|
-
// Switch to the progress page with terminated state
|
1706
|
-
switchPage('research-progress');
|
1707
|
-
|
1708
|
-
// Show the query
|
1709
|
-
document.getElementById('current-query').textContent = research.query || '';
|
1710
|
-
|
1711
|
-
// Update UI for terminated state
|
1712
|
-
updateTerminationUIState('suspended', `Research was ${research.status}`);
|
1713
|
-
|
1714
|
-
// Update progress percentage
|
1715
|
-
const progress = research.progress || 0;
|
1716
|
-
document.getElementById('progress-fill').style.width = `${progress}%`;
|
1717
|
-
document.getElementById('progress-percentage').textContent = `${progress}%`;
|
1718
|
-
|
1719
|
-
// Load logs for this research
|
1720
|
-
loadLogsForResearch(research.id);
|
1721
|
-
|
1722
|
-
// Don't connect to socket or start polling for terminated research
|
1723
|
-
return;
|
1724
|
-
}
|
1725
|
-
|
1726
|
-
document.getElementById('current-query').textContent = research.query;
|
1727
|
-
document.getElementById('progress-fill').style.width = `${research.progress || 0}%`;
|
1728
|
-
document.getElementById('progress-percentage').textContent = `${research.progress || 0}%`;
|
1729
|
-
|
1730
|
-
// Navigate to progress page
|
1731
|
-
switchPage('research-progress');
|
1732
|
-
|
1733
|
-
// Load logs for this research
|
1734
|
-
loadLogsForResearch(research.id);
|
1735
|
-
|
1736
|
-
// Connect to socket for this research
|
1737
|
-
window.connectToResearchSocket(research.id);
|
1738
|
-
|
1739
|
-
// Start polling for status
|
1740
|
-
pollResearchStatus(research.id);
|
1741
|
-
}
|
1742
|
-
|
1743
|
-
// Function to delete a research record
|
1744
|
-
async function deleteResearch(researchId) {
|
1745
|
-
try {
|
1746
|
-
const response = await fetch(getApiUrl(`/api/research/${researchId}/delete`), {
|
1747
|
-
method: 'DELETE'
|
1748
|
-
});
|
1749
|
-
|
1750
|
-
if (response.ok) {
|
1751
|
-
// Reload the history
|
1752
|
-
loadResearchHistory();
|
1753
|
-
} else {
|
1754
|
-
alert('Failed to delete research. Please try again.');
|
1755
|
-
}
|
1756
|
-
} catch (error) {
|
1757
|
-
console.error('Error deleting research:', error);
|
1758
|
-
alert('An error occurred while deleting the research.');
|
1759
|
-
}
|
1760
|
-
}
|
1761
|
-
|
1762
|
-
// Update the loadResearch function to handle terminated/suspended research better
|
1763
|
-
async function loadResearch(researchId) {
|
1764
|
-
try {
|
1765
|
-
console.log(`Loading research results for research ID: ${researchId}`);
|
1766
|
-
|
1767
|
-
// Set the current viewing research ID
|
1768
|
-
viewingResearchId = researchId;
|
1769
|
-
|
1770
|
-
// Get research data first to check status
|
1771
|
-
fetch(getApiUrl(`/api/research/${researchId}`))
|
1772
|
-
.then(response => response.json())
|
1773
|
-
.then(researchData => {
|
1774
|
-
// Check if research was terminated or failed
|
1775
|
-
if (researchData.status === 'suspended' || researchData.status === 'failed') {
|
1776
|
-
console.log(`Research ${researchId} was ${researchData.status}, not loading results`);
|
1777
|
-
|
1778
|
-
// Switch to research progress page if not already there
|
1779
|
-
const progressPage = document.getElementById('research-progress');
|
1780
|
-
if (!progressPage.classList.contains('active')) {
|
1781
|
-
switchPage('research-progress');
|
1782
|
-
}
|
1783
|
-
|
1784
|
-
// Show error message and Try Again button
|
1785
|
-
const errorMessage = document.getElementById('error-message');
|
1786
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
1787
|
-
|
1788
|
-
if (errorMessage) {
|
1789
|
-
errorMessage.textContent = researchData.error || `Research was ${researchData.status}`;
|
1790
|
-
errorMessage.style.display = 'block';
|
1791
|
-
}
|
1792
|
-
|
1793
|
-
if (tryAgainBtn) {
|
1794
|
-
tryAgainBtn.style.display = 'block';
|
1795
|
-
}
|
1796
|
-
|
1797
|
-
// Update UI elements for this research
|
1798
|
-
document.getElementById('current-query').textContent = researchData.query || 'Unknown query';
|
1799
|
-
|
1800
|
-
// Update progress bar
|
1801
|
-
const progressFill = document.getElementById('progress-fill');
|
1802
|
-
const progressPercentage = document.getElementById('progress-percentage');
|
1803
|
-
if (progressFill) progressFill.style.width = '0%';
|
1804
|
-
if (progressPercentage) progressPercentage.textContent = '0%';
|
1805
|
-
|
1806
|
-
// Load logs for this research
|
1807
|
-
loadLogsForResearch(researchId);
|
1808
|
-
|
1809
|
-
return; // Exit early, no need to load report for terminated research
|
1810
|
-
}
|
1811
|
-
|
1812
|
-
// Normal flow for completed research
|
1813
|
-
fetch(getApiUrl(`/api/report/${researchId}`))
|
1814
|
-
.then(response => response.json())
|
1815
|
-
.then(data => {
|
1816
|
-
if (data.status === 'error') {
|
1817
|
-
throw new Error('Research report not found');
|
1818
|
-
}
|
1819
|
-
|
1820
|
-
if (!data.content) {
|
1821
|
-
console.error('No report content found in research data');
|
1822
|
-
throw new Error('Report content is empty');
|
1823
|
-
}
|
1824
|
-
|
1825
|
-
// Set the report content and metadata
|
1826
|
-
document.getElementById('result-query').textContent = researchData.query || 'Unknown Query';
|
1827
|
-
document.getElementById('result-date').textContent = formatDate(
|
1828
|
-
researchData.completed_at,
|
1829
|
-
researchData.duration_seconds
|
1830
|
-
);
|
1831
|
-
document.getElementById('result-mode').textContent = formatMode(researchData.mode);
|
1832
|
-
|
1833
|
-
// Update duration if available (for backward compatibility with existing UI elements)
|
1834
|
-
if (researchData.created_at && researchData.completed_at && !researchData.duration_seconds) {
|
1835
|
-
// Calculate duration if it's not provided by the API
|
1836
|
-
const startDate = new Date(researchData.created_at);
|
1837
|
-
const endDate = new Date(researchData.completed_at);
|
1838
|
-
const durationSec = Math.floor((endDate - startDate) / 1000);
|
1839
|
-
|
1840
|
-
// Update the date display with calculated duration
|
1841
|
-
document.getElementById('result-date').textContent = formatDate(
|
1842
|
-
researchData.completed_at,
|
1843
|
-
durationSec
|
1844
|
-
);
|
1845
|
-
|
1846
|
-
// Also update any UI elements that might rely on updateResearchDuration
|
1847
|
-
const metadata = {
|
1848
|
-
started_at: researchData.created_at,
|
1849
|
-
completed_at: researchData.completed_at
|
1850
|
-
};
|
1851
|
-
updateResearchDuration(metadata);
|
1852
|
-
}
|
1853
|
-
|
1854
|
-
// Render the content
|
1855
|
-
const resultsContent = document.getElementById('results-content');
|
1856
|
-
resultsContent.innerHTML = ''; // Clear any previous content
|
1857
|
-
|
1858
|
-
// Convert markdown to HTML
|
1859
|
-
const htmlContent = marked.parse(data.content);
|
1860
|
-
resultsContent.innerHTML = htmlContent;
|
1861
|
-
|
1862
|
-
// Apply code highlighting
|
1863
|
-
document.querySelectorAll('pre code').forEach((block) => {
|
1864
|
-
hljs.highlightBlock(block);
|
1865
|
-
});
|
1866
|
-
|
1867
|
-
// Load logs for this research
|
1868
|
-
loadLogsForResearch(researchId);
|
1869
|
-
|
1870
|
-
// Switch to the results page
|
1871
|
-
switchPage('research-results');
|
1872
|
-
})
|
1873
|
-
.catch(error => {
|
1874
|
-
console.error(`Error loading research: ${error}`);
|
1875
|
-
});
|
1876
|
-
})
|
1877
|
-
.catch(error => {
|
1878
|
-
console.error(`Error checking research status: ${error}`);
|
1879
|
-
});
|
1880
|
-
} catch (error) {
|
1881
|
-
console.error(`Error loading research: ${error}`);
|
1882
|
-
}
|
1883
|
-
}
|
1884
|
-
|
1885
|
-
// Function to load research details
|
1886
|
-
async function loadResearchDetails(researchId) {
|
1887
|
-
try {
|
1888
|
-
// Show loading indicators
|
1889
|
-
document.getElementById('research-log').innerHTML = '<div class="loading-spinner centered"><div class="spinner"></div></div>';
|
1890
|
-
|
1891
|
-
// Set the current viewing research ID
|
1892
|
-
viewingResearchId = researchId;
|
1893
|
-
|
1894
|
-
// Fetch the research data
|
1895
|
-
const response = await fetch(getApiUrl(`/api/research/${researchId}/details`));
|
1896
|
-
const data = await response.json();
|
1897
|
-
|
1898
|
-
if (data.status === 'success') {
|
1899
|
-
// Update the metadata display
|
1900
|
-
document.getElementById('detail-query').textContent = data.query || 'Unknown query';
|
1901
|
-
document.getElementById('detail-status').textContent = formatStatus(data.status);
|
1902
|
-
document.getElementById('detail-mode').textContent = formatMode(data.mode);
|
1903
|
-
|
1904
|
-
// Update progress percentage
|
1905
|
-
const progressFill = document.getElementById('detail-progress-fill');
|
1906
|
-
const progressPercentage = document.getElementById('detail-progress-percentage');
|
1907
|
-
|
1908
|
-
if (progressFill && progressPercentage) {
|
1909
|
-
const progress = data.progress || 0;
|
1910
|
-
progressFill.style.width = `${progress}%`;
|
1911
|
-
progressPercentage.textContent = `${progress}%`;
|
1912
|
-
}
|
1913
|
-
|
1914
|
-
// Update duration if available
|
1915
|
-
const metadata = {
|
1916
|
-
created_at: data.created_at,
|
1917
|
-
completed_at: data.completed_at
|
1918
|
-
};
|
1919
|
-
updateResearchDuration(metadata);
|
1920
|
-
|
1921
|
-
// Render the log entries from the response directly
|
1922
|
-
if (data.log && Array.isArray(data.log)) {
|
1923
|
-
renderResearchLog(data.log, researchId);
|
1924
|
-
} else {
|
1925
|
-
// Fallback to the dedicated logs endpoint if log data is missing
|
1926
|
-
try {
|
1927
|
-
const logResponse = await fetch(getApiUrl(`/api/research/${researchId}/logs`));
|
1928
|
-
const logData = await logResponse.json();
|
1929
|
-
|
1930
|
-
if (logData.status === 'success' && logData.logs && Array.isArray(logData.logs)) {
|
1931
|
-
renderResearchLog(logData.logs, researchId);
|
1932
|
-
} else {
|
1933
|
-
document.getElementById('research-log').innerHTML = '<div class="empty-state">No log entries available</div>';
|
1934
|
-
}
|
1935
|
-
} catch (logError) {
|
1936
|
-
console.error('Error fetching logs:', logError);
|
1937
|
-
document.getElementById('research-log').innerHTML = '<div class="empty-state">Failed to load log entries</div>';
|
1938
|
-
}
|
1939
|
-
}
|
1940
|
-
|
1941
|
-
// Update detail actions based on research status
|
1942
|
-
updateDetailActions(data);
|
1943
|
-
|
1944
|
-
// Load logs for the console panel as well
|
1945
|
-
loadLogsForResearch(researchId);
|
1946
|
-
|
1947
|
-
// Switch to the details page
|
1948
|
-
switchPage('research-details');
|
1949
|
-
} else {
|
1950
|
-
alert('Failed to load research details: ' + (data.message || 'Unknown error'));
|
1951
|
-
}
|
1952
|
-
} catch (error) {
|
1953
|
-
console.error('Error loading research details:', error);
|
1954
|
-
alert('An error occurred while loading the research details');
|
1955
|
-
}
|
1956
|
-
}
|
1957
|
-
|
1958
|
-
// Function to render log entries
|
1959
|
-
function renderResearchLog(logEntries, researchId) {
|
1960
|
-
const researchLog = document.getElementById('research-log');
|
1961
|
-
researchLog.innerHTML = '';
|
1962
|
-
|
1963
|
-
if (!logEntries || logEntries.length === 0) {
|
1964
|
-
researchLog.innerHTML = '<div class="empty-state">No log entries available.</div>';
|
1965
|
-
return;
|
1966
|
-
}
|
1967
|
-
|
1968
|
-
try {
|
1969
|
-
// Use a document fragment for better performance
|
1970
|
-
const fragment = document.createDocumentFragment();
|
1971
|
-
const template = document.getElementById('log-entry-template');
|
1972
|
-
|
1973
|
-
if (!template) {
|
1974
|
-
console.error('Log entry template not found');
|
1975
|
-
researchLog.innerHTML = '<div class="error-message">Error rendering log entries: Template not found</div>';
|
1976
|
-
return;
|
1977
|
-
}
|
1978
|
-
|
1979
|
-
logEntries.forEach(entry => {
|
1980
|
-
if (!entry) return; // Skip invalid entries
|
1981
|
-
|
1982
|
-
try {
|
1983
|
-
const clone = document.importNode(template.content, true);
|
1984
|
-
|
1985
|
-
// Format the timestamp
|
1986
|
-
let timeStr = 'N/A';
|
1987
|
-
try {
|
1988
|
-
if (entry.time) {
|
1989
|
-
const time = new Date(entry.time);
|
1990
|
-
timeStr = time.toLocaleTimeString();
|
1991
|
-
}
|
1992
|
-
} catch (timeErr) {
|
1993
|
-
console.warn('Error formatting time:', timeErr);
|
1994
|
-
}
|
1995
|
-
|
1996
|
-
const timeEl = clone.querySelector('.log-entry-time');
|
1997
|
-
if (timeEl) timeEl.textContent = timeStr;
|
1998
|
-
|
1999
|
-
// Add message with phase highlighting if available
|
2000
|
-
const messageEl = clone.querySelector('.log-entry-message');
|
2001
|
-
if (messageEl) {
|
2002
|
-
let phaseClass = '';
|
2003
|
-
if (entry.metadata && entry.metadata.phase) {
|
2004
|
-
phaseClass = `phase-${entry.metadata.phase}`;
|
2005
|
-
}
|
2006
|
-
messageEl.textContent = entry.message || 'No message';
|
2007
|
-
messageEl.classList.add(phaseClass);
|
2008
|
-
}
|
2009
|
-
|
2010
|
-
// Add progress information if available
|
2011
|
-
const progressEl = clone.querySelector('.log-entry-progress');
|
2012
|
-
if (progressEl) {
|
2013
|
-
if (entry.progress !== null && entry.progress !== undefined) {
|
2014
|
-
progressEl.textContent = `Progress: ${entry.progress}%`;
|
2015
|
-
} else {
|
2016
|
-
progressEl.textContent = '';
|
2017
|
-
}
|
2018
|
-
}
|
2019
|
-
|
2020
|
-
fragment.appendChild(clone);
|
2021
|
-
} catch (entryError) {
|
2022
|
-
console.error('Error processing log entry:', entryError, entry);
|
2023
|
-
// Continue with other entries
|
2024
|
-
}
|
2025
|
-
});
|
2026
|
-
|
2027
|
-
researchLog.appendChild(fragment);
|
2028
|
-
|
2029
|
-
// Scroll to the bottom
|
2030
|
-
researchLog.scrollTop = researchLog.scrollHeight;
|
2031
|
-
} catch (error) {
|
2032
|
-
console.error('Error rendering log entries:', error);
|
2033
|
-
researchLog.innerHTML = '<div class="error-message">Error rendering log entries. Please try again later.</div>';
|
2034
|
-
}
|
2035
|
-
|
2036
|
-
// Connect to socket for updates if this is an in-progress research
|
2037
|
-
// Check for research ID and whether it's in progress by checking the database status
|
2038
|
-
if (researchId) {
|
2039
|
-
// Check research status in the database to determine if we should connect to socket
|
2040
|
-
fetch(getApiUrl(`/api/research/${researchId}`))
|
2041
|
-
.then(response => response.json())
|
2042
|
-
.then(data => {
|
2043
|
-
if (data && data.status === 'in_progress') {
|
2044
|
-
console.log(`Connecting to socket for research ${researchId} from log view`);
|
2045
|
-
window.connectToResearchSocket(researchId);
|
2046
|
-
}
|
2047
|
-
})
|
2048
|
-
.catch(err => console.error(`Error checking research status for socket connection: ${err}`));
|
2049
|
-
}
|
2050
|
-
}
|
2051
|
-
|
2052
|
-
// Function to update detail log with a new entry
|
2053
|
-
function updateDetailLogEntry(logEntry) {
|
2054
|
-
if (!logEntry || !document.getElementById('research-details').classList.contains('active')) {
|
2055
|
-
return;
|
2056
|
-
}
|
2057
|
-
|
2058
|
-
const researchLog = document.getElementById('research-log');
|
2059
|
-
const template = document.getElementById('log-entry-template');
|
2060
|
-
const clone = document.importNode(template.content, true);
|
2061
|
-
|
2062
|
-
// Format the timestamp
|
2063
|
-
const time = new Date(logEntry.time);
|
2064
|
-
clone.querySelector('.log-entry-time').textContent = time.toLocaleTimeString();
|
2065
|
-
|
2066
|
-
// Add message with phase highlighting if available
|
2067
|
-
const messageEl = clone.querySelector('.log-entry-message');
|
2068
|
-
let phaseClass = '';
|
2069
|
-
if (logEntry.metadata && logEntry.metadata.phase) {
|
2070
|
-
phaseClass = `phase-${logEntry.metadata.phase}`;
|
2071
|
-
}
|
2072
|
-
messageEl.textContent = logEntry.message;
|
2073
|
-
messageEl.classList.add(phaseClass);
|
2074
|
-
|
2075
|
-
// Add progress information if available
|
2076
|
-
const progressEl = clone.querySelector('.log-entry-progress');
|
2077
|
-
if (logEntry.progress !== null && logEntry.progress !== undefined) {
|
2078
|
-
progressEl.textContent = `Progress: ${logEntry.progress}%`;
|
2079
|
-
|
2080
|
-
// Also update the progress bar in the details view
|
2081
|
-
document.getElementById('detail-progress-fill').style.width = `${logEntry.progress}%`;
|
2082
|
-
document.getElementById('detail-progress-percentage').textContent = `${logEntry.progress}%`;
|
2083
|
-
} else {
|
2084
|
-
progressEl.textContent = '';
|
2085
|
-
}
|
2086
|
-
|
2087
|
-
researchLog.appendChild(clone);
|
2088
|
-
|
2089
|
-
// Scroll to the bottom
|
2090
|
-
researchLog.scrollTop = researchLog.scrollHeight;
|
2091
|
-
}
|
2092
|
-
|
2093
|
-
// Back to history button handlers - using direct page switching
|
2094
|
-
document.getElementById('back-to-history')?.addEventListener('click', () => {
|
2095
|
-
switchPage('history');
|
2096
|
-
});
|
2097
|
-
|
2098
|
-
document.getElementById('back-to-history-from-details')?.addEventListener('click', () => {
|
2099
|
-
switchPage('history');
|
2100
|
-
});
|
2101
|
-
|
2102
|
-
// Helper functions
|
2103
|
-
function capitalizeFirstLetter(string) {
|
2104
|
-
return string.charAt(0).toUpperCase() + string.slice(1);
|
2105
|
-
}
|
2106
|
-
|
2107
|
-
// Function to update progress UI from current research
|
2108
|
-
function updateProgressFromCurrentResearch() {
|
2109
|
-
if (!isResearchInProgress || !currentResearchId) return;
|
2110
|
-
|
2111
|
-
// Fetch current status
|
2112
|
-
fetch(getApiUrl(`/api/research/${currentResearchId}`))
|
2113
|
-
.then(response => response.json())
|
2114
|
-
.then(data => {
|
2115
|
-
document.getElementById('current-query').textContent = data.query || '';
|
2116
|
-
document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
|
2117
|
-
document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
|
2118
|
-
|
2119
|
-
// Connect to socket for this research
|
2120
|
-
window.connectToResearchSocket(currentResearchId);
|
2121
|
-
})
|
2122
|
-
.catch(error => {
|
2123
|
-
console.error('Error fetching research status:', error);
|
2124
|
-
});
|
2125
|
-
}
|
2126
|
-
|
2127
|
-
// Function to update the sidebar navigation based on research status
|
2128
|
-
function updateNavigationBasedOnResearchStatus() {
|
2129
|
-
const isResearchPage = document.getElementById('research-progress').classList.contains('active');
|
2130
|
-
const isAnyResearchInProgress = window.currentResearchId !== null && isResearchInProgress;
|
2131
|
-
|
2132
|
-
// Get the sidebar nav items and mobile tab items
|
2133
|
-
const sidebarNewResearchItem = document.querySelector('.sidebar-nav li[data-page="new-research"]');
|
2134
|
-
const sidebarHistoryItem = document.querySelector('.sidebar-nav li[data-page="history"]');
|
2135
|
-
const mobileNewResearchItem = document.querySelector('.mobile-tab-bar li[data-page="new-research"]');
|
2136
|
-
const mobileHistoryItem = document.querySelector('.mobile-tab-bar li[data-page="history"]');
|
2137
|
-
|
2138
|
-
// Control elements
|
2139
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
2140
|
-
const errorMessage = document.getElementById('error-message');
|
2141
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
2142
|
-
|
2143
|
-
// Log panel should only be visible on research-progress and research-results pages
|
2144
|
-
const logPanel = document.querySelector('.collapsible-log-panel');
|
2145
|
-
|
2146
|
-
// If research is in progress
|
2147
|
-
if (isAnyResearchInProgress) {
|
2148
|
-
// Disable new research and history navigation while research is in progress
|
2149
|
-
if (sidebarNewResearchItem) sidebarNewResearchItem.classList.add('disabled');
|
2150
|
-
if (sidebarHistoryItem) sidebarHistoryItem.classList.add('disabled');
|
2151
|
-
if (mobileNewResearchItem) mobileNewResearchItem.classList.add('disabled');
|
2152
|
-
if (mobileHistoryItem) mobileHistoryItem.classList.add('disabled');
|
2153
|
-
|
2154
|
-
// If user is not already on the research progress page, switch to it
|
2155
|
-
if (!isResearchPage) {
|
2156
|
-
switchPage('research-progress');
|
2157
|
-
}
|
2158
|
-
|
2159
|
-
// Show terminate button and hide error message and try again button
|
2160
|
-
if (terminateBtn) terminateBtn.style.display = 'block';
|
2161
|
-
if (errorMessage) errorMessage.style.display = 'none';
|
2162
|
-
if (tryAgainBtn) tryAgainBtn.style.display = 'none';
|
2163
|
-
} else {
|
2164
|
-
// Enable navigation when no research is in progress
|
2165
|
-
if (sidebarNewResearchItem) sidebarNewResearchItem.classList.remove('disabled');
|
2166
|
-
if (sidebarHistoryItem) sidebarHistoryItem.classList.remove('disabled');
|
2167
|
-
if (mobileNewResearchItem) mobileNewResearchItem.classList.remove('disabled');
|
2168
|
-
if (mobileHistoryItem) mobileHistoryItem.classList.remove('disabled');
|
2169
|
-
|
2170
|
-
// Hide terminate button when no research is in progress
|
2171
|
-
if (terminateBtn) terminateBtn.style.display = 'none';
|
2172
|
-
}
|
2173
|
-
|
2174
|
-
console.log('Updated navigation based on research status. In progress:', isAnyResearchInProgress);
|
2175
|
-
}
|
2176
|
-
|
2177
|
-
// Function to update the research progress page from nav click
|
2178
|
-
function updateProgressPage() {
|
2179
|
-
if (!currentResearchId && !window.currentResearchId) {
|
2180
|
-
return;
|
2181
|
-
}
|
2182
|
-
|
2183
|
-
const researchId = currentResearchId || window.currentResearchId;
|
2184
|
-
|
2185
|
-
// Update the progress page
|
2186
|
-
fetch(getApiUrl(`/api/research/${researchId}`))
|
2187
|
-
.then(response => response.json())
|
2188
|
-
.then(data => {
|
2189
|
-
// Update the query display
|
2190
|
-
document.getElementById('current-query').textContent = data.query;
|
2191
|
-
|
2192
|
-
// Check status before updating UI
|
2193
|
-
if (data.status === 'in_progress') {
|
2194
|
-
// Update the progress bar
|
2195
|
-
updateProgressUI(data.progress || 0, data.status);
|
2196
|
-
|
2197
|
-
// Check if we need to show the terminate button
|
2198
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
2199
|
-
if (terminateBtn) {
|
2200
|
-
terminateBtn.style.display = 'inline-flex';
|
2201
|
-
terminateBtn.disabled = false;
|
2202
|
-
}
|
2203
|
-
|
2204
|
-
// Only connect to socket for in-progress research
|
2205
|
-
window.connectToResearchSocket(researchId);
|
2206
|
-
// Only start polling for in-progress research
|
2207
|
-
if (!pollingInterval) {
|
2208
|
-
pollResearchStatus(researchId);
|
2209
|
-
}
|
2210
|
-
}
|
2211
|
-
else if (data.status === 'suspended' || data.status === 'failed') {
|
2212
|
-
// Update UI for terminated research
|
2213
|
-
updateTerminationUIState('suspended', data.error || `Research was ${data.status}`);
|
2214
|
-
// Update progress bar
|
2215
|
-
document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
|
2216
|
-
document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
|
2217
|
-
}
|
2218
|
-
else {
|
2219
|
-
// Just update progress for completed research
|
2220
|
-
document.getElementById('progress-fill').style.width = `${data.progress || 0}%`;
|
2221
|
-
document.getElementById('progress-percentage').textContent = `${data.progress || 0}%`;
|
2222
|
-
}
|
2223
|
-
})
|
2224
|
-
.catch(error => {
|
2225
|
-
console.error('Error fetching research status:', error);
|
2226
|
-
});
|
2227
|
-
}
|
2228
|
-
|
2229
|
-
// Function to update a specific history item without reloading the whole list
|
2230
|
-
function updateHistoryItemStatus(researchId, status, statusText) {
|
2231
|
-
const historyList = document.getElementById('history-list');
|
2232
|
-
|
2233
|
-
// Format the status for display
|
2234
|
-
const displayStatus = statusText ||
|
2235
|
-
(status === 'in_progress' ? 'In Progress' :
|
2236
|
-
status.charAt(0).toUpperCase() + status.slice(1));
|
2237
|
-
|
2238
|
-
// Format the CSS class
|
2239
|
-
const statusClass = `status-${status.replace('_', '-')}`;
|
2240
|
-
|
2241
|
-
// Look for the item in the active research banner
|
2242
|
-
const activeBanner = historyList.querySelector(`.active-research-banner[data-research-id="${researchId}"]`);
|
2243
|
-
if (activeBanner) {
|
2244
|
-
const statusEl = activeBanner.querySelector('.history-item-status');
|
2245
|
-
if (statusEl) {
|
2246
|
-
statusEl.textContent = displayStatus;
|
2247
|
-
statusEl.className = 'history-item-status';
|
2248
|
-
statusEl.classList.add(statusClass);
|
2249
|
-
}
|
2250
|
-
|
2251
|
-
// Update buttons
|
2252
|
-
const terminateBtn = activeBanner.querySelector('.terminate-btn');
|
2253
|
-
if (terminateBtn) {
|
2254
|
-
terminateBtn.style.display = 'none';
|
2255
|
-
}
|
2256
|
-
|
2257
|
-
const viewProgressBtn = activeBanner.querySelector('.view-progress-btn');
|
2258
|
-
if (viewProgressBtn) {
|
2259
|
-
if (status === 'suspended') {
|
2260
|
-
viewProgressBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
|
2261
|
-
} else if (status === 'failed') {
|
2262
|
-
viewProgressBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
|
2263
|
-
}
|
2264
|
-
viewProgressBtn.disabled = true;
|
2265
|
-
}
|
2266
|
-
|
2267
|
-
return;
|
2268
|
-
}
|
2269
|
-
|
2270
|
-
// Look for the item in the regular list
|
2271
|
-
const historyItem = historyList.querySelector(`.history-item[data-research-id="${researchId}"]`);
|
2272
|
-
if (historyItem) {
|
2273
|
-
const statusEl = historyItem.querySelector('.history-item-status');
|
2274
|
-
if (statusEl) {
|
2275
|
-
statusEl.textContent = displayStatus;
|
2276
|
-
statusEl.className = 'history-item-status';
|
2277
|
-
statusEl.classList.add(statusClass);
|
2278
|
-
}
|
2279
|
-
|
2280
|
-
// Update view button
|
2281
|
-
const viewBtn = historyItem.querySelector('.view-btn');
|
2282
|
-
if (viewBtn) {
|
2283
|
-
if (status === 'suspended') {
|
2284
|
-
viewBtn.innerHTML = '<i class="fas fa-pause-circle"></i> Suspended';
|
2285
|
-
viewBtn.disabled = true;
|
2286
|
-
} else if (status === 'failed') {
|
2287
|
-
viewBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
|
2288
|
-
viewBtn.disabled = true;
|
2289
|
-
} else if (status === 'completed') {
|
2290
|
-
viewBtn.innerHTML = '<i class="fas fa-eye"></i> View';
|
2291
|
-
viewBtn.disabled = false;
|
2292
|
-
|
2293
|
-
// Also make the PDF button visible if not already
|
2294
|
-
const pdfBtn = historyItem.querySelector('.pdf-btn');
|
2295
|
-
if (pdfBtn) {
|
2296
|
-
pdfBtn.style.display = 'inline-flex';
|
2297
|
-
}
|
2298
|
-
}
|
2299
|
-
}
|
2300
|
-
}
|
2301
|
-
}
|
2302
|
-
|
2303
|
-
// PDF Generation Functions
|
2304
|
-
function generatePdf() {
|
2305
|
-
const resultsContent = document.getElementById('results-content');
|
2306
|
-
const query = document.getElementById('result-query').textContent;
|
2307
|
-
const date = document.getElementById('result-date').textContent;
|
2308
|
-
const mode = document.getElementById('result-mode').textContent;
|
2309
|
-
|
2310
|
-
// Show loading indicator
|
2311
|
-
const loadingIndicator = document.createElement('div');
|
2312
|
-
loadingIndicator.className = 'loading-spinner centered';
|
2313
|
-
loadingIndicator.innerHTML = '<div class="spinner"></div><p style="margin-top: 10px;">Generating PDF...</p>';
|
2314
|
-
resultsContent.parentNode.insertBefore(loadingIndicator, resultsContent);
|
2315
|
-
resultsContent.style.display = 'none';
|
2316
|
-
|
2317
|
-
// Create a clone of the content for PDF generation
|
2318
|
-
const contentClone = resultsContent.cloneNode(true);
|
2319
|
-
contentClone.style.display = 'block';
|
2320
|
-
contentClone.style.position = 'absolute';
|
2321
|
-
contentClone.style.left = '-9999px';
|
2322
|
-
contentClone.style.width = '800px';
|
2323
|
-
|
2324
|
-
// Apply PDF-specific styling for better readability
|
2325
|
-
contentClone.style.background = '#ffffff';
|
2326
|
-
contentClone.style.color = '#333333';
|
2327
|
-
contentClone.style.padding = '20px';
|
2328
|
-
|
2329
|
-
// Improve visibility by adjusting styles specifically for PDF
|
2330
|
-
const applyPdfStyles = (element) => {
|
2331
|
-
// Set all text to dark color for better readability on white background
|
2332
|
-
element.querySelectorAll('*').forEach(el => {
|
2333
|
-
// Skip elements that already have inline color styles
|
2334
|
-
if (!el.style.color) {
|
2335
|
-
el.style.color = '#333333';
|
2336
|
-
}
|
2337
|
-
|
2338
|
-
// Fix background colors
|
2339
|
-
if (el.style.backgroundColor &&
|
2340
|
-
(el.style.backgroundColor.includes('var(--bg') ||
|
2341
|
-
el.style.backgroundColor.includes('#121212') ||
|
2342
|
-
el.style.backgroundColor.includes('#1e1e2d') ||
|
2343
|
-
el.style.backgroundColor.includes('#2a2a3a'))) {
|
2344
|
-
el.style.backgroundColor = '#f8f8f8';
|
2345
|
-
}
|
2346
|
-
|
2347
|
-
// Handle code blocks specifically
|
2348
|
-
if (el.tagName === 'PRE' || el.tagName === 'CODE') {
|
2349
|
-
el.style.backgroundColor = '#f5f5f5';
|
2350
|
-
el.style.border = '1px solid #e0e0e0';
|
2351
|
-
el.style.color = '#333333';
|
2352
|
-
}
|
2353
|
-
|
2354
|
-
// Make links visible
|
2355
|
-
if (el.tagName === 'A') {
|
2356
|
-
el.style.color = '#0066cc';
|
2357
|
-
el.style.textDecoration = 'underline';
|
2358
|
-
}
|
2359
|
-
});
|
2360
|
-
|
2361
|
-
// Fix specific syntax highlighting elements for PDF
|
2362
|
-
element.querySelectorAll('.hljs').forEach(hljs => {
|
2363
|
-
hljs.style.backgroundColor = '#f8f8f8';
|
2364
|
-
hljs.style.color = '#333333';
|
2365
|
-
|
2366
|
-
// Fix common syntax highlighting colors for PDF
|
2367
|
-
hljs.querySelectorAll('.hljs-keyword').forEach(el => el.style.color = '#0000cc');
|
2368
|
-
hljs.querySelectorAll('.hljs-string').forEach(el => el.style.color = '#008800');
|
2369
|
-
hljs.querySelectorAll('.hljs-number').forEach(el => el.style.color = '#aa0000');
|
2370
|
-
hljs.querySelectorAll('.hljs-comment').forEach(el => el.style.color = '#888888');
|
2371
|
-
hljs.querySelectorAll('.hljs-function').forEach(el => el.style.color = '#880000');
|
2372
|
-
});
|
2373
|
-
};
|
2374
|
-
|
2375
|
-
document.body.appendChild(contentClone);
|
2376
|
-
|
2377
|
-
// Apply PDF-specific styles
|
2378
|
-
applyPdfStyles(contentClone);
|
2379
|
-
|
2380
|
-
// Add title and metadata to the PDF content
|
2381
|
-
const headerDiv = document.createElement('div');
|
2382
|
-
headerDiv.innerHTML = `
|
2383
|
-
<h1 style="color: #6e4ff6; font-size: 24px; margin-bottom: 10px;">${query}</h1>
|
2384
|
-
<div style="margin-bottom: 20px; font-size: 14px; color: #666;">
|
2385
|
-
<p><strong>Generated:</strong> ${date}</p>
|
2386
|
-
<p><strong>Mode:</strong> ${mode}</p>
|
2387
|
-
<p><strong>Source:</strong> Deep Research Lab</p>
|
2388
|
-
</div>
|
2389
|
-
<hr style="margin-bottom: 20px; border: 1px solid #eee;">
|
2390
|
-
`;
|
2391
|
-
contentClone.insertBefore(headerDiv, contentClone.firstChild);
|
2392
|
-
|
2393
|
-
setTimeout(() => {
|
2394
|
-
try {
|
2395
|
-
// Use window.jspdf which is from the UMD bundle
|
2396
|
-
const { jsPDF } = window.jspdf;
|
2397
|
-
const pdf = new jsPDF('p', 'pt', 'a4');
|
2398
|
-
const pdfWidth = pdf.internal.pageSize.getWidth();
|
2399
|
-
const pdfHeight = pdf.internal.pageSize.getHeight();
|
2400
|
-
const margin = 40;
|
2401
|
-
const contentWidth = pdfWidth - 2 * margin;
|
2402
|
-
|
2403
|
-
// Create a more efficient PDF generation approach that keeps text selectable
|
2404
|
-
const generateTextBasedPDF = async () => {
|
2405
|
-
try {
|
2406
|
-
// Get all text elements and handle them differently than images and special content
|
2407
|
-
const elements = Array.from(contentClone.children);
|
2408
|
-
let currentY = margin;
|
2409
|
-
let pageNum = 1;
|
2410
|
-
|
2411
|
-
// Function to add a page with header
|
2412
|
-
const addPageWithHeader = (pageNum) => {
|
2413
|
-
if (pageNum > 1) {
|
2414
|
-
pdf.addPage();
|
2415
|
-
}
|
2416
|
-
pdf.setFontSize(8);
|
2417
|
-
pdf.setTextColor(100, 100, 100);
|
2418
|
-
pdf.text(`Deep Research - ${query} - Page ${pageNum}`, margin, pdfHeight - 20);
|
2419
|
-
};
|
2420
|
-
|
2421
|
-
addPageWithHeader(pageNum);
|
2422
|
-
|
2423
|
-
// Process each element
|
2424
|
-
for (const element of elements) {
|
2425
|
-
// Simple text content - handled directly by jsPDF
|
2426
|
-
if ((element.tagName === 'P' || element.tagName === 'DIV') &&
|
2427
|
-
!element.querySelector('img, canvas, svg') &&
|
2428
|
-
element.children.length === 0) {
|
2429
|
-
|
2430
|
-
pdf.setFontSize(11);
|
2431
|
-
pdf.setTextColor(0, 0, 0);
|
2432
|
-
|
2433
|
-
const text = element.textContent.trim();
|
2434
|
-
if (!text) continue; // Skip empty text
|
2435
|
-
|
2436
|
-
const textLines = pdf.splitTextToSize(text, contentWidth);
|
2437
|
-
|
2438
|
-
// Check if we need a new page
|
2439
|
-
if (currentY + (textLines.length * 14) > pdfHeight - margin) {
|
2440
|
-
pageNum++;
|
2441
|
-
addPageWithHeader(pageNum);
|
2442
|
-
currentY = margin;
|
2443
|
-
}
|
2444
|
-
|
2445
|
-
pdf.text(textLines, margin, currentY + 12);
|
2446
|
-
currentY += (textLines.length * 14) + 10;
|
2447
|
-
}
|
2448
|
-
// Handle headings
|
2449
|
-
else if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(element.tagName)) {
|
2450
|
-
const fontSize = {
|
2451
|
-
'H1': 24,
|
2452
|
-
'H2': 20,
|
2453
|
-
'H3': 16,
|
2454
|
-
'H4': 14,
|
2455
|
-
'H5': 12,
|
2456
|
-
'H6': 11
|
2457
|
-
}[element.tagName];
|
2458
|
-
|
2459
|
-
// Add heading text as native PDF text
|
2460
|
-
pdf.setFontSize(fontSize);
|
2461
|
-
|
2462
|
-
// Use a different color for headings to match styling
|
2463
|
-
if (element.tagName === 'H1') {
|
2464
|
-
pdf.setTextColor(110, 79, 246); // Purple for main headers
|
2465
|
-
} else if (element.tagName === 'H2') {
|
2466
|
-
pdf.setTextColor(70, 90, 150); // Darker blue for H2
|
2467
|
-
} else {
|
2468
|
-
pdf.setTextColor(0, 0, 0); // Black for other headings
|
2469
|
-
}
|
2470
|
-
|
2471
|
-
const text = element.textContent.trim();
|
2472
|
-
if (!text) continue; // Skip empty headings
|
2473
|
-
|
2474
|
-
const textLines = pdf.splitTextToSize(text, contentWidth);
|
2475
|
-
|
2476
|
-
// Check if we need a new page
|
2477
|
-
if (currentY + (textLines.length * (fontSize + 4)) > pdfHeight - margin) {
|
2478
|
-
pageNum++;
|
2479
|
-
addPageWithHeader(pageNum);
|
2480
|
-
currentY = margin + 40; // Reset Y position after header
|
2481
|
-
}
|
2482
|
-
|
2483
|
-
pdf.text(textLines, margin, currentY + fontSize);
|
2484
|
-
currentY += (textLines.length * (fontSize + 4)) + 10;
|
2485
|
-
|
2486
|
-
// Add a subtle underline for H1 and H2
|
2487
|
-
if (element.tagName === 'H1' || element.tagName === 'H2') {
|
2488
|
-
pdf.setDrawColor(110, 79, 246, 0.5);
|
2489
|
-
pdf.setLineWidth(0.5);
|
2490
|
-
pdf.line(
|
2491
|
-
margin,
|
2492
|
-
currentY - 5,
|
2493
|
-
margin + Math.min(contentWidth, pdf.getTextWidth(text) * 1.2),
|
2494
|
-
currentY - 5
|
2495
|
-
);
|
2496
|
-
currentY += 5; // Add a bit more space after underlined headings
|
2497
|
-
}
|
2498
|
-
}
|
2499
|
-
// Handle lists
|
2500
|
-
else if (element.tagName === 'UL' || element.tagName === 'OL') {
|
2501
|
-
pdf.setFontSize(11);
|
2502
|
-
pdf.setTextColor(0, 0, 0);
|
2503
|
-
|
2504
|
-
const listItems = element.querySelectorAll('li');
|
2505
|
-
let itemNumber = 1;
|
2506
|
-
|
2507
|
-
for (const item of listItems) {
|
2508
|
-
const prefix = element.tagName === 'UL' ? '• ' : `${itemNumber}. `;
|
2509
|
-
const text = item.textContent.trim();
|
2510
|
-
|
2511
|
-
if (!text) continue; // Skip empty list items
|
2512
|
-
|
2513
|
-
// Split text to fit width, accounting for bullet/number indent
|
2514
|
-
const textLines = pdf.splitTextToSize(text, contentWidth - 15);
|
2515
|
-
|
2516
|
-
// Check if we need a new page
|
2517
|
-
if (currentY + (textLines.length * 14) > pdfHeight - margin) {
|
2518
|
-
pageNum++;
|
2519
|
-
addPageWithHeader(pageNum);
|
2520
|
-
currentY = margin;
|
2521
|
-
}
|
2522
|
-
|
2523
|
-
// Add the bullet/number
|
2524
|
-
pdf.text(prefix, margin, currentY + 12);
|
2525
|
-
|
2526
|
-
// Add the text with indent
|
2527
|
-
pdf.text(textLines, margin + 15, currentY + 12);
|
2528
|
-
currentY += (textLines.length * 14) + 5;
|
2529
|
-
|
2530
|
-
if (element.tagName === 'OL') itemNumber++;
|
2531
|
-
}
|
2532
|
-
|
2533
|
-
currentY += 5; // Extra space after list
|
2534
|
-
}
|
2535
|
-
// Handle code blocks as text
|
2536
|
-
else if (element.tagName === 'PRE' || element.querySelector('pre')) {
|
2537
|
-
const codeElement = element.tagName === 'PRE' ? element : element.querySelector('pre');
|
2538
|
-
const codeText = codeElement.textContent.trim();
|
2539
|
-
|
2540
|
-
if (!codeText) continue; // Skip empty code blocks
|
2541
|
-
|
2542
|
-
// Use monospace font for code
|
2543
|
-
pdf.setFont("courier", "normal");
|
2544
|
-
pdf.setFontSize(9); // Smaller font for code
|
2545
|
-
|
2546
|
-
// Calculate code block size
|
2547
|
-
const codeLines = codeText.split('\n');
|
2548
|
-
const lineHeight = 10; // Smaller line height for code
|
2549
|
-
const codeBlockHeight = (codeLines.length * lineHeight) + 20; // Add padding
|
2550
|
-
|
2551
|
-
// Add a background for the code block
|
2552
|
-
if (currentY + codeBlockHeight > pdfHeight - margin) {
|
2553
|
-
pageNum++;
|
2554
|
-
addPageWithHeader(pageNum);
|
2555
|
-
currentY = margin;
|
2556
|
-
}
|
2557
|
-
|
2558
|
-
// Draw code block background
|
2559
|
-
pdf.setFillColor(245, 245, 245); // Light gray background
|
2560
|
-
pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'F');
|
2561
|
-
|
2562
|
-
// Draw a border
|
2563
|
-
pdf.setDrawColor(220, 220, 220);
|
2564
|
-
pdf.setLineWidth(0.5);
|
2565
|
-
pdf.rect(margin - 5, currentY, contentWidth + 10, codeBlockHeight, 'S');
|
2566
|
-
|
2567
|
-
// Add the code text
|
2568
|
-
pdf.setTextColor(0, 0, 0);
|
2569
|
-
currentY += 10; // Add padding at top
|
2570
|
-
|
2571
|
-
codeLines.forEach(line => {
|
2572
|
-
// Handle indentation by preserving leading spaces
|
2573
|
-
const spacePadding = line.match(/^(\s*)/)[0].length;
|
2574
|
-
const visibleLine = line.trimLeft();
|
2575
|
-
|
2576
|
-
// Calculate width of space character
|
2577
|
-
const spaceWidth = pdf.getStringUnitWidth(' ') * 9 / pdf.internal.scaleFactor;
|
2578
|
-
|
2579
|
-
pdf.text(visibleLine, margin + (spacePadding * spaceWidth), currentY);
|
2580
|
-
currentY += lineHeight;
|
2581
|
-
});
|
2582
|
-
|
2583
|
-
currentY += 10; // Add padding at bottom
|
2584
|
-
|
2585
|
-
// Reset to normal font
|
2586
|
-
pdf.setFont("helvetica", "normal");
|
2587
|
-
pdf.setFontSize(11);
|
2588
|
-
}
|
2589
|
-
// Handle tables as text
|
2590
|
-
else if (element.tagName === 'TABLE' || element.querySelector('table')) {
|
2591
|
-
const tableElement = element.tagName === 'TABLE' ? element : element.querySelector('table');
|
2592
|
-
|
2593
|
-
if (!tableElement) continue;
|
2594
|
-
|
2595
|
-
// Get table rows
|
2596
|
-
const rows = Array.from(tableElement.querySelectorAll('tr'));
|
2597
|
-
if (rows.length === 0) continue;
|
2598
|
-
|
2599
|
-
// Calculate column widths
|
2600
|
-
const headerCells = Array.from(rows[0].querySelectorAll('th, td'));
|
2601
|
-
const numColumns = headerCells.length;
|
2602
|
-
|
2603
|
-
if (numColumns === 0) continue;
|
2604
|
-
|
2605
|
-
// Default column width distribution (equal)
|
2606
|
-
const colWidth = contentWidth / numColumns;
|
2607
|
-
|
2608
|
-
// Start drawing table
|
2609
|
-
let tableY = currentY + 10;
|
2610
|
-
|
2611
|
-
// Check if we need a new page
|
2612
|
-
if (tableY + (rows.length * 20) > pdfHeight - margin) {
|
2613
|
-
pageNum++;
|
2614
|
-
addPageWithHeader(pageNum);
|
2615
|
-
tableY = margin + 10;
|
2616
|
-
currentY = margin;
|
2617
|
-
}
|
2618
|
-
|
2619
|
-
// Draw table header
|
2620
|
-
pdf.setFillColor(240, 240, 240);
|
2621
|
-
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
2622
|
-
|
2623
|
-
pdf.setFont("helvetica", "bold");
|
2624
|
-
pdf.setFontSize(10);
|
2625
|
-
pdf.setTextColor(0, 0, 0);
|
2626
|
-
|
2627
|
-
headerCells.forEach((cell, index) => {
|
2628
|
-
const text = cell.textContent.trim();
|
2629
|
-
const x = margin + (index * colWidth) + 5;
|
2630
|
-
pdf.text(text, x, tableY + 13);
|
2631
|
-
});
|
2632
|
-
|
2633
|
-
// Draw horizontal line after header
|
2634
|
-
pdf.setDrawColor(200, 200, 200);
|
2635
|
-
pdf.setLineWidth(0.5);
|
2636
|
-
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
2637
|
-
|
2638
|
-
tableY += 20;
|
2639
|
-
|
2640
|
-
// Draw table rows
|
2641
|
-
pdf.setFont("helvetica", "normal");
|
2642
|
-
for (let i = 1; i < rows.length; i++) {
|
2643
|
-
// Check if we need a new page
|
2644
|
-
if (tableY + 20 > pdfHeight - margin) {
|
2645
|
-
// Draw bottom border for last row on current page
|
2646
|
-
pdf.line(margin, tableY, margin + contentWidth, tableY);
|
2647
|
-
|
2648
|
-
// Add new page
|
2649
|
-
pageNum++;
|
2650
|
-
addPageWithHeader(pageNum);
|
2651
|
-
tableY = margin + 10;
|
2652
|
-
|
2653
|
-
// Redraw header on new page
|
2654
|
-
pdf.setFillColor(240, 240, 240);
|
2655
|
-
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
2656
|
-
|
2657
|
-
pdf.setFont("helvetica", "bold");
|
2658
|
-
headerCells.forEach((cell, index) => {
|
2659
|
-
const text = cell.textContent.trim();
|
2660
|
-
const x = margin + (index * colWidth) + 5;
|
2661
|
-
pdf.text(text, x, tableY + 13);
|
2662
|
-
});
|
2663
|
-
|
2664
|
-
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
2665
|
-
tableY += 20;
|
2666
|
-
pdf.setFont("helvetica", "normal");
|
2667
|
-
}
|
2668
|
-
|
2669
|
-
// Get cells for this row
|
2670
|
-
const cells = Array.from(rows[i].querySelectorAll('td, th'));
|
2671
|
-
|
2672
|
-
// Alternate row background for better readability
|
2673
|
-
if (i % 2 === 0) {
|
2674
|
-
pdf.setFillColor(250, 250, 250);
|
2675
|
-
pdf.rect(margin, tableY, contentWidth, 20, 'F');
|
2676
|
-
}
|
2677
|
-
|
2678
|
-
// Add cell content
|
2679
|
-
cells.forEach((cell, index) => {
|
2680
|
-
const text = cell.textContent.trim();
|
2681
|
-
const x = margin + (index * colWidth) + 5;
|
2682
|
-
pdf.text(text, x, tableY + 13);
|
2683
|
-
});
|
2684
|
-
|
2685
|
-
// Draw horizontal line after row
|
2686
|
-
pdf.line(margin, tableY + 20, margin + contentWidth, tableY + 20);
|
2687
|
-
tableY += 20;
|
2688
|
-
}
|
2689
|
-
|
2690
|
-
// Draw vertical lines for columns
|
2691
|
-
for (let i = 0; i <= numColumns; i++) {
|
2692
|
-
const x = margin + (i * colWidth);
|
2693
|
-
pdf.line(x, currentY + 10, x, tableY);
|
2694
|
-
}
|
2695
|
-
|
2696
|
-
currentY = tableY + 10;
|
2697
|
-
}
|
2698
|
-
// Images still need to be handled as images
|
2699
|
-
else if (element.tagName === 'IMG' || element.querySelector('img')) {
|
2700
|
-
const imgElement = element.tagName === 'IMG' ? element : element.querySelector('img');
|
2701
|
-
|
2702
|
-
if (!imgElement || !imgElement.src) continue;
|
2703
|
-
|
2704
|
-
try {
|
2705
|
-
// Create a new image to get dimensions
|
2706
|
-
const img = new Image();
|
2707
|
-
img.src = imgElement.src;
|
2708
|
-
|
2709
|
-
// Calculate dimensions
|
2710
|
-
const imgWidth = contentWidth;
|
2711
|
-
const imgHeight = img.height * (contentWidth / img.width);
|
2712
|
-
|
2713
|
-
// Check if we need a new page
|
2714
|
-
if (currentY + imgHeight > pdfHeight - margin) {
|
2715
|
-
pageNum++;
|
2716
|
-
addPageWithHeader(pageNum);
|
2717
|
-
currentY = margin;
|
2718
|
-
}
|
2719
|
-
|
2720
|
-
// Add image to PDF
|
2721
|
-
pdf.addImage(img.src, 'JPEG', margin, currentY, imgWidth, imgHeight);
|
2722
|
-
currentY += imgHeight + 10;
|
2723
|
-
} catch (imgError) {
|
2724
|
-
console.error('Error adding image:', imgError);
|
2725
|
-
pdf.text("[Image could not be rendered]", margin, currentY + 12);
|
2726
|
-
currentY += 20;
|
2727
|
-
}
|
2728
|
-
}
|
2729
|
-
// Other complex elements still use html2canvas as fallback
|
2730
|
-
else {
|
2731
|
-
try {
|
2732
|
-
const canvas = await html2canvas(element, {
|
2733
|
-
scale: 2,
|
2734
|
-
useCORS: true,
|
2735
|
-
logging: false,
|
2736
|
-
backgroundColor: '#FFFFFF'
|
2737
|
-
});
|
2738
|
-
|
2739
|
-
const imgData = canvas.toDataURL('image/png');
|
2740
|
-
const imgWidth = contentWidth;
|
2741
|
-
const imgHeight = (canvas.height * contentWidth) / canvas.width;
|
2742
|
-
|
2743
|
-
if (currentY + imgHeight > pdfHeight - margin) {
|
2744
|
-
pageNum++;
|
2745
|
-
addPageWithHeader(pageNum);
|
2746
|
-
currentY = margin;
|
2747
|
-
}
|
2748
|
-
|
2749
|
-
pdf.addImage(imgData, 'PNG', margin, currentY, imgWidth, imgHeight);
|
2750
|
-
currentY += imgHeight + 10;
|
2751
|
-
} catch (canvasError) {
|
2752
|
-
console.error('Error rendering complex element:', canvasError);
|
2753
|
-
pdf.text("[Complex content could not be rendered]", margin, currentY + 12);
|
2754
|
-
currentY += 20;
|
2755
|
-
}
|
2756
|
-
}
|
2757
|
-
}
|
2758
|
-
|
2759
|
-
// Download the PDF
|
2760
|
-
const filename = `${query.replace(/[^a-z0-9]/gi, '_').substring(0, 30).toLowerCase()}_research.pdf`;
|
2761
|
-
pdf.save(filename);
|
2762
|
-
|
2763
|
-
// Clean up
|
2764
|
-
document.body.removeChild(contentClone);
|
2765
|
-
resultsContent.style.display = 'block';
|
2766
|
-
loadingIndicator.remove();
|
2767
|
-
} catch (error) {
|
2768
|
-
console.error('Error generating PDF:', error);
|
2769
|
-
alert('An error occurred while generating the PDF. Please try again.');
|
2770
|
-
document.body.removeChild(contentClone);
|
2771
|
-
resultsContent.style.display = 'block';
|
2772
|
-
loadingIndicator.remove();
|
2773
|
-
}
|
2774
|
-
};
|
2775
|
-
|
2776
|
-
generateTextBasedPDF();
|
2777
|
-
} catch (error) {
|
2778
|
-
console.error('Error initializing PDF generation:', error);
|
2779
|
-
alert('An error occurred while preparing the PDF. Please try again.');
|
2780
|
-
document.body.removeChild(contentClone);
|
2781
|
-
resultsContent.style.display = 'block';
|
2782
|
-
loadingIndicator.remove();
|
2783
|
-
}
|
2784
|
-
}, 100);
|
2785
|
-
}
|
2786
|
-
|
2787
|
-
// Function to generate PDF from a specific research ID
|
2788
|
-
async function generatePdfFromResearch(researchId) {
|
2789
|
-
try {
|
2790
|
-
// Load research details
|
2791
|
-
const detailsResponse = await fetch(getApiUrl(`/api/research/${researchId}`));
|
2792
|
-
const details = await detailsResponse.json();
|
2793
|
-
|
2794
|
-
// Load the report content
|
2795
|
-
const reportResponse = await fetch(getApiUrl(`/api/report/${researchId}`));
|
2796
|
-
const reportData = await reportResponse.json();
|
2797
|
-
|
2798
|
-
if (reportData.status === 'success') {
|
2799
|
-
// Create a temporary container to render the content
|
2800
|
-
const tempContainer = document.createElement('div');
|
2801
|
-
tempContainer.className = 'results-content pdf-optimized';
|
2802
|
-
tempContainer.style.display = 'none';
|
2803
|
-
document.body.appendChild(tempContainer);
|
2804
|
-
|
2805
|
-
// Render markdown with optimized styles for PDF
|
2806
|
-
const renderedContent = marked.parse(reportData.content);
|
2807
|
-
tempContainer.innerHTML = renderedContent;
|
2808
|
-
|
2809
|
-
// Apply syntax highlighting
|
2810
|
-
tempContainer.querySelectorAll('pre code').forEach((block) => {
|
2811
|
-
hljs.highlightElement(block);
|
2812
|
-
});
|
2813
|
-
|
2814
|
-
// Format date with duration
|
2815
|
-
let dateText = formatDate(
|
2816
|
-
new Date(details.completed_at || details.created_at),
|
2817
|
-
details.duration_seconds
|
2818
|
-
);
|
2819
|
-
|
2820
|
-
// Set up data for PDF generation
|
2821
|
-
document.getElementById('result-query').textContent = details.query;
|
2822
|
-
document.getElementById('result-date').textContent = dateText;
|
2823
|
-
document.getElementById('result-mode').textContent = details.mode === 'quick' ? 'Quick Summary' : 'Detailed Report';
|
2824
|
-
|
2825
|
-
// Replace the current content with our temporary content
|
2826
|
-
const resultsContent = document.getElementById('results-content');
|
2827
|
-
const originalContent = resultsContent.innerHTML;
|
2828
|
-
resultsContent.innerHTML = tempContainer.innerHTML;
|
2829
|
-
|
2830
|
-
// Generate the PDF
|
2831
|
-
generatePdf();
|
2832
|
-
|
2833
|
-
// Restore original content if we're not on the results page
|
2834
|
-
setTimeout(() => {
|
2835
|
-
if (!document.getElementById('research-results').classList.contains('active')) {
|
2836
|
-
resultsContent.innerHTML = originalContent;
|
2837
|
-
}
|
2838
|
-
document.body.removeChild(tempContainer);
|
2839
|
-
}, 500);
|
2840
|
-
|
2841
|
-
} else {
|
2842
|
-
alert('Error loading report. Could not generate PDF.');
|
2843
|
-
}
|
2844
|
-
} catch (error) {
|
2845
|
-
console.error('Error generating PDF:', error);
|
2846
|
-
alert('An error occurred while generating the PDF. Please try again.');
|
2847
|
-
}
|
2848
|
-
}
|
2849
|
-
|
2850
|
-
// Initialize the terminate button event listener
|
2851
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
2852
|
-
if (terminateBtn) {
|
2853
|
-
terminateBtn.addEventListener('click', function() {
|
2854
|
-
if (currentResearchId) {
|
2855
|
-
terminateResearch(currentResearchId);
|
2856
|
-
} else {
|
2857
|
-
console.error('No active research ID found for termination');
|
2858
|
-
alert('No active research ID found. Please try again.');
|
2859
|
-
}
|
2860
|
-
});
|
2861
|
-
}
|
2862
|
-
|
2863
|
-
// Initialize PDF download button
|
2864
|
-
const downloadPdfBtn = document.getElementById('download-pdf-btn');
|
2865
|
-
if (downloadPdfBtn) {
|
2866
|
-
downloadPdfBtn.addEventListener('click', generatePdf);
|
2867
|
-
}
|
2868
|
-
|
2869
|
-
// Function to set up the research form
|
2870
|
-
function setupResearchForm() {
|
2871
|
-
const researchForm = document.getElementById('research-form');
|
2872
|
-
const notificationToggle = document.getElementById('notification-toggle');
|
2873
|
-
|
2874
|
-
// Set notification state from toggle
|
2875
|
-
if (notificationToggle) {
|
2876
|
-
notificationsEnabled = notificationToggle.checked;
|
2877
|
-
|
2878
|
-
// Listen for changes to the toggle
|
2879
|
-
notificationToggle.addEventListener('change', function() {
|
2880
|
-
notificationsEnabled = this.checked;
|
2881
|
-
// Store preference in localStorage for persistence
|
2882
|
-
localStorage.setItem('notificationsEnabled', notificationsEnabled);
|
2883
|
-
});
|
2884
|
-
|
2885
|
-
// Load saved preference from localStorage
|
2886
|
-
const savedPref = localStorage.getItem('notificationsEnabled');
|
2887
|
-
if (savedPref !== null) {
|
2888
|
-
notificationsEnabled = savedPref === 'true';
|
2889
|
-
notificationToggle.checked = notificationsEnabled;
|
2890
|
-
}
|
2891
|
-
}
|
2892
|
-
|
2893
|
-
// ... existing form setup ...
|
2894
|
-
}
|
2895
|
-
|
2896
|
-
// Add event listener for view results button
|
2897
|
-
const viewResultsBtn = document.getElementById('view-results-btn');
|
2898
|
-
if (viewResultsBtn) {
|
2899
|
-
viewResultsBtn.addEventListener('click', () => {
|
2900
|
-
console.log('View results button clicked');
|
2901
|
-
if (currentResearchId) {
|
2902
|
-
loadResearch(currentResearchId);
|
2903
|
-
} else {
|
2904
|
-
console.error('No research ID available');
|
2905
|
-
}
|
2906
|
-
});
|
2907
|
-
}
|
2908
|
-
|
2909
|
-
// Add listener for research_completed custom event
|
2910
|
-
document.addEventListener('research_completed', (event) => {
|
2911
|
-
console.log('Research completed event received:', event.detail);
|
2912
|
-
const data = event.detail;
|
2913
|
-
|
2914
|
-
// Mark research as no longer in progress
|
2915
|
-
isResearchInProgress = false;
|
2916
|
-
|
2917
|
-
// Hide terminate button
|
2918
|
-
const terminateBtn = document.getElementById('terminate-research-btn');
|
2919
|
-
if (terminateBtn) {
|
2920
|
-
terminateBtn.style.display = 'none';
|
2921
|
-
}
|
2922
|
-
|
2923
|
-
// Update navigation
|
2924
|
-
updateNavigationBasedOnResearchStatus();
|
2925
|
-
});
|
2926
|
-
|
2927
|
-
// Function to reset progress animations and UI elements
|
2928
|
-
function resetProgressAnimations() {
|
2929
|
-
// Reset the progress bar animation
|
2930
|
-
const progressFill = document.getElementById('progress-fill');
|
2931
|
-
if (progressFill) {
|
2932
|
-
// Force a reflow to reset the animation
|
2933
|
-
progressFill.style.display = 'none';
|
2934
|
-
progressFill.offsetHeight; // Trigger reflow
|
2935
|
-
progressFill.style.display = 'block';
|
2936
|
-
}
|
2937
|
-
|
2938
|
-
// Reset any spinning icons
|
2939
|
-
const spinners = document.querySelectorAll('.fa-spinner.fa-spin');
|
2940
|
-
spinners.forEach(spinner => {
|
2941
|
-
const parent = spinner.parentElement;
|
2942
|
-
if (parent && parent.tagName === 'BUTTON') {
|
2943
|
-
if (parent.classList.contains('terminate-btn')) {
|
2944
|
-
parent.innerHTML = '<i class="fas fa-stop-circle"></i> Terminate Research';
|
2945
|
-
parent.disabled = false;
|
2946
|
-
}
|
2947
|
-
}
|
2948
|
-
});
|
2949
|
-
|
2950
|
-
// Ensure the isTerminating flag is reset
|
2951
|
-
isTerminating = false;
|
2952
|
-
}
|
2953
|
-
|
2954
|
-
// Create a dynamic favicon
|
2955
|
-
function createDynamicFavicon(emoji = '⚡') {
|
2956
|
-
console.log(`Creating dynamic favicon with emoji: ${emoji}`);
|
2957
|
-
|
2958
|
-
// Create a canvas element
|
2959
|
-
const canvas = document.createElement('canvas');
|
2960
|
-
canvas.width = 32;
|
2961
|
-
canvas.height = 32;
|
2962
|
-
|
2963
|
-
// Get the canvas context
|
2964
|
-
const ctx = canvas.getContext('2d');
|
2965
|
-
|
2966
|
-
// Clear the canvas with a transparent background
|
2967
|
-
ctx.clearRect(0, 0, 32, 32);
|
2968
|
-
|
2969
|
-
// Set the font size and font family
|
2970
|
-
ctx.font = '24px Arial';
|
2971
|
-
ctx.textAlign = 'center';
|
2972
|
-
ctx.textBaseline = 'middle';
|
2973
|
-
|
2974
|
-
// Draw the emoji in the center of the canvas
|
2975
|
-
ctx.fillText(emoji, 16, 16);
|
2976
|
-
|
2977
|
-
// Convert canvas to favicon URL
|
2978
|
-
const faviconUrl = canvas.toDataURL('image/png');
|
2979
|
-
|
2980
|
-
// Find existing favicon or create a new one
|
2981
|
-
let link = document.querySelector('link[rel="icon"]');
|
2982
|
-
if (!link) {
|
2983
|
-
link = document.createElement('link');
|
2984
|
-
link.rel = 'icon';
|
2985
|
-
link.id = 'dynamic-favicon';
|
2986
|
-
document.head.appendChild(link);
|
2987
|
-
}
|
2988
|
-
|
2989
|
-
// Update the favicon
|
2990
|
-
link.type = 'image/x-icon';
|
2991
|
-
link.href = faviconUrl;
|
2992
|
-
|
2993
|
-
return faviconUrl;
|
2994
|
-
}
|
2995
|
-
|
2996
|
-
// Function to set favicon based on research mode
|
2997
|
-
function setFavicon(mode) {
|
2998
|
-
const emoji = mode === 'detailed' ? '🔬' : '⚡';
|
2999
|
-
return createDynamicFavicon(emoji);
|
3000
|
-
}
|
3001
|
-
|
3002
|
-
// Global log tracking variables
|
3003
|
-
let consoleLogEntries = [];
|
3004
|
-
let logCount = 0;
|
3005
|
-
let lastLogMessage = null; // Track the last log message to prevent duplicates
|
3006
|
-
let lastLogTimestamp = null; // Track the last log timestamp
|
3007
|
-
let viewingResearchId = null; // Track which research ID is currently being viewed
|
3008
|
-
|
3009
|
-
// Function to filter console logs by type
|
3010
|
-
function filterConsoleLogs(filterType = 'all') {
|
3011
|
-
console.log(`----- FILTER LOGS START (${filterType}) -----`);
|
3012
|
-
console.log(`Filtering logs by type: ${filterType}`);
|
3013
|
-
|
3014
|
-
// Make sure the filter type is lowercase for consistency
|
3015
|
-
filterType = filterType.toLowerCase();
|
3016
|
-
|
3017
|
-
// Get all log entries from the DOM
|
3018
|
-
const logEntries = document.querySelectorAll('.console-log-entry');
|
3019
|
-
console.log(`Found ${logEntries.length} log entries to filter`);
|
3020
|
-
|
3021
|
-
// If no entries found, exit early
|
3022
|
-
if (logEntries.length === 0) {
|
3023
|
-
console.log('No log entries found to filter');
|
3024
|
-
console.log(`----- FILTER LOGS END (${filterType}) -----`);
|
3025
|
-
return;
|
3026
|
-
}
|
3027
|
-
|
3028
|
-
let visibleCount = 0;
|
3029
|
-
|
3030
|
-
// Apply filters
|
3031
|
-
logEntries.forEach(entry => {
|
3032
|
-
// Use the data attribute directly - this comes directly from the database
|
3033
|
-
const logType = entry.dataset.logType;
|
3034
|
-
|
3035
|
-
// Determine visibility based on filter type
|
3036
|
-
let shouldShow = false;
|
3037
|
-
|
3038
|
-
switch (filterType) {
|
3039
|
-
case 'all':
|
3040
|
-
shouldShow = true;
|
3041
|
-
break;
|
3042
|
-
case 'info':
|
3043
|
-
shouldShow = logType === 'info';
|
3044
|
-
break;
|
3045
|
-
case 'milestone':
|
3046
|
-
case 'milestones': // Handle plural form too
|
3047
|
-
shouldShow = logType === 'milestone';
|
3048
|
-
break;
|
3049
|
-
case 'error':
|
3050
|
-
case 'errors': // Handle plural form too
|
3051
|
-
shouldShow = logType === 'error';
|
3052
|
-
break;
|
3053
|
-
default:
|
3054
|
-
shouldShow = true; // Default to showing everything
|
3055
|
-
console.warn(`Unknown filter type: ${filterType}, showing all logs`);
|
3056
|
-
}
|
3057
|
-
|
3058
|
-
// Set display style based on filter result
|
3059
|
-
entry.style.display = shouldShow ? '' : 'none';
|
3060
|
-
|
3061
|
-
if (shouldShow) {
|
3062
|
-
visibleCount++;
|
3063
|
-
}
|
3064
|
-
});
|
3065
|
-
|
3066
|
-
console.log(`Filtering complete. Showing ${visibleCount} of ${logEntries.length} logs with filter: ${filterType}`);
|
3067
|
-
|
3068
|
-
// Show 'no logs' message if all logs are filtered out
|
3069
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3070
|
-
if (consoleContainer && logEntries.length > 0) {
|
3071
|
-
// Remove any existing empty message
|
3072
|
-
const existingEmptyMessage = consoleContainer.querySelector('.empty-log-message');
|
3073
|
-
if (existingEmptyMessage) {
|
3074
|
-
existingEmptyMessage.remove();
|
3075
|
-
}
|
3076
|
-
|
3077
|
-
// Add empty message if needed
|
3078
|
-
if (visibleCount === 0) {
|
3079
|
-
console.log(`Adding 'no logs' message for filter: ${filterType}`);
|
3080
|
-
const newEmptyMessage = document.createElement('div');
|
3081
|
-
newEmptyMessage.className = 'empty-log-message';
|
3082
|
-
newEmptyMessage.textContent = `No ${filterType} logs to display.`;
|
3083
|
-
consoleContainer.appendChild(newEmptyMessage);
|
3084
|
-
}
|
3085
|
-
}
|
3086
|
-
|
3087
|
-
console.log(`----- FILTER LOGS END (${filterType}) -----`);
|
3088
|
-
}
|
3089
|
-
|
3090
|
-
// Function to initialize the log panel
|
3091
|
-
function initializeLogPanel() {
|
3092
|
-
console.log('Initializing log panel');
|
3093
|
-
|
3094
|
-
// Get DOM elements
|
3095
|
-
const logPanelToggle = document.getElementById('log-panel-toggle');
|
3096
|
-
const logPanelContent = document.getElementById('log-panel-content');
|
3097
|
-
const filterButtons = document.querySelectorAll('.filter-buttons .small-btn');
|
3098
|
-
|
3099
|
-
// Check if elements exist
|
3100
|
-
if (!logPanelToggle || !logPanelContent) {
|
3101
|
-
console.error('Log panel elements not found');
|
3102
|
-
return;
|
3103
|
-
}
|
3104
|
-
|
3105
|
-
console.log('Log panel elements found:', {
|
3106
|
-
toggle: logPanelToggle ? 'Found' : 'Missing',
|
3107
|
-
content: logPanelContent ? 'Found' : 'Missing',
|
3108
|
-
filterButtons: filterButtons.length > 0 ? `Found ${filterButtons.length} buttons` : 'Missing'
|
3109
|
-
});
|
3110
|
-
|
3111
|
-
// Set up toggle click handler
|
3112
|
-
logPanelToggle.addEventListener('click', function() {
|
3113
|
-
console.log('Log panel toggle clicked');
|
3114
|
-
|
3115
|
-
// Toggle collapsed state
|
3116
|
-
logPanelContent.classList.toggle('collapsed');
|
3117
|
-
logPanelToggle.classList.toggle('collapsed');
|
3118
|
-
|
3119
|
-
// Update toggle icon
|
3120
|
-
const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
|
3121
|
-
if (toggleIcon) {
|
3122
|
-
if (logPanelToggle.classList.contains('collapsed')) {
|
3123
|
-
toggleIcon.className = 'fas fa-chevron-right toggle-icon';
|
3124
|
-
} else {
|
3125
|
-
toggleIcon.className = 'fas fa-chevron-down toggle-icon';
|
3126
|
-
|
3127
|
-
// Scroll log container to bottom
|
3128
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3129
|
-
if (consoleContainer) {
|
3130
|
-
setTimeout(() => {
|
3131
|
-
consoleContainer.scrollTop = consoleContainer.scrollHeight;
|
3132
|
-
}, 10);
|
3133
|
-
}
|
3134
|
-
}
|
3135
|
-
}
|
3136
|
-
|
3137
|
-
console.log('Log panel is now ' + (logPanelContent.classList.contains('collapsed') ? 'collapsed' : 'expanded'));
|
3138
|
-
});
|
3139
|
-
|
3140
|
-
// Initial state - collapsed
|
3141
|
-
logPanelContent.classList.add('collapsed');
|
3142
|
-
logPanelToggle.classList.add('collapsed');
|
3143
|
-
const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
|
3144
|
-
if (toggleIcon) {
|
3145
|
-
toggleIcon.className = 'fas fa-chevron-right toggle-icon';
|
3146
|
-
}
|
3147
|
-
|
3148
|
-
// Initial filtering - use a longer delay to ensure DOM is ready
|
3149
|
-
setTimeout(() => {
|
3150
|
-
console.log('Applying initial log filter: all');
|
3151
|
-
filterConsoleLogs('all');
|
3152
|
-
}, 300);
|
3153
|
-
|
3154
|
-
console.log('Log panel initialization completed');
|
3155
|
-
}
|
3156
|
-
|
3157
|
-
// Function to clear all console logs
|
3158
|
-
function clearConsoleLogs() {
|
3159
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3160
|
-
if (!consoleContainer) return;
|
3161
|
-
|
3162
|
-
console.log('Clearing console logs');
|
3163
|
-
|
3164
|
-
// Reset the processed messages set to allow new logs after clearing
|
3165
|
-
window.processedMessages = new Set();
|
3166
|
-
|
3167
|
-
// Clear the container
|
3168
|
-
consoleContainer.innerHTML = '<div class="empty-log-message">No logs yet. Research logs will appear here as they occur.</div>';
|
3169
|
-
|
3170
|
-
// Reset the log entries array
|
3171
|
-
consoleLogEntries = [];
|
3172
|
-
|
3173
|
-
// Reset the log count
|
3174
|
-
logCount = 0;
|
3175
|
-
updateLogIndicator();
|
3176
|
-
|
3177
|
-
// Reset last message tracking
|
3178
|
-
lastLogMessage = null;
|
3179
|
-
lastLogTimestamp = null;
|
3180
|
-
|
3181
|
-
// DO NOT reset viewingResearchId here, as clearing logs doesn't mean
|
3182
|
-
// we're changing which research we're viewing
|
3183
|
-
|
3184
|
-
console.log('Console logs cleared');
|
3185
|
-
}
|
3186
|
-
|
3187
|
-
// Function to determine if a log message is a milestone
|
3188
|
-
function isMilestoneLog(message, metadata) {
|
3189
|
-
if (!message) return false;
|
3190
|
-
|
3191
|
-
// Critical milestones - main research flow points
|
3192
|
-
const criticalMilestones = [
|
3193
|
-
// Research start/end
|
3194
|
-
/^Research (started|complete)/i,
|
3195
|
-
/^Starting research/i,
|
3196
|
-
|
3197
|
-
// Iteration markers
|
3198
|
-
/^Starting iteration \d+/i,
|
3199
|
-
/^Iteration \d+ complete/i,
|
3200
|
-
|
3201
|
-
// Final completion
|
3202
|
-
/^Research completed successfully/i,
|
3203
|
-
/^Report generation complete/i,
|
3204
|
-
/^Writing research report to file/i
|
3205
|
-
];
|
3206
|
-
|
3207
|
-
// Check for critical milestones first
|
3208
|
-
const isCriticalMilestone = criticalMilestones.some(pattern => pattern.test(message));
|
3209
|
-
if (isCriticalMilestone) return true;
|
3210
|
-
|
3211
|
-
// Check metadata phase for critical phases only
|
3212
|
-
const isCriticalPhase = metadata && (
|
3213
|
-
metadata.phase === 'init' ||
|
3214
|
-
metadata.phase === 'iteration_start' ||
|
3215
|
-
metadata.phase === 'iteration_complete' ||
|
3216
|
-
metadata.phase === 'complete' ||
|
3217
|
-
metadata.phase === 'report_generation' ||
|
3218
|
-
metadata.phase === 'report_complete'
|
3219
|
-
);
|
3220
|
-
|
3221
|
-
return isCriticalPhase;
|
3222
|
-
}
|
3223
|
-
|
3224
|
-
// Initialize the log panel when the application starts
|
3225
|
-
function initializeAppWithLogs() {
|
3226
|
-
// Original initializeApp function
|
3227
|
-
const originalInitializeApp = window.initializeApp || initializeApp;
|
3228
|
-
window.initializeApp = function() {
|
3229
|
-
// Call the original initialization
|
3230
|
-
originalInitializeApp();
|
3231
|
-
|
3232
|
-
// Ensure global log variables are initialized
|
3233
|
-
window.consoleLogEntries = [];
|
3234
|
-
window.logCount = 0;
|
3235
|
-
window.lastLogMessage = null;
|
3236
|
-
window.lastLogTimestamp = null;
|
3237
|
-
|
3238
|
-
// Initialize the log panel with a delay to ensure DOM is ready
|
3239
|
-
setTimeout(() => {
|
3240
|
-
initializeLogPanel();
|
3241
|
-
|
3242
|
-
// Add an initial welcome log entry
|
3243
|
-
addConsoleLog('Research system initialized and ready', 'milestone');
|
3244
|
-
|
3245
|
-
console.log('Log panel initialization completed');
|
3246
|
-
}, 100);
|
3247
|
-
};
|
3248
|
-
}
|
3249
|
-
|
3250
|
-
// Call the initialization function immediately
|
3251
|
-
initializeAppWithLogs();
|
3252
|
-
|
3253
|
-
// Function to get active research ID
|
3254
|
-
function getActiveResearchId() {
|
3255
|
-
return window.currentResearchId || currentResearchId;
|
3256
|
-
}
|
3257
|
-
|
3258
|
-
// Function to update research duration on results display
|
3259
|
-
function updateResearchDuration(metadata) {
|
3260
|
-
if (!metadata || !metadata.started_at || !metadata.completed_at) return;
|
3261
|
-
|
3262
|
-
try {
|
3263
|
-
// Parse ISO dates
|
3264
|
-
const startDate = new Date(metadata.started_at);
|
3265
|
-
const endDate = new Date(metadata.completed_at);
|
3266
|
-
|
3267
|
-
// Calculate duration in seconds
|
3268
|
-
const durationMs = endDate - startDate;
|
3269
|
-
const durationSec = Math.floor(durationMs / 1000);
|
3270
|
-
|
3271
|
-
// Format as minutes and seconds
|
3272
|
-
const minutes = Math.floor(durationSec / 60);
|
3273
|
-
const seconds = durationSec % 60;
|
3274
|
-
const formattedDuration = `${minutes}m ${seconds}s`;
|
3275
|
-
|
3276
|
-
// Add to results metadata
|
3277
|
-
const resultsMetadata = document.querySelector('.results-metadata');
|
3278
|
-
if (resultsMetadata) {
|
3279
|
-
// Check if duration element already exists
|
3280
|
-
let durationEl = resultsMetadata.querySelector('.metadata-item.duration');
|
3281
|
-
|
3282
|
-
if (!durationEl) {
|
3283
|
-
// Create new duration element
|
3284
|
-
durationEl = document.createElement('div');
|
3285
|
-
durationEl.className = 'metadata-item duration';
|
3286
|
-
|
3287
|
-
const labelEl = document.createElement('span');
|
3288
|
-
labelEl.className = 'metadata-label';
|
3289
|
-
labelEl.textContent = 'Duration:';
|
3290
|
-
|
3291
|
-
const valueEl = document.createElement('span');
|
3292
|
-
valueEl.className = 'metadata-value';
|
3293
|
-
valueEl.id = 'result-duration';
|
3294
|
-
|
3295
|
-
durationEl.appendChild(labelEl);
|
3296
|
-
durationEl.appendChild(valueEl);
|
3297
|
-
|
3298
|
-
// Insert after "Generated" metadata
|
3299
|
-
const generatedEl = resultsMetadata.querySelector('.metadata-item:nth-child(2)');
|
3300
|
-
if (generatedEl) {
|
3301
|
-
resultsMetadata.insertBefore(durationEl, generatedEl.nextSibling);
|
3302
|
-
} else {
|
3303
|
-
resultsMetadata.appendChild(durationEl);
|
3304
|
-
}
|
3305
|
-
}
|
3306
|
-
|
3307
|
-
// Update duration value
|
3308
|
-
const durationValueEl = resultsMetadata.querySelector('#result-duration');
|
3309
|
-
if (durationValueEl) {
|
3310
|
-
durationValueEl.textContent = formattedDuration;
|
3311
|
-
}
|
3312
|
-
}
|
3313
|
-
|
3314
|
-
// Also add to research details metadata
|
3315
|
-
const detailsMetadata = document.querySelector('.research-metadata');
|
3316
|
-
if (detailsMetadata) {
|
3317
|
-
// Check if duration element already exists
|
3318
|
-
let durationEl = null;
|
3319
|
-
|
3320
|
-
// Use querySelectorAll and iterate to find the element with "Duration" label
|
3321
|
-
const metadataItems = detailsMetadata.querySelectorAll('.metadata-item');
|
3322
|
-
for (let i = 0; i < metadataItems.length; i++) {
|
3323
|
-
const labelEl = metadataItems[i].querySelector('.metadata-label');
|
3324
|
-
if (labelEl && labelEl.textContent.includes('Duration')) {
|
3325
|
-
durationEl = metadataItems[i];
|
3326
|
-
break;
|
3327
|
-
}
|
3328
|
-
}
|
3329
|
-
|
3330
|
-
if (!durationEl) {
|
3331
|
-
// Create new duration element
|
3332
|
-
durationEl = document.createElement('div');
|
3333
|
-
durationEl.className = 'metadata-item';
|
3334
|
-
|
3335
|
-
const labelEl = document.createElement('span');
|
3336
|
-
labelEl.className = 'metadata-label';
|
3337
|
-
labelEl.textContent = 'Duration:';
|
3338
|
-
|
3339
|
-
const valueEl = document.createElement('span');
|
3340
|
-
valueEl.className = 'metadata-value';
|
3341
|
-
valueEl.id = 'detail-duration';
|
3342
|
-
|
3343
|
-
durationEl.appendChild(labelEl);
|
3344
|
-
durationEl.appendChild(valueEl);
|
3345
|
-
|
3346
|
-
// Insert after mode metadata
|
3347
|
-
const modeEl = detailsMetadata.querySelector('.metadata-item:nth-child(3)');
|
3348
|
-
if (modeEl) {
|
3349
|
-
detailsMetadata.insertBefore(durationEl, modeEl.nextSibling);
|
3350
|
-
} else {
|
3351
|
-
detailsMetadata.appendChild(durationEl);
|
3352
|
-
}
|
3353
|
-
}
|
3354
|
-
|
3355
|
-
// Update duration value
|
3356
|
-
const durationValueEl = durationEl.querySelector('.metadata-value');
|
3357
|
-
if (durationValueEl) {
|
3358
|
-
durationValueEl.textContent = formattedDuration;
|
3359
|
-
}
|
3360
|
-
}
|
3361
|
-
|
3362
|
-
console.log(`Research duration: ${formattedDuration}`);
|
3363
|
-
} catch (error) {
|
3364
|
-
console.error('Error calculating research duration:', error);
|
3365
|
-
}
|
3366
|
-
}
|
3367
|
-
|
3368
|
-
// Function to add a log entry to the console with reduced deduplication time window
|
3369
|
-
function addConsoleLog(message, type = 'info', metadata = null, forResearchId = null) {
|
3370
|
-
// Skip if identical to the last message to prevent duplication
|
3371
|
-
const currentTime = new Date().getTime();
|
3372
|
-
if (message === lastLogMessage && lastLogTimestamp && currentTime - lastLogTimestamp < 300) {
|
3373
|
-
// Skip duplicate message if it's within 300ms of the last one (reduced from 2000ms)
|
3374
|
-
return null;
|
3375
|
-
}
|
3376
|
-
|
3377
|
-
// Skip if we're viewing a specific research and this log is for a different research
|
3378
|
-
if (viewingResearchId && forResearchId && viewingResearchId !== forResearchId) {
|
3379
|
-
console.log(`Skipping log for research ${forResearchId} because viewing ${viewingResearchId}`);
|
3380
|
-
return null;
|
3381
|
-
}
|
3382
|
-
|
3383
|
-
// Update tracking variables
|
3384
|
-
lastLogMessage = message;
|
3385
|
-
lastLogTimestamp = currentTime;
|
3386
|
-
|
3387
|
-
// Use the direct log addition function
|
3388
|
-
return addConsoleLogDirect(message, type, metadata);
|
3389
|
-
}
|
3390
|
-
|
3391
|
-
// Function to update the log indicator count
|
3392
|
-
function updateLogIndicator() {
|
3393
|
-
const indicator = document.getElementById('log-indicator');
|
3394
|
-
if (indicator) {
|
3395
|
-
indicator.textContent = logCount;
|
3396
|
-
}
|
3397
|
-
}
|
3398
|
-
|
3399
|
-
// New function to update log panel visibility based on current page
|
3400
|
-
function updateLogPanelVisibility(pageId) {
|
3401
|
-
console.log('Updating log panel visibility for page:', pageId);
|
3402
|
-
|
3403
|
-
// Only show log panel on research progress and results pages
|
3404
|
-
if (pageId === 'research-progress' || pageId === 'research-results') {
|
3405
|
-
console.log('Showing log panel');
|
3406
|
-
// Let CSS handle the display
|
3407
|
-
} else {
|
3408
|
-
console.log('Hiding log panel');
|
3409
|
-
// Let CSS handle the display
|
3410
|
-
}
|
3411
|
-
}
|
3412
|
-
|
3413
|
-
// Function to update the navigation UI based on active page
|
3414
|
-
function updateNavigationUI(pageId) {
|
3415
|
-
// Update sidebar navigation
|
3416
|
-
const sidebarNavItems = document.querySelectorAll('.sidebar-nav li');
|
3417
|
-
sidebarNavItems.forEach(item => {
|
3418
|
-
if (item.getAttribute('data-page') === pageId) {
|
3419
|
-
item.classList.add('active');
|
3420
|
-
} else {
|
3421
|
-
item.classList.remove('active');
|
3422
|
-
}
|
3423
|
-
});
|
3424
|
-
|
3425
|
-
// Update mobile tab bar
|
3426
|
-
const mobileNavItems = document.querySelectorAll('.mobile-tab-bar li');
|
3427
|
-
mobileNavItems.forEach(item => {
|
3428
|
-
if (item.getAttribute('data-page') === pageId) {
|
3429
|
-
item.classList.add('active');
|
3430
|
-
} else {
|
3431
|
-
item.classList.remove('active');
|
3432
|
-
}
|
3433
|
-
});
|
3434
|
-
}
|
3435
|
-
|
3436
|
-
// Function to check research status before polling
|
3437
|
-
async function checkResearchStatusBeforePolling(researchId) {
|
3438
|
-
try {
|
3439
|
-
// Get the current status from the server
|
3440
|
-
const response = await fetch(getApiUrl(`/api/research/${researchId}`));
|
3441
|
-
const data = await response.json();
|
3442
|
-
|
3443
|
-
// If research is in_progress, start polling normally
|
3444
|
-
if (data.status === 'in_progress') {
|
3445
|
-
console.log(`Research ${researchId} is in progress, starting polling`);
|
3446
|
-
pollResearchStatus(researchId);
|
3447
|
-
window.connectToResearchSocket(researchId);
|
3448
|
-
return true;
|
3449
|
-
}
|
3450
|
-
// If terminated or complete, update UI but don't start polling
|
3451
|
-
else if (data.status === 'suspended' || data.status === 'failed') {
|
3452
|
-
console.log(`Research ${researchId} is ${data.status}, not starting polling`);
|
3453
|
-
updateTerminationUIState('suspended', `Research was ${data.status}`);
|
3454
|
-
return false;
|
3455
|
-
}
|
3456
|
-
// If completed, don't start polling
|
3457
|
-
else if (data.status === 'completed') {
|
3458
|
-
console.log(`Research ${researchId} is completed, not starting polling`);
|
3459
|
-
return false;
|
3460
|
-
}
|
3461
|
-
} catch (error) {
|
3462
|
-
console.error(`Error checking research status: ${error}`);
|
3463
|
-
return false;
|
3464
|
-
}
|
3465
|
-
|
3466
|
-
return false;
|
3467
|
-
}
|
3468
|
-
|
3469
|
-
// Function to reset research state before starting a new research
|
3470
|
-
function resetResearchState() {
|
3471
|
-
// Clean up any existing research resources
|
3472
|
-
window.cleanupResearchResources();
|
3473
|
-
|
3474
|
-
// Clear any previous polling intervals
|
3475
|
-
clearPollingInterval();
|
3476
|
-
|
3477
|
-
// Reset research flags
|
3478
|
-
isResearchInProgress = false;
|
3479
|
-
currentResearchId = null;
|
3480
|
-
window.currentResearchId = null;
|
3481
|
-
|
3482
|
-
// Clear console logs
|
3483
|
-
clearConsoleLogs();
|
3484
|
-
|
3485
|
-
// Reset any UI elements
|
3486
|
-
const errorMessage = document.getElementById('error-message');
|
3487
|
-
if (errorMessage) {
|
3488
|
-
errorMessage.style.display = 'none';
|
3489
|
-
errorMessage.textContent = '';
|
3490
|
-
}
|
3491
|
-
|
3492
|
-
// Hide the try again button if visible
|
3493
|
-
const tryAgainBtn = document.getElementById('try-again-btn');
|
3494
|
-
if (tryAgainBtn) {
|
3495
|
-
tryAgainBtn.style.display = 'none';
|
3496
|
-
}
|
3497
|
-
|
3498
|
-
// Reset progress bar
|
3499
|
-
const progressFill = document.getElementById('progress-fill');
|
3500
|
-
if (progressFill) {
|
3501
|
-
progressFill.style.width = '0%';
|
3502
|
-
}
|
3503
|
-
|
3504
|
-
// Reset progress percentage
|
3505
|
-
const progressPercentage = document.getElementById('progress-percentage');
|
3506
|
-
if (progressPercentage) {
|
3507
|
-
progressPercentage.textContent = '0%';
|
3508
|
-
}
|
3509
|
-
|
3510
|
-
// Reset progress status
|
3511
|
-
const progressStatus = document.getElementById('progress-status');
|
3512
|
-
if (progressStatus) {
|
3513
|
-
progressStatus.textContent = 'Initializing research process...';
|
3514
|
-
progressStatus.className = 'progress-status';
|
3515
|
-
}
|
3516
|
-
|
3517
|
-
// Reset the Start Research button
|
3518
|
-
resetStartResearchButton();
|
3519
|
-
|
3520
|
-
// Add initial log entry
|
3521
|
-
addConsoleLog('Preparing to start new research', 'info');
|
3522
|
-
}
|
3523
|
-
|
3524
|
-
// Function to load research logs into the console log panel
|
3525
|
-
function loadLogsForResearch(researchId) {
|
3526
|
-
console.log(`Loading logs for research ${researchId}`);
|
3527
|
-
|
3528
|
-
// Clear existing logs
|
3529
|
-
clearConsoleLogs();
|
3530
|
-
|
3531
|
-
// Set the current viewing research ID
|
3532
|
-
viewingResearchId = researchId;
|
3533
|
-
|
3534
|
-
// Fetch logs from the server for this research
|
3535
|
-
fetch(getApiUrl(`/api/research/${researchId}/logs`))
|
3536
|
-
.then(response => response.json())
|
3537
|
-
.then(data => {
|
3538
|
-
if (data.status === 'success' && Array.isArray(data.logs) && data.logs.length > 0) {
|
3539
|
-
console.log(`Loaded ${data.logs.length} logs for research ${researchId}`);
|
3540
|
-
|
3541
|
-
// Sort logs by timestamp if needed
|
3542
|
-
data.logs.sort((a, b) => {
|
3543
|
-
const timeA = new Date(a.time);
|
3544
|
-
const timeB = new Date(b.time);
|
3545
|
-
return timeA - timeB;
|
3546
|
-
});
|
3547
|
-
|
3548
|
-
// Add logs to the console
|
3549
|
-
data.logs.forEach(log => {
|
3550
|
-
let logType = 'info';
|
3551
|
-
|
3552
|
-
// Use the type directly from the database if available
|
3553
|
-
if (log.type) {
|
3554
|
-
logType = log.type;
|
3555
|
-
} else if (isMilestoneLog(log.message, log.metadata)) {
|
3556
|
-
logType = 'milestone';
|
3557
|
-
} else if (log.metadata && log.metadata.phase === 'error') {
|
3558
|
-
logType = 'error';
|
3559
|
-
}
|
3560
|
-
|
3561
|
-
// Add to console without triggering the duplicate detection
|
3562
|
-
// by directly creating the log entry
|
3563
|
-
addConsoleLogDirect(log.message, logType, log.metadata, log.time);
|
3564
|
-
});
|
3565
|
-
|
3566
|
-
// Apply initial filter (all)
|
3567
|
-
filterConsoleLogs('all');
|
3568
|
-
|
3569
|
-
// Make sure the "All" button is selected
|
3570
|
-
const allButton = document.querySelector('.filter-buttons .small-btn');
|
3571
|
-
if (allButton) {
|
3572
|
-
// Remove selected class from all buttons
|
3573
|
-
document.querySelectorAll('.filter-buttons .small-btn').forEach(btn => {
|
3574
|
-
btn.classList.remove('selected');
|
3575
|
-
});
|
3576
|
-
// Add selected class to the All button
|
3577
|
-
allButton.classList.add('selected');
|
3578
|
-
}
|
3579
|
-
} else {
|
3580
|
-
console.log(`No logs found for research ${researchId}`);
|
3581
|
-
// Add a message indicating no logs are available
|
3582
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3583
|
-
if (consoleContainer) {
|
3584
|
-
consoleContainer.innerHTML = '<div class="empty-log-message">No logs available for this research.</div>';
|
3585
|
-
}
|
3586
|
-
}
|
3587
|
-
})
|
3588
|
-
.catch(error => {
|
3589
|
-
console.error(`Error loading logs for research ${researchId}:`, error);
|
3590
|
-
// Show error message in console log
|
3591
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3592
|
-
if (consoleContainer) {
|
3593
|
-
consoleContainer.innerHTML = '<div class="empty-log-message error-message">Failed to load logs. Please try again.</div>';
|
3594
|
-
}
|
3595
|
-
});
|
3596
|
-
}
|
3597
|
-
|
3598
|
-
// New direct log addition function that bypasses duplicate detection
|
3599
|
-
function addConsoleLogDirect(message, type = 'info', metadata = null, timestamp = null) {
|
3600
|
-
// Get DOM elements
|
3601
|
-
const consoleContainer = document.getElementById('console-log-container');
|
3602
|
-
const template = document.getElementById('console-log-entry-template');
|
3603
|
-
|
3604
|
-
if (!consoleContainer || !template) {
|
3605
|
-
console.error('Console log container or template not found');
|
3606
|
-
return null;
|
3607
|
-
}
|
3608
|
-
|
3609
|
-
// Clear the empty message if it's the first log
|
3610
|
-
const emptyMessage = consoleContainer.querySelector('.empty-log-message');
|
3611
|
-
if (emptyMessage) {
|
3612
|
-
consoleContainer.removeChild(emptyMessage);
|
3613
|
-
}
|
3614
|
-
|
3615
|
-
// Create a new log entry from the template
|
3616
|
-
const clone = document.importNode(template.content, true);
|
3617
|
-
const logEntry = clone.querySelector('.console-log-entry');
|
3618
|
-
|
3619
|
-
// Make sure type is valid and standardized
|
3620
|
-
let validType = 'info';
|
3621
|
-
if (['info', 'milestone', 'error'].includes(type.toLowerCase())) {
|
3622
|
-
validType = type.toLowerCase();
|
3623
|
-
}
|
3624
|
-
|
3625
|
-
// Add appropriate class based on type
|
3626
|
-
logEntry.classList.add(`log-${validType}`);
|
3627
|
-
|
3628
|
-
// IMPORTANT: Store the log type directly as a data attribute
|
3629
|
-
logEntry.dataset.logType = validType;
|
3630
|
-
|
3631
|
-
// Set the timestamp
|
3632
|
-
const actualTimestamp = timestamp ? new Date(timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
|
3633
|
-
const timestampEl = logEntry.querySelector('.log-timestamp');
|
3634
|
-
if (timestampEl) timestampEl.textContent = actualTimestamp;
|
3635
|
-
|
3636
|
-
// Set the badge text based on type
|
3637
|
-
const badgeEl = logEntry.querySelector('.log-badge');
|
3638
|
-
if (badgeEl) {
|
3639
|
-
badgeEl.textContent = validType.toUpperCase();
|
3640
|
-
}
|
3641
|
-
|
3642
|
-
// Process message to add search engine info if it's a search query
|
3643
|
-
let displayMessage = message;
|
3644
|
-
|
3645
|
-
// Check if this is a search query message
|
3646
|
-
if (message && typeof message === 'string' && message.startsWith('Searching for:')) {
|
3647
|
-
// Determine search engine - add SearXNG by default since that's what we see in logs
|
3648
|
-
let searchEngine = 'SearXNG';
|
3649
|
-
|
3650
|
-
// Check metadata if available for any search engine info
|
3651
|
-
if (metadata) {
|
3652
|
-
if (metadata.engine) searchEngine = metadata.engine;
|
3653
|
-
else if (metadata.search_engine) searchEngine = metadata.search_engine;
|
3654
|
-
else if (metadata.source) searchEngine = metadata.source;
|
3655
|
-
else if (metadata.phase && metadata.phase.includes('searxng')) searchEngine = 'SearXNG';
|
3656
|
-
else if (metadata.phase && metadata.phase.includes('google')) searchEngine = 'Google';
|
3657
|
-
else if (metadata.phase && metadata.phase.includes('bing')) searchEngine = 'Bing';
|
3658
|
-
else if (metadata.phase && metadata.phase.includes('duckduckgo')) searchEngine = 'DuckDuckGo';
|
3659
|
-
}
|
3660
|
-
|
3661
|
-
// Append search engine info to message
|
3662
|
-
displayMessage = `${message} [Engine: ${searchEngine}]`;
|
3663
|
-
}
|
3664
|
-
|
3665
|
-
// Set the message
|
3666
|
-
const messageEl = logEntry.querySelector('.log-message');
|
3667
|
-
if (messageEl) messageEl.textContent = displayMessage;
|
3668
|
-
|
3669
|
-
// Add the log entry to the container
|
3670
|
-
consoleContainer.appendChild(logEntry);
|
3671
|
-
|
3672
|
-
// Store the log entry for filtering
|
3673
|
-
consoleLogEntries.push({
|
3674
|
-
element: logEntry,
|
3675
|
-
type: validType,
|
3676
|
-
message: displayMessage,
|
3677
|
-
timestamp: actualTimestamp,
|
3678
|
-
researchId: viewingResearchId
|
3679
|
-
});
|
3680
|
-
|
3681
|
-
// Update log count
|
3682
|
-
logCount++;
|
3683
|
-
updateLogIndicator();
|
3684
|
-
|
3685
|
-
// Use setTimeout to ensure DOM updates before scrolling
|
3686
|
-
setTimeout(() => {
|
3687
|
-
// Scroll to the bottom
|
3688
|
-
consoleContainer.scrollTop = consoleContainer.scrollHeight;
|
3689
|
-
}, 0);
|
3690
|
-
|
3691
|
-
return logEntry;
|
3692
|
-
}
|
3693
|
-
|
3694
|
-
// Add a global function to filter logs directly from HTML
|
3695
|
-
window.filterLogsByType = function(filterType) {
|
3696
|
-
console.log('Direct filterLogsByType called with:', filterType);
|
3697
|
-
if (filterType && typeof filterConsoleLogs === 'function') {
|
3698
|
-
// Update the button styling for the selected filter
|
3699
|
-
const filterButtons = document.querySelectorAll('.filter-buttons .small-btn');
|
3700
|
-
if (filterButtons.length > 0) {
|
3701
|
-
// Remove 'selected' class from all buttons
|
3702
|
-
filterButtons.forEach(btn => {
|
3703
|
-
btn.classList.remove('selected');
|
3704
|
-
});
|
3705
|
-
|
3706
|
-
// Add 'selected' class to the clicked button
|
3707
|
-
const selectedButton = Array.from(filterButtons).find(
|
3708
|
-
btn => btn.textContent.toLowerCase().includes(filterType) ||
|
3709
|
-
(filterType === 'all' && btn.textContent.toLowerCase() === 'all')
|
3710
|
-
);
|
3711
|
-
|
3712
|
-
if (selectedButton) {
|
3713
|
-
selectedButton.classList.add('selected');
|
3714
|
-
}
|
3715
|
-
}
|
3716
|
-
|
3717
|
-
// Apply the filter
|
3718
|
-
filterConsoleLogs(filterType);
|
3719
|
-
} else {
|
3720
|
-
console.error('Unable to filter logs - filterConsoleLogs function not available');
|
3721
|
-
}
|
3722
|
-
};
|
3723
|
-
|
3724
|
-
// Function to check if there are any logs available
|
3725
|
-
window.checkIfLogsAvailable = function() {
|
3726
|
-
const logEntries = document.querySelectorAll('.console-log-entry');
|
3727
|
-
if (logEntries.length === 0) {
|
3728
|
-
console.log('No logs available to filter');
|
3729
|
-
return false;
|
3730
|
-
}
|
3731
|
-
return true;
|
3732
|
-
};
|
3733
|
-
|
3734
|
-
// Add direct log panel toggle handler as backup
|
3735
|
-
const logPanelToggle = document.getElementById('log-panel-toggle');
|
3736
|
-
const logPanelContent = document.getElementById('log-panel-content');
|
3737
|
-
|
3738
|
-
if (logPanelToggle && logPanelContent) {
|
3739
|
-
console.log('Adding direct DOM event listener to log panel toggle');
|
3740
|
-
|
3741
|
-
logPanelToggle.addEventListener('click', function(event) {
|
3742
|
-
console.log('Log panel toggle clicked (direct handler)');
|
3743
|
-
event.preventDefault();
|
3744
|
-
event.stopPropagation();
|
3745
|
-
|
3746
|
-
// Toggle collapsed state
|
3747
|
-
logPanelContent.classList.toggle('collapsed');
|
3748
|
-
logPanelToggle.classList.toggle('collapsed');
|
3749
|
-
|
3750
|
-
// Update toggle icon
|
3751
|
-
const toggleIcon = logPanelToggle.querySelector('.toggle-icon');
|
3752
|
-
if (toggleIcon) {
|
3753
|
-
if (logPanelToggle.classList.contains('collapsed')) {
|
3754
|
-
toggleIcon.className = 'fas fa-chevron-right toggle-icon';
|
3755
|
-
} else {
|
3756
|
-
toggleIcon.className = 'fas fa-chevron-down toggle-icon';
|
3757
|
-
}
|
3758
|
-
}
|
3759
|
-
|
3760
|
-
return false;
|
3761
|
-
});
|
3762
|
-
}
|
3763
|
-
});
|