local-deep-research 0.1.26__py3-none-any.whl → 0.2.2__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 +154 -160
- 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 +87 -45
- local_deep_research/search_system.py +153 -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 +1583 -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 +212 -160
- 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.2.dist-info}/METADATA +177 -97
- local_deep_research-0.2.2.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.2.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.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1107 @@
|
|
1
|
+
/**
|
2
|
+
* Progress Component
|
3
|
+
* Manages research progress display and updates via Socket.IO
|
4
|
+
*/
|
5
|
+
(function() {
|
6
|
+
// Component state
|
7
|
+
let currentResearchId = null;
|
8
|
+
let pollInterval = null;
|
9
|
+
let isCompleted = false;
|
10
|
+
let socketErrorShown = false;
|
11
|
+
|
12
|
+
// DOM Elements
|
13
|
+
let progressBar = null;
|
14
|
+
let progressPercentage = null;
|
15
|
+
let statusText = null;
|
16
|
+
let currentTaskText = null;
|
17
|
+
let cancelButton = null;
|
18
|
+
let viewResultsButton = null;
|
19
|
+
|
20
|
+
// Socket instance
|
21
|
+
let socket = null;
|
22
|
+
let reconnectAttempts = 0;
|
23
|
+
const MAX_RECONNECT_ATTEMPTS = 5;
|
24
|
+
const RECONNECT_DELAY = 3000;
|
25
|
+
|
26
|
+
// Current research info
|
27
|
+
let researchCompleted = false;
|
28
|
+
let notificationsEnabled = false;
|
29
|
+
|
30
|
+
/**
|
31
|
+
* Initialize the progress component
|
32
|
+
*/
|
33
|
+
function initializeProgress() {
|
34
|
+
// Get research ID from URL or localStorage
|
35
|
+
currentResearchId = getResearchIdFromUrl() || localStorage.getItem('currentResearchId');
|
36
|
+
|
37
|
+
if (!currentResearchId) {
|
38
|
+
console.error('No research ID found');
|
39
|
+
if (window.ui) window.ui.showError('No active research found. Please start a new research.');
|
40
|
+
setTimeout(() => {
|
41
|
+
window.location.href = '/';
|
42
|
+
}, 3000);
|
43
|
+
return;
|
44
|
+
}
|
45
|
+
|
46
|
+
// Get DOM elements
|
47
|
+
progressBar = document.getElementById('progress-bar');
|
48
|
+
progressPercentage = document.getElementById('progress-percentage');
|
49
|
+
statusText = document.getElementById('status-text');
|
50
|
+
currentTaskText = document.getElementById('current-task');
|
51
|
+
cancelButton = document.getElementById('cancel-research-btn');
|
52
|
+
viewResultsButton = document.getElementById('view-results-btn');
|
53
|
+
|
54
|
+
// Log available elements for debugging
|
55
|
+
console.log('Progress DOM elements:', {
|
56
|
+
progressBar: !!progressBar,
|
57
|
+
progressPercentage: !!progressPercentage,
|
58
|
+
statusText: !!statusText,
|
59
|
+
currentTaskText: !!currentTaskText,
|
60
|
+
cancelButton: !!cancelButton,
|
61
|
+
viewResultsButton: !!viewResultsButton
|
62
|
+
});
|
63
|
+
|
64
|
+
// Check for required elements
|
65
|
+
const missingElements = [];
|
66
|
+
if (!progressBar) missingElements.push('progress-bar');
|
67
|
+
if (!statusText) missingElements.push('status-text');
|
68
|
+
if (!currentTaskText) missingElements.push('current-task');
|
69
|
+
|
70
|
+
if (missingElements.length > 0) {
|
71
|
+
console.error('Required DOM elements not found for progress component:', missingElements.join(', '));
|
72
|
+
// Try to create fallback elements if not found
|
73
|
+
createFallbackElements(missingElements);
|
74
|
+
}
|
75
|
+
|
76
|
+
// Set up event listeners
|
77
|
+
if (cancelButton) {
|
78
|
+
cancelButton.addEventListener('click', handleCancelResearch);
|
79
|
+
}
|
80
|
+
|
81
|
+
// Note: Log panel is now automatically initialized by logpanel.js
|
82
|
+
// No need to manually initialize it here
|
83
|
+
|
84
|
+
// Make sure navigation stays working even if Socket.IO fails
|
85
|
+
setupSafeNavigationHandling();
|
86
|
+
|
87
|
+
// Initialize socket connection if available
|
88
|
+
if (window.socket) {
|
89
|
+
initializeSocket();
|
90
|
+
} else {
|
91
|
+
console.warn('Socket service not available, falling back to polling');
|
92
|
+
// Set up polling as fallback
|
93
|
+
pollInterval = setInterval(checkProgress, 3000);
|
94
|
+
}
|
95
|
+
|
96
|
+
// Initial progress check
|
97
|
+
checkProgress();
|
98
|
+
|
99
|
+
console.log('Progress component initialized for research ID:', currentResearchId);
|
100
|
+
|
101
|
+
// Get notification preference
|
102
|
+
notificationsEnabled = localStorage.getItem('notificationsEnabled') === 'true';
|
103
|
+
|
104
|
+
// Get initial research status
|
105
|
+
getInitialStatus();
|
106
|
+
}
|
107
|
+
|
108
|
+
/**
|
109
|
+
* Set up safe navigation handling to prevent WebSocket errors from blocking navigation
|
110
|
+
*/
|
111
|
+
function setupSafeNavigationHandling() {
|
112
|
+
// Find all navigation links
|
113
|
+
const navLinks = document.querySelectorAll('a, .sidebar-nav li, .mobile-tab-bar li');
|
114
|
+
|
115
|
+
navLinks.forEach(link => {
|
116
|
+
// Don't override existing click handlers, add our handler
|
117
|
+
const originalClickHandler = link.onclick;
|
118
|
+
|
119
|
+
link.onclick = function(event) {
|
120
|
+
// If socket has errors, disconnect it before navigation
|
121
|
+
if (window.socket && typeof window.socket.isUsingPolling === 'function' && window.socket.isUsingPolling()) {
|
122
|
+
console.log('Navigation with polling fallback active, ensuring clean state');
|
123
|
+
try {
|
124
|
+
// Clean up any polling intervals
|
125
|
+
if (window.pollIntervals) {
|
126
|
+
Object.keys(window.pollIntervals).forEach(id => {
|
127
|
+
clearInterval(window.pollIntervals[id]);
|
128
|
+
});
|
129
|
+
}
|
130
|
+
} catch (e) {
|
131
|
+
console.error('Error cleaning up before navigation:', e);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
// Call the original click handler if it exists
|
136
|
+
if (typeof originalClickHandler === 'function') {
|
137
|
+
return originalClickHandler.call(this, event);
|
138
|
+
}
|
139
|
+
|
140
|
+
// Default behavior
|
141
|
+
return true;
|
142
|
+
};
|
143
|
+
});
|
144
|
+
}
|
145
|
+
|
146
|
+
/**
|
147
|
+
* Create fallback elements if they're missing
|
148
|
+
* @param {Array} missingElements - Array of missing element IDs
|
149
|
+
*/
|
150
|
+
function createFallbackElements(missingElements) {
|
151
|
+
const progressContainer = document.querySelector('.progress-container');
|
152
|
+
const statusContainer = document.querySelector('.status-container');
|
153
|
+
const taskContainer = document.querySelector('.task-container');
|
154
|
+
|
155
|
+
if (missingElements.includes('progress-bar') && progressContainer) {
|
156
|
+
console.log('Creating fallback progress bar');
|
157
|
+
const progressBarContainer = document.createElement('div');
|
158
|
+
progressBarContainer.className = 'progress-bar';
|
159
|
+
progressBarContainer.innerHTML = '<div id="progress-bar" class="progress-fill" style="width: 0%"></div>';
|
160
|
+
progressContainer.prepend(progressBarContainer);
|
161
|
+
progressBar = document.getElementById('progress-bar');
|
162
|
+
|
163
|
+
if (!progressPercentage) {
|
164
|
+
const percentEl = document.createElement('div');
|
165
|
+
percentEl.id = 'progress-percentage';
|
166
|
+
percentEl.className = 'progress-percentage';
|
167
|
+
percentEl.textContent = '0%';
|
168
|
+
progressContainer.appendChild(percentEl);
|
169
|
+
progressPercentage = percentEl;
|
170
|
+
}
|
171
|
+
}
|
172
|
+
|
173
|
+
if (missingElements.includes('status-text') && statusContainer) {
|
174
|
+
console.log('Creating fallback status text');
|
175
|
+
const statusEl = document.createElement('div');
|
176
|
+
statusEl.id = 'status-text';
|
177
|
+
statusEl.className = 'status-indicator';
|
178
|
+
statusEl.textContent = 'Initializing';
|
179
|
+
statusContainer.appendChild(statusEl);
|
180
|
+
statusText = statusEl;
|
181
|
+
}
|
182
|
+
|
183
|
+
if (missingElements.includes('current-task') && taskContainer) {
|
184
|
+
console.log('Creating fallback task text');
|
185
|
+
const taskEl = document.createElement('div');
|
186
|
+
taskEl.id = 'current-task';
|
187
|
+
taskEl.className = 'task-text';
|
188
|
+
taskEl.textContent = 'Starting research...';
|
189
|
+
taskContainer.appendChild(taskEl);
|
190
|
+
currentTaskText = taskEl;
|
191
|
+
}
|
192
|
+
}
|
193
|
+
|
194
|
+
/**
|
195
|
+
* Extract research ID from URL
|
196
|
+
* @returns {string|null} The research ID or null if not found
|
197
|
+
*/
|
198
|
+
function getResearchIdFromUrl() {
|
199
|
+
const pathParts = window.location.pathname.split('/');
|
200
|
+
const idIndex = pathParts.indexOf('progress') + 1;
|
201
|
+
|
202
|
+
if (idIndex > 0 && idIndex < pathParts.length) {
|
203
|
+
return pathParts[idIndex];
|
204
|
+
}
|
205
|
+
|
206
|
+
return null;
|
207
|
+
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Initialize Socket.IO connection and listeners
|
211
|
+
*/
|
212
|
+
function initializeSocket() {
|
213
|
+
try {
|
214
|
+
console.log('Initializing socket connection for research ID:', currentResearchId);
|
215
|
+
|
216
|
+
// Check if socket service is available
|
217
|
+
if (!window.socket) {
|
218
|
+
console.warn('Socket service not available, falling back to polling');
|
219
|
+
// Set up polling as fallback
|
220
|
+
fallbackToPolling();
|
221
|
+
return;
|
222
|
+
}
|
223
|
+
|
224
|
+
// Subscribe to research events
|
225
|
+
window.socket.subscribeToResearch(currentResearchId, handleProgressUpdate);
|
226
|
+
|
227
|
+
// Handle socket reconnection
|
228
|
+
window.socket.onReconnect(() => {
|
229
|
+
console.log('Socket reconnected, resubscribing to research events');
|
230
|
+
window.socket.subscribeToResearch(currentResearchId, handleProgressUpdate);
|
231
|
+
});
|
232
|
+
|
233
|
+
// Check socket status after a short delay to see if we're connected
|
234
|
+
setTimeout(() => {
|
235
|
+
if (window.socket.isUsingPolling && window.socket.isUsingPolling()) {
|
236
|
+
console.log('Socket using polling fallback');
|
237
|
+
if (!socketErrorShown) {
|
238
|
+
socketErrorShown = true;
|
239
|
+
// Add an info message to the console log if it exists
|
240
|
+
if (window.addConsoleLog) {
|
241
|
+
window.addConsoleLog('Using polling for updates due to WebSocket connection issues', 'info');
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
// Ensure we check for updates right away
|
246
|
+
checkProgress();
|
247
|
+
} else {
|
248
|
+
console.log('Socket using WebSockets successfully');
|
249
|
+
}
|
250
|
+
}, 2000);
|
251
|
+
} catch (error) {
|
252
|
+
console.error('Error initializing socket:', error);
|
253
|
+
// Fall back to polling
|
254
|
+
fallbackToPolling();
|
255
|
+
}
|
256
|
+
}
|
257
|
+
|
258
|
+
/**
|
259
|
+
* Fall back to polling for updates
|
260
|
+
*/
|
261
|
+
function fallbackToPolling() {
|
262
|
+
console.log('Setting up polling fallback for research updates');
|
263
|
+
|
264
|
+
if (!pollInterval) {
|
265
|
+
pollInterval = setInterval(checkProgress, 3000);
|
266
|
+
|
267
|
+
// Add a log entry about polling
|
268
|
+
if (window.addConsoleLog) {
|
269
|
+
window.addConsoleLog('Using polling for research updates instead of WebSockets', 'info');
|
270
|
+
}
|
271
|
+
}
|
272
|
+
}
|
273
|
+
|
274
|
+
/**
|
275
|
+
* Handle progress update from socket
|
276
|
+
* @param {Object} data - The progress data
|
277
|
+
*/
|
278
|
+
function handleProgressUpdate(data) {
|
279
|
+
console.log('Received progress update:', data);
|
280
|
+
|
281
|
+
if (!data) return;
|
282
|
+
|
283
|
+
// Process progress_log if available and add to logs
|
284
|
+
// NOTE: This is now handled by the logpanel component directly
|
285
|
+
// We'll just ensure the panel is visible and let it manage logs
|
286
|
+
if (data.progress_log && typeof data.progress_log === 'string') {
|
287
|
+
try {
|
288
|
+
// Validate that the progress_log is valid JSON
|
289
|
+
const progressLogsCheck = JSON.parse(data.progress_log);
|
290
|
+
if (Array.isArray(progressLogsCheck) && progressLogsCheck.length > 0) {
|
291
|
+
console.log(`Found ${progressLogsCheck.length} logs in progress update - forwarding to log panel`);
|
292
|
+
|
293
|
+
// Make the log panel visible if it exists
|
294
|
+
const logPanel = document.querySelector('.collapsible-log-panel');
|
295
|
+
if (logPanel && window.getComputedStyle(logPanel).display === 'none') {
|
296
|
+
logPanel.style.display = 'flex';
|
297
|
+
}
|
298
|
+
|
299
|
+
// The actual log processing is now handled by socket.js and logpanel.js
|
300
|
+
// We don't need to process logs here anymore
|
301
|
+
}
|
302
|
+
} catch (e) {
|
303
|
+
console.error('Error checking progress_log format:', e);
|
304
|
+
}
|
305
|
+
}
|
306
|
+
|
307
|
+
// Update progress UI
|
308
|
+
updateProgressUI(data);
|
309
|
+
|
310
|
+
// Check if research is completed
|
311
|
+
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
|
312
|
+
handleResearchCompletion(data);
|
313
|
+
}
|
314
|
+
|
315
|
+
// Update the current query text if available
|
316
|
+
const currentQueryEl = document.getElementById('current-query');
|
317
|
+
if (currentQueryEl && localStorage.getItem('currentQuery')) {
|
318
|
+
currentQueryEl.textContent = localStorage.getItem('currentQuery');
|
319
|
+
}
|
320
|
+
|
321
|
+
// Check for task message updates with better fallbacks
|
322
|
+
let taskUpdated = false;
|
323
|
+
|
324
|
+
if (data.task_message && data.task_message.trim() !== '') {
|
325
|
+
// Direct task message is highest priority
|
326
|
+
setCurrentTask(data.task_message);
|
327
|
+
taskUpdated = true;
|
328
|
+
} else if (data.current_task && data.current_task.trim() !== '') {
|
329
|
+
// Then try current_task field
|
330
|
+
setCurrentTask(data.current_task);
|
331
|
+
taskUpdated = true;
|
332
|
+
} else if (data.message && data.message.trim() !== '') {
|
333
|
+
// Finally fall back to general message
|
334
|
+
// But only if it's informative (not just a status update)
|
335
|
+
const msg = data.message.toLowerCase();
|
336
|
+
if (!msg.includes('in progress') && !msg.includes('status update')) {
|
337
|
+
setCurrentTask(data.message);
|
338
|
+
taskUpdated = true;
|
339
|
+
}
|
340
|
+
}
|
341
|
+
|
342
|
+
// If no task info was provided, leave the current task as is
|
343
|
+
// This prevents tasks from being overwritten by empty updates
|
344
|
+
}
|
345
|
+
|
346
|
+
/**
|
347
|
+
* Determine log level based on status
|
348
|
+
* @param {string} status - The research status
|
349
|
+
* @returns {string} Log level (info, milestone, error, etc)
|
350
|
+
*/
|
351
|
+
function determineLogLevel(status) {
|
352
|
+
if (!status) return 'info';
|
353
|
+
|
354
|
+
if (status === 'completed' || status === 'failed' || status === 'cancelled' || status === 'error') {
|
355
|
+
return 'milestone';
|
356
|
+
}
|
357
|
+
|
358
|
+
if (status === 'error' || status.includes('error')) {
|
359
|
+
return 'error';
|
360
|
+
}
|
361
|
+
|
362
|
+
return 'info';
|
363
|
+
}
|
364
|
+
|
365
|
+
/**
|
366
|
+
* Check research progress via API
|
367
|
+
*/
|
368
|
+
async function checkProgress() {
|
369
|
+
try {
|
370
|
+
if (!window.api || !window.api.getResearchStatus) {
|
371
|
+
console.error('API service not available');
|
372
|
+
return;
|
373
|
+
}
|
374
|
+
|
375
|
+
console.log('Checking research progress for ID:', currentResearchId);
|
376
|
+
const data = await window.api.getResearchStatus(currentResearchId);
|
377
|
+
|
378
|
+
if (data) {
|
379
|
+
console.log('Got research status update:', data);
|
380
|
+
|
381
|
+
// Update progress UI
|
382
|
+
updateProgressUI(data);
|
383
|
+
|
384
|
+
// Check if research is completed
|
385
|
+
if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
|
386
|
+
handleResearchCompletion(data);
|
387
|
+
} else {
|
388
|
+
// Set up polling for status updates as backup for socket
|
389
|
+
if (!pollInterval && (!window.socket || (window.socket.isUsingPolling && window.socket.isUsingPolling()))) {
|
390
|
+
console.log('Setting up polling interval for progress updates');
|
391
|
+
pollInterval = setInterval(checkProgress, 5000);
|
392
|
+
}
|
393
|
+
|
394
|
+
// Log a message every 5th poll to show activity
|
395
|
+
if (reconnectAttempts % 5 === 0) {
|
396
|
+
console.log('Still monitoring research progress...');
|
397
|
+
}
|
398
|
+
reconnectAttempts++; // Just using this as a counter for logging
|
399
|
+
}
|
400
|
+
} else {
|
401
|
+
console.warn('No data received from API');
|
402
|
+
}
|
403
|
+
} catch (error) {
|
404
|
+
console.error('Error checking research progress:', error);
|
405
|
+
if (statusText) {
|
406
|
+
statusText.textContent = 'Error checking research status';
|
407
|
+
}
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
411
|
+
/**
|
412
|
+
* Update progress bar
|
413
|
+
* @param {HTMLElement} progressBar - The progress bar element
|
414
|
+
* @param {number} progress - Progress percentage (0-100)
|
415
|
+
*/
|
416
|
+
function updateProgressBar(progressBar, progress) {
|
417
|
+
if (!progressBar) return;
|
418
|
+
|
419
|
+
// Ensure progress is between 0-100
|
420
|
+
const percentage = Math.max(0, Math.min(100, Math.floor(progress)));
|
421
|
+
|
422
|
+
// Update progress bar width with transition for smooth animation
|
423
|
+
progressBar.style.transition = 'width 0.3s ease-in-out';
|
424
|
+
progressBar.style.width = `${percentage}%`;
|
425
|
+
|
426
|
+
// Update percentage text if available
|
427
|
+
if (progressPercentage) {
|
428
|
+
progressPercentage.textContent = `${percentage}%`;
|
429
|
+
}
|
430
|
+
}
|
431
|
+
|
432
|
+
/**
|
433
|
+
* Update the progress UI with data
|
434
|
+
* @param {Object} data - The progress data
|
435
|
+
*/
|
436
|
+
function updateProgressUI(data) {
|
437
|
+
console.log('Updating progress UI with data:', data);
|
438
|
+
|
439
|
+
// Update progress bar
|
440
|
+
if (data.progress !== undefined && progressBar) {
|
441
|
+
updateProgressBar(progressBar, data.progress);
|
442
|
+
}
|
443
|
+
|
444
|
+
// Update status text with better formatting
|
445
|
+
if (data.status && statusText) {
|
446
|
+
let formattedStatus;
|
447
|
+
if (window.formatting && typeof window.formatting.formatStatus === 'function') {
|
448
|
+
formattedStatus = window.formatting.formatStatus(data.status);
|
449
|
+
} else {
|
450
|
+
// Manual status formatting for better display
|
451
|
+
switch (data.status) {
|
452
|
+
case 'in_progress':
|
453
|
+
// Don't show "In Progress" at all in status text
|
454
|
+
return; // Skip status update entirely for in_progress
|
455
|
+
case 'completed':
|
456
|
+
formattedStatus = 'Completed';
|
457
|
+
break;
|
458
|
+
case 'failed':
|
459
|
+
formattedStatus = 'Failed';
|
460
|
+
break;
|
461
|
+
case 'cancelled':
|
462
|
+
formattedStatus = 'Cancelled';
|
463
|
+
break;
|
464
|
+
default:
|
465
|
+
formattedStatus = data.status.charAt(0).toUpperCase() +
|
466
|
+
data.status.slice(1).replace(/_/g, ' ');
|
467
|
+
}
|
468
|
+
}
|
469
|
+
|
470
|
+
// Only update status text if we have a non-empty formatted status
|
471
|
+
if (formattedStatus && formattedStatus.trim() !== '') {
|
472
|
+
statusText.textContent = formattedStatus;
|
473
|
+
|
474
|
+
// Add status class for styling
|
475
|
+
document.querySelectorAll('.status-indicator').forEach(el => {
|
476
|
+
el.className = 'status-indicator';
|
477
|
+
el.classList.add(`status-${data.status}`);
|
478
|
+
});
|
479
|
+
}
|
480
|
+
}
|
481
|
+
|
482
|
+
// Extract current task from progress_log
|
483
|
+
if (currentTaskText) {
|
484
|
+
let taskMessage = null;
|
485
|
+
|
486
|
+
// Try to parse progress_log to get the latest task
|
487
|
+
if (data.progress_log && typeof data.progress_log === 'string') {
|
488
|
+
try {
|
489
|
+
const progressLogs = JSON.parse(data.progress_log);
|
490
|
+
if (Array.isArray(progressLogs) && progressLogs.length > 0) {
|
491
|
+
// Get the latest log entry with a non-null message
|
492
|
+
for (let i = progressLogs.length - 1; i >= 0; i--) {
|
493
|
+
if (progressLogs[i].message && progressLogs[i].message.trim() !== '') {
|
494
|
+
taskMessage = progressLogs[i].message;
|
495
|
+
break;
|
496
|
+
}
|
497
|
+
}
|
498
|
+
}
|
499
|
+
} catch (e) {
|
500
|
+
console.error('Error parsing progress_log for task message:', e);
|
501
|
+
}
|
502
|
+
}
|
503
|
+
|
504
|
+
// Check various fields that might contain the current task message
|
505
|
+
if (!taskMessage) {
|
506
|
+
if (data.current_task) {
|
507
|
+
taskMessage = data.current_task;
|
508
|
+
} else if (data.message) {
|
509
|
+
taskMessage = data.message;
|
510
|
+
} else if (data.task) {
|
511
|
+
taskMessage = data.task;
|
512
|
+
} else if (data.step) {
|
513
|
+
taskMessage = data.step;
|
514
|
+
} else if (data.phase) {
|
515
|
+
taskMessage = `Phase: ${data.phase}`;
|
516
|
+
} else if (data.log_entry && data.log_entry.message) {
|
517
|
+
taskMessage = data.log_entry.message;
|
518
|
+
}
|
519
|
+
}
|
520
|
+
|
521
|
+
// Update the task text if we found a message AND it's not just "In Progress"
|
522
|
+
if (taskMessage && taskMessage.trim() !== 'In Progress' && taskMessage.trim() !== 'in progress') {
|
523
|
+
console.log('Updating current task text to:', taskMessage);
|
524
|
+
currentTaskText.textContent = taskMessage;
|
525
|
+
// Remember this message to avoid overwriting with generic messages
|
526
|
+
currentTaskText.dataset.lastMessage = taskMessage;
|
527
|
+
}
|
528
|
+
|
529
|
+
// If no message but we have a status, generate a more descriptive message
|
530
|
+
// BUT ONLY if we don't already have a meaningful message displayed
|
531
|
+
if (!taskMessage && data.status && (!currentTaskText.dataset.lastMessage || currentTaskText.textContent === 'In Progress')) {
|
532
|
+
let statusMsg;
|
533
|
+
switch (data.status) {
|
534
|
+
case 'starting':
|
535
|
+
statusMsg = 'Starting research process...';
|
536
|
+
break;
|
537
|
+
case 'searching':
|
538
|
+
statusMsg = 'Searching for information...';
|
539
|
+
break;
|
540
|
+
case 'processing':
|
541
|
+
statusMsg = 'Processing search results...';
|
542
|
+
break;
|
543
|
+
case 'analyzing':
|
544
|
+
statusMsg = 'Analyzing gathered information...';
|
545
|
+
break;
|
546
|
+
case 'writing':
|
547
|
+
statusMsg = 'Writing research report...';
|
548
|
+
break;
|
549
|
+
case 'reviewing':
|
550
|
+
statusMsg = 'Reviewing and finalizing report...';
|
551
|
+
break;
|
552
|
+
case 'in_progress':
|
553
|
+
// Don't overwrite existing content with generic "In Progress" message
|
554
|
+
if (!currentTaskText.dataset.lastMessage || currentTaskText.textContent === '') {
|
555
|
+
statusMsg = 'Performing research...';
|
556
|
+
} else {
|
557
|
+
statusMsg = null; // Skip update
|
558
|
+
}
|
559
|
+
break;
|
560
|
+
default:
|
561
|
+
statusMsg = `${data.status.charAt(0).toUpperCase() + data.status.slice(1).replace('_', ' ')}...`;
|
562
|
+
}
|
563
|
+
|
564
|
+
// Only update if we have a new message
|
565
|
+
if (statusMsg) {
|
566
|
+
console.log('Using enhanced status-based message:', statusMsg);
|
567
|
+
currentTaskText.textContent = statusMsg;
|
568
|
+
// Don't remember generic messages
|
569
|
+
delete currentTaskText.dataset.lastMessage;
|
570
|
+
}
|
571
|
+
}
|
572
|
+
}
|
573
|
+
|
574
|
+
// Update page title with progress
|
575
|
+
if (data.progress !== undefined) {
|
576
|
+
document.title = `Research (${Math.floor(data.progress)}%) - Local Deep Research`;
|
577
|
+
}
|
578
|
+
|
579
|
+
// Update favicon based on status
|
580
|
+
if (window.ui && typeof window.ui.updateFavicon === 'function') {
|
581
|
+
window.ui.updateFavicon(data.status || 'in_progress');
|
582
|
+
}
|
583
|
+
|
584
|
+
// Show notification if enabled
|
585
|
+
if (data.status === 'completed' && localStorage.getItem('notificationsEnabled') === 'true') {
|
586
|
+
showNotification('Research Completed', 'Your research has been completed successfully.');
|
587
|
+
}
|
588
|
+
|
589
|
+
// Ensure log entry is added if message exists but no specific log_entry
|
590
|
+
if (data.message && window.addConsoleLog && !data.log_entry) {
|
591
|
+
console.log('Adding message to console log:', data.message);
|
592
|
+
window.addConsoleLog(data.message, determineLogLevel(data.status));
|
593
|
+
}
|
594
|
+
}
|
595
|
+
|
596
|
+
/**
|
597
|
+
* Handle research completion
|
598
|
+
* @param {Object} data - The completion data
|
599
|
+
*/
|
600
|
+
function handleResearchCompletion(data) {
|
601
|
+
if (isCompleted) return;
|
602
|
+
isCompleted = true;
|
603
|
+
|
604
|
+
// Clear polling interval
|
605
|
+
if (pollInterval) {
|
606
|
+
clearInterval(pollInterval);
|
607
|
+
pollInterval = null;
|
608
|
+
}
|
609
|
+
|
610
|
+
// Update UI for completion
|
611
|
+
if (data.status === 'completed') {
|
612
|
+
// Show view results button
|
613
|
+
if (viewResultsButton) {
|
614
|
+
viewResultsButton.style.display = 'inline-block';
|
615
|
+
viewResultsButton.href = `/research/results/${currentResearchId}`;
|
616
|
+
}
|
617
|
+
|
618
|
+
// Hide cancel button
|
619
|
+
if (cancelButton) {
|
620
|
+
cancelButton.style.display = 'none';
|
621
|
+
}
|
622
|
+
} else if (data.status === 'failed' || data.status === 'cancelled') {
|
623
|
+
// Show error message
|
624
|
+
if (window.ui) {
|
625
|
+
window.ui.showError(data.error || 'Research was unsuccessful');
|
626
|
+
} else {
|
627
|
+
console.error('Research failed:', data.error || 'Unknown error');
|
628
|
+
}
|
629
|
+
|
630
|
+
// Update button to go back to home
|
631
|
+
if (viewResultsButton) {
|
632
|
+
viewResultsButton.textContent = 'Start New Research';
|
633
|
+
viewResultsButton.href = '/';
|
634
|
+
viewResultsButton.style.display = 'inline-block';
|
635
|
+
}
|
636
|
+
|
637
|
+
// Hide cancel button
|
638
|
+
if (cancelButton) {
|
639
|
+
cancelButton.style.display = 'none';
|
640
|
+
}
|
641
|
+
}
|
642
|
+
}
|
643
|
+
|
644
|
+
/**
|
645
|
+
* Handle research cancellation
|
646
|
+
*/
|
647
|
+
async function handleCancelResearch() {
|
648
|
+
if (!confirm('Are you sure you want to cancel this research?')) {
|
649
|
+
return;
|
650
|
+
}
|
651
|
+
|
652
|
+
// Disable cancel button
|
653
|
+
if (cancelButton) {
|
654
|
+
cancelButton.disabled = true;
|
655
|
+
cancelButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Cancelling...';
|
656
|
+
}
|
657
|
+
|
658
|
+
try {
|
659
|
+
if (!window.api || !window.api.terminateResearch) {
|
660
|
+
throw new Error('API service not available');
|
661
|
+
}
|
662
|
+
|
663
|
+
await window.api.terminateResearch(currentResearchId);
|
664
|
+
|
665
|
+
// Update status manually (in case socket fails)
|
666
|
+
if (statusText) {
|
667
|
+
statusText.textContent = 'Cancelled';
|
668
|
+
document.querySelectorAll('.status-indicator').forEach(el => {
|
669
|
+
el.className = 'status-indicator status-cancelled';
|
670
|
+
});
|
671
|
+
}
|
672
|
+
|
673
|
+
// Show message
|
674
|
+
if (window.ui) {
|
675
|
+
window.ui.showMessage('Research has been cancelled.');
|
676
|
+
}
|
677
|
+
|
678
|
+
// Update cancel button
|
679
|
+
if (cancelButton) {
|
680
|
+
cancelButton.style.display = 'none';
|
681
|
+
}
|
682
|
+
|
683
|
+
// Show go home button
|
684
|
+
if (viewResultsButton) {
|
685
|
+
viewResultsButton.textContent = 'Start New Research';
|
686
|
+
viewResultsButton.href = '/';
|
687
|
+
viewResultsButton.style.display = 'inline-block';
|
688
|
+
}
|
689
|
+
|
690
|
+
} catch (error) {
|
691
|
+
console.error('Error cancelling research:', error);
|
692
|
+
|
693
|
+
// Re-enable cancel button
|
694
|
+
if (cancelButton) {
|
695
|
+
cancelButton.disabled = false;
|
696
|
+
cancelButton.innerHTML = '<i class="fas fa-stop-circle"></i> Cancel Research';
|
697
|
+
}
|
698
|
+
|
699
|
+
// Show error message
|
700
|
+
if (window.ui) {
|
701
|
+
window.ui.showError('Failed to cancel research. Please try again.');
|
702
|
+
}
|
703
|
+
}
|
704
|
+
}
|
705
|
+
|
706
|
+
/**
|
707
|
+
* Show a notification to the user
|
708
|
+
* @param {string} title - Notification title
|
709
|
+
* @param {string} message - Notification message
|
710
|
+
* @param {string} type - Notification type ('info', 'warning', 'error')
|
711
|
+
* @param {number} duration - Duration in ms to show in-app notification (0 to not auto-hide)
|
712
|
+
*/
|
713
|
+
function showNotification(title, message, type = 'info', duration = 5000) {
|
714
|
+
// First attempt browser notification if enabled
|
715
|
+
if ('Notification' in window) {
|
716
|
+
// Check if permission is already granted
|
717
|
+
if (Notification.permission === 'granted') {
|
718
|
+
try {
|
719
|
+
const notification = new Notification(title, {
|
720
|
+
body: message,
|
721
|
+
icon: type === 'error' ? '/research/static/img/error-icon.png' : '/research/static/img/favicon.png'
|
722
|
+
});
|
723
|
+
|
724
|
+
// Auto-close after 10 seconds
|
725
|
+
setTimeout(() => notification.close(), 10000);
|
726
|
+
} catch (e) {
|
727
|
+
console.warn('Browser notification failed, falling back to in-app notification', e);
|
728
|
+
}
|
729
|
+
}
|
730
|
+
// Otherwise, request permission (only if it's not been denied)
|
731
|
+
else if (Notification.permission !== 'denied') {
|
732
|
+
Notification.requestPermission().then(permission => {
|
733
|
+
if (permission === 'granted') {
|
734
|
+
new Notification(title, {
|
735
|
+
body: message,
|
736
|
+
icon: type === 'error' ? '/research/static/img/error-icon.png' : '/research/static/img/favicon.png'
|
737
|
+
});
|
738
|
+
}
|
739
|
+
});
|
740
|
+
}
|
741
|
+
}
|
742
|
+
|
743
|
+
// Also show in-app notification
|
744
|
+
try {
|
745
|
+
// Create or get notification container
|
746
|
+
let notificationContainer = document.getElementById('notification-container');
|
747
|
+
if (!notificationContainer) {
|
748
|
+
notificationContainer = document.createElement('div');
|
749
|
+
notificationContainer.id = 'notification-container';
|
750
|
+
notificationContainer.style.position = 'fixed';
|
751
|
+
notificationContainer.style.top = '20px';
|
752
|
+
notificationContainer.style.right = '20px';
|
753
|
+
notificationContainer.style.zIndex = '9999';
|
754
|
+
notificationContainer.style.width = '350px';
|
755
|
+
document.body.appendChild(notificationContainer);
|
756
|
+
}
|
757
|
+
|
758
|
+
// Create notification element
|
759
|
+
const notificationEl = document.createElement('div');
|
760
|
+
notificationEl.className = 'alert alert-dismissible fade show';
|
761
|
+
|
762
|
+
// Set type-specific styling
|
763
|
+
switch(type) {
|
764
|
+
case 'error':
|
765
|
+
notificationEl.classList.add('alert-danger');
|
766
|
+
break;
|
767
|
+
case 'warning':
|
768
|
+
notificationEl.classList.add('alert-warning');
|
769
|
+
break;
|
770
|
+
default:
|
771
|
+
notificationEl.classList.add('alert-info');
|
772
|
+
}
|
773
|
+
|
774
|
+
// Add title and message
|
775
|
+
notificationEl.innerHTML = `
|
776
|
+
<strong>${title}</strong>
|
777
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
778
|
+
<hr>
|
779
|
+
<p>${message}</p>
|
780
|
+
`;
|
781
|
+
|
782
|
+
// Add to container
|
783
|
+
notificationContainer.appendChild(notificationEl);
|
784
|
+
|
785
|
+
// Set up auto-dismiss if duration is provided
|
786
|
+
if (duration > 0) {
|
787
|
+
setTimeout(() => {
|
788
|
+
notificationEl.classList.remove('show');
|
789
|
+
setTimeout(() => {
|
790
|
+
notificationContainer.removeChild(notificationEl);
|
791
|
+
}, 300); // Wait for fade animation
|
792
|
+
}, duration);
|
793
|
+
}
|
794
|
+
|
795
|
+
// Set up click to dismiss
|
796
|
+
notificationEl.querySelector('.btn-close').addEventListener('click', () => {
|
797
|
+
notificationEl.classList.remove('show');
|
798
|
+
setTimeout(() => {
|
799
|
+
if (notificationContainer.contains(notificationEl)) {
|
800
|
+
notificationContainer.removeChild(notificationEl);
|
801
|
+
}
|
802
|
+
}, 300);
|
803
|
+
});
|
804
|
+
|
805
|
+
} catch (e) {
|
806
|
+
console.error('Failed to show in-app notification', e);
|
807
|
+
}
|
808
|
+
|
809
|
+
// Also log to console
|
810
|
+
const logMethod = type === 'error' ? console.error :
|
811
|
+
type === 'warning' ? console.warn : console.log;
|
812
|
+
logMethod(`${title}: ${message}`);
|
813
|
+
}
|
814
|
+
|
815
|
+
/**
|
816
|
+
* Get initial research status from API
|
817
|
+
*/
|
818
|
+
async function getInitialStatus() {
|
819
|
+
try {
|
820
|
+
const status = await window.api.getResearchStatus(currentResearchId);
|
821
|
+
|
822
|
+
// Process status
|
823
|
+
if (status) {
|
824
|
+
// If complete, show complete UI
|
825
|
+
if (status.status === 'completed') {
|
826
|
+
handleResearchComplete({ research_id: currentResearchId });
|
827
|
+
}
|
828
|
+
// If error, show error UI
|
829
|
+
else if (status.status === 'error') {
|
830
|
+
handleResearchError({
|
831
|
+
research_id: currentResearchId,
|
832
|
+
error: status.message || 'Unknown error'
|
833
|
+
});
|
834
|
+
}
|
835
|
+
// Otherwise update progress
|
836
|
+
else {
|
837
|
+
updateProgressUI(status);
|
838
|
+
}
|
839
|
+
}
|
840
|
+
} catch (error) {
|
841
|
+
console.error('Error getting initial status:', error);
|
842
|
+
setErrorState('Error loading research status. Please refresh the page to try again.');
|
843
|
+
}
|
844
|
+
}
|
845
|
+
|
846
|
+
/**
|
847
|
+
* Handle research complete event
|
848
|
+
* @param {Object} data - Complete event data
|
849
|
+
*/
|
850
|
+
function handleResearchComplete(data) {
|
851
|
+
console.log('Research complete received:', data);
|
852
|
+
|
853
|
+
if (data.research_id != currentResearchId) {
|
854
|
+
console.warn('Received complete event for different research ID');
|
855
|
+
return;
|
856
|
+
}
|
857
|
+
|
858
|
+
// Update UI
|
859
|
+
setProgressValue(100);
|
860
|
+
setStatus('completed');
|
861
|
+
setCurrentTask('Research completed successfully');
|
862
|
+
|
863
|
+
// Hide cancel button
|
864
|
+
if (cancelButton) {
|
865
|
+
cancelButton.style.display = 'none';
|
866
|
+
}
|
867
|
+
|
868
|
+
// Show results button
|
869
|
+
showResultsButton();
|
870
|
+
|
871
|
+
// Show notification if enabled
|
872
|
+
showNotification('Research Complete', 'Your research has been completed successfully.');
|
873
|
+
|
874
|
+
// Update favicon
|
875
|
+
updateFavicon(100);
|
876
|
+
|
877
|
+
// Set flag
|
878
|
+
researchCompleted = true;
|
879
|
+
}
|
880
|
+
|
881
|
+
/**
|
882
|
+
* Handle research error event
|
883
|
+
* @param {Object} data - Error event data
|
884
|
+
*/
|
885
|
+
function handleResearchError(data) {
|
886
|
+
console.error('Research error received:', data);
|
887
|
+
|
888
|
+
if (data.research_id != currentResearchId) {
|
889
|
+
console.warn('Received error event for different research ID');
|
890
|
+
return;
|
891
|
+
}
|
892
|
+
|
893
|
+
// Update UI to error state
|
894
|
+
setProgressValue(100);
|
895
|
+
setStatus('error');
|
896
|
+
setCurrentTask(`Error: ${data.error || 'Unknown error'}`);
|
897
|
+
|
898
|
+
// Add error class to progress bar
|
899
|
+
if (progressBar) {
|
900
|
+
progressBar.classList.remove('bg-primary', 'bg-success');
|
901
|
+
progressBar.classList.add('bg-danger');
|
902
|
+
}
|
903
|
+
|
904
|
+
// Hide cancel button
|
905
|
+
if (cancelButton) {
|
906
|
+
cancelButton.style.display = 'none';
|
907
|
+
}
|
908
|
+
|
909
|
+
// Show results button (might have partial results)
|
910
|
+
showResultsButton();
|
911
|
+
|
912
|
+
// Show notification if enabled
|
913
|
+
showNotification('Research Error', `There was an error with your research: ${data.error}`);
|
914
|
+
|
915
|
+
// Update favicon
|
916
|
+
updateFavicon(100, true);
|
917
|
+
}
|
918
|
+
|
919
|
+
/**
|
920
|
+
* Set progress bar value
|
921
|
+
* @param {number} value - Progress value (0-100)
|
922
|
+
*/
|
923
|
+
function setProgressValue(value) {
|
924
|
+
if (!progressBar) return;
|
925
|
+
|
926
|
+
// Ensure value is in range
|
927
|
+
value = Math.min(Math.max(value, 0), 100);
|
928
|
+
|
929
|
+
// Update progress bar
|
930
|
+
progressBar.style.width = `${value}%`;
|
931
|
+
progressBar.setAttribute('aria-valuenow', value);
|
932
|
+
|
933
|
+
// Update classes based on progress
|
934
|
+
if (value >= 100) {
|
935
|
+
progressBar.classList.remove('bg-primary');
|
936
|
+
progressBar.classList.add('bg-success');
|
937
|
+
} else {
|
938
|
+
progressBar.classList.remove('bg-success', 'bg-danger');
|
939
|
+
progressBar.classList.add('bg-primary');
|
940
|
+
}
|
941
|
+
}
|
942
|
+
|
943
|
+
/**
|
944
|
+
* Set status text
|
945
|
+
* @param {string} status - Status string
|
946
|
+
*/
|
947
|
+
function setStatus(status) {
|
948
|
+
if (!statusText) return;
|
949
|
+
|
950
|
+
let statusDisplay = 'Unknown';
|
951
|
+
|
952
|
+
// Map status to display text
|
953
|
+
switch (status) {
|
954
|
+
case 'not_started':
|
955
|
+
statusDisplay = 'Not Started';
|
956
|
+
break;
|
957
|
+
case 'in_progress':
|
958
|
+
statusDisplay = 'In Progress';
|
959
|
+
break;
|
960
|
+
case 'completed':
|
961
|
+
statusDisplay = 'Completed';
|
962
|
+
break;
|
963
|
+
case 'cancelled':
|
964
|
+
statusDisplay = 'Cancelled';
|
965
|
+
break;
|
966
|
+
case 'error':
|
967
|
+
statusDisplay = 'Error';
|
968
|
+
break;
|
969
|
+
default:
|
970
|
+
statusDisplay = status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown';
|
971
|
+
}
|
972
|
+
|
973
|
+
statusText.textContent = statusDisplay;
|
974
|
+
}
|
975
|
+
|
976
|
+
/**
|
977
|
+
* Set current task text
|
978
|
+
* @param {string} task - Current task description
|
979
|
+
*/
|
980
|
+
function setCurrentTask(task) {
|
981
|
+
if (!currentTaskText) return;
|
982
|
+
currentTaskText.textContent = task || 'No active task';
|
983
|
+
}
|
984
|
+
|
985
|
+
/**
|
986
|
+
* Set error state for the UI
|
987
|
+
* @param {string} message - Error message
|
988
|
+
*/
|
989
|
+
function setErrorState(message) {
|
990
|
+
// Update progress UI
|
991
|
+
setProgressValue(100);
|
992
|
+
setStatus('error');
|
993
|
+
setCurrentTask(`Error: ${message}`);
|
994
|
+
|
995
|
+
// Add error class to progress bar
|
996
|
+
if (progressBar) {
|
997
|
+
progressBar.classList.remove('bg-primary', 'bg-success');
|
998
|
+
progressBar.classList.add('bg-danger');
|
999
|
+
}
|
1000
|
+
|
1001
|
+
// Hide cancel button
|
1002
|
+
if (cancelButton) {
|
1003
|
+
cancelButton.style.display = 'none';
|
1004
|
+
}
|
1005
|
+
}
|
1006
|
+
|
1007
|
+
/**
|
1008
|
+
* Show results button
|
1009
|
+
*/
|
1010
|
+
function showResultsButton() {
|
1011
|
+
if (!viewResultsButton) return;
|
1012
|
+
|
1013
|
+
viewResultsButton.style.display = 'inline-block';
|
1014
|
+
viewResultsButton.disabled = false;
|
1015
|
+
}
|
1016
|
+
|
1017
|
+
/**
|
1018
|
+
* Update favicon with progress
|
1019
|
+
* @param {number} progress - Progress value (0-100)
|
1020
|
+
* @param {boolean} isError - Whether there is an error
|
1021
|
+
*/
|
1022
|
+
function updateFavicon(progress, isError = false) {
|
1023
|
+
try {
|
1024
|
+
// Find favicon link or create it if it doesn't exist
|
1025
|
+
let link = document.querySelector("link[rel='icon']") ||
|
1026
|
+
document.querySelector("link[rel='shortcut icon']");
|
1027
|
+
|
1028
|
+
if (!link) {
|
1029
|
+
// If no favicon link exists, don't try to create it
|
1030
|
+
// This avoids error spam in the console
|
1031
|
+
console.debug('Favicon link not found, skipping dynamic favicon update');
|
1032
|
+
return;
|
1033
|
+
}
|
1034
|
+
|
1035
|
+
// Create canvas for drawing the favicon
|
1036
|
+
const canvas = document.createElement('canvas');
|
1037
|
+
canvas.width = 32;
|
1038
|
+
canvas.height = 32;
|
1039
|
+
|
1040
|
+
const ctx = canvas.getContext('2d');
|
1041
|
+
|
1042
|
+
// Draw background
|
1043
|
+
ctx.fillStyle = '#343a40'; // Dark background
|
1044
|
+
ctx.beginPath();
|
1045
|
+
ctx.arc(16, 16, 16, 0, 2 * Math.PI);
|
1046
|
+
ctx.fill();
|
1047
|
+
|
1048
|
+
// Draw progress arc
|
1049
|
+
ctx.beginPath();
|
1050
|
+
ctx.moveTo(16, 16);
|
1051
|
+
ctx.arc(16, 16, 14, -0.5 * Math.PI, (-0.5 + 2 * progress / 100) * Math.PI);
|
1052
|
+
ctx.lineTo(16, 16);
|
1053
|
+
|
1054
|
+
// Color based on status
|
1055
|
+
if (isError) {
|
1056
|
+
ctx.fillStyle = '#dc3545'; // Danger red
|
1057
|
+
} else if (progress >= 100) {
|
1058
|
+
ctx.fillStyle = '#28a745'; // Success green
|
1059
|
+
} else {
|
1060
|
+
ctx.fillStyle = '#007bff'; // Primary blue
|
1061
|
+
}
|
1062
|
+
|
1063
|
+
ctx.fill();
|
1064
|
+
|
1065
|
+
// Draw center circle
|
1066
|
+
ctx.fillStyle = '#343a40';
|
1067
|
+
ctx.beginPath();
|
1068
|
+
ctx.arc(16, 16, 8, 0, 2 * Math.PI);
|
1069
|
+
ctx.fill();
|
1070
|
+
|
1071
|
+
// Draw letter R
|
1072
|
+
ctx.fillStyle = '#ffffff';
|
1073
|
+
ctx.font = 'bold 14px Arial';
|
1074
|
+
ctx.textAlign = 'center';
|
1075
|
+
ctx.textBaseline = 'middle';
|
1076
|
+
ctx.fillText('R', 16, 16);
|
1077
|
+
|
1078
|
+
// Update favicon
|
1079
|
+
link.href = canvas.toDataURL('image/png');
|
1080
|
+
|
1081
|
+
} catch (error) {
|
1082
|
+
console.error('Error updating favicon:', error);
|
1083
|
+
// Failure to update favicon is not critical, so we just log the error
|
1084
|
+
}
|
1085
|
+
}
|
1086
|
+
|
1087
|
+
// Initialize on page load
|
1088
|
+
document.addEventListener('DOMContentLoaded', initializeProgress);
|
1089
|
+
|
1090
|
+
// Expose components publicly for testing and debugging
|
1091
|
+
window.progressComponent = {
|
1092
|
+
checkProgress,
|
1093
|
+
handleCancelResearch
|
1094
|
+
};
|
1095
|
+
|
1096
|
+
// Add global error handler for WebSocket errors
|
1097
|
+
window.addEventListener('error', function(event) {
|
1098
|
+
if (event.message && event.message.includes('WebSocket') && event.message.includes('frame header')) {
|
1099
|
+
console.warn('Caught WebSocket frame header error, suppressing');
|
1100
|
+
event.preventDefault();
|
1101
|
+
return true; // Prevent the error from showing in console
|
1102
|
+
}
|
1103
|
+
});
|
1104
|
+
|
1105
|
+
// Expose notification function globally
|
1106
|
+
window.showNotification = showNotification;
|
1107
|
+
})();
|