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,1865 @@
|
|
1
|
+
/**
|
2
|
+
* Research Component
|
3
|
+
* Manages the research form and handles submissions
|
4
|
+
*/
|
5
|
+
(function() {
|
6
|
+
// DOM Elements
|
7
|
+
let form = null;
|
8
|
+
let queryInput = null;
|
9
|
+
let modeOptions = null;
|
10
|
+
let notificationToggle = null;
|
11
|
+
let startBtn = null;
|
12
|
+
let modelProviderSelect = null;
|
13
|
+
let customEndpointInput = null;
|
14
|
+
let endpointContainer = null;
|
15
|
+
let modelInput = null;
|
16
|
+
let modelDropdown = null;
|
17
|
+
let modelDropdownList = null;
|
18
|
+
let modelRefreshBtn = null;
|
19
|
+
let searchEngineInput = null;
|
20
|
+
let searchEngineDropdown = null;
|
21
|
+
let searchEngineDropdownList = null;
|
22
|
+
let searchEngineRefreshBtn = null;
|
23
|
+
let iterationsInput = null;
|
24
|
+
let questionsPerIterationInput = null;
|
25
|
+
let advancedToggle = null;
|
26
|
+
let advancedPanel = null;
|
27
|
+
|
28
|
+
// Cache keys
|
29
|
+
const CACHE_KEYS = {
|
30
|
+
MODELS: 'deepResearch.availableModels',
|
31
|
+
SEARCH_ENGINES: 'deepResearch.searchEngines',
|
32
|
+
CACHE_TIMESTAMP: 'deepResearch.cacheTimestamp'
|
33
|
+
};
|
34
|
+
|
35
|
+
// Cache expiration time (24 hours in milliseconds)
|
36
|
+
const CACHE_EXPIRATION = 24 * 60 * 60 * 1000;
|
37
|
+
|
38
|
+
// Flag to track if we're using fallback data
|
39
|
+
let usingFallbackModels = false;
|
40
|
+
let usingFallbackSearchEngines = false;
|
41
|
+
|
42
|
+
// State variables for dropdowns
|
43
|
+
let modelOptions = [];
|
44
|
+
let selectedModelValue = '';
|
45
|
+
let modelSelectedIndex = -1;
|
46
|
+
let searchEngineOptions = [];
|
47
|
+
let selectedSearchEngineValue = '';
|
48
|
+
let searchEngineSelectedIndex = -1;
|
49
|
+
|
50
|
+
// Track initialization to prevent unwanted saves during initial setup
|
51
|
+
let isInitializing = true;
|
52
|
+
|
53
|
+
// Model provider options from README
|
54
|
+
const MODEL_PROVIDERS = [
|
55
|
+
{ value: 'OLLAMA', label: 'Ollama (Local)' },
|
56
|
+
{ value: 'OPENAI', label: 'OpenAI (Cloud)' },
|
57
|
+
{ value: 'ANTHROPIC', label: 'Anthropic (Cloud)' },
|
58
|
+
{ value: 'OPENAI_ENDPOINT', label: 'Custom OpenAI Endpoint' },
|
59
|
+
{ value: 'VLLM', label: 'vLLM (Local)' },
|
60
|
+
{ value: 'LMSTUDIO', label: 'LM Studio (Local)' },
|
61
|
+
{ value: 'LLAMACPP', label: 'Llama.cpp (Local)' }
|
62
|
+
];
|
63
|
+
|
64
|
+
// Store available models by provider
|
65
|
+
let availableModels = {
|
66
|
+
OLLAMA: [],
|
67
|
+
OPENAI: [
|
68
|
+
{ value: 'gpt-4o', label: 'GPT-4o' },
|
69
|
+
{ value: 'gpt-4', label: 'GPT-4' },
|
70
|
+
{ value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo' }
|
71
|
+
],
|
72
|
+
ANTHROPIC: [
|
73
|
+
{ value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet' },
|
74
|
+
{ value: 'claude-3-opus-20240229', label: 'Claude 3 Opus' },
|
75
|
+
{ value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet' },
|
76
|
+
{ value: 'claude-3-haiku-20240307', label: 'Claude 3 Haiku' }
|
77
|
+
],
|
78
|
+
VLLM: [],
|
79
|
+
LMSTUDIO: [],
|
80
|
+
LLAMACPP: [],
|
81
|
+
OPENAI_ENDPOINT: []
|
82
|
+
};
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Initialize the research component
|
86
|
+
*/
|
87
|
+
function initializeResearch() {
|
88
|
+
// Set initializing flag
|
89
|
+
isInitializing = true;
|
90
|
+
|
91
|
+
// Get DOM elements
|
92
|
+
form = document.getElementById('research-form');
|
93
|
+
queryInput = document.getElementById('query');
|
94
|
+
modeOptions = document.querySelectorAll('.mode-option');
|
95
|
+
notificationToggle = document.getElementById('notification-toggle');
|
96
|
+
startBtn = document.getElementById('start-research-btn');
|
97
|
+
modelProviderSelect = document.getElementById('model_provider');
|
98
|
+
customEndpointInput = document.getElementById('custom_endpoint');
|
99
|
+
endpointContainer = document.getElementById('endpoint_container');
|
100
|
+
|
101
|
+
// Custom dropdown elements
|
102
|
+
modelInput = document.getElementById('model');
|
103
|
+
modelDropdown = document.getElementById('model-dropdown');
|
104
|
+
modelDropdownList = document.getElementById('model-dropdown-list');
|
105
|
+
modelRefreshBtn = document.getElementById('model-refresh');
|
106
|
+
|
107
|
+
searchEngineInput = document.getElementById('search_engine');
|
108
|
+
searchEngineDropdown = document.getElementById('search-engine-dropdown');
|
109
|
+
searchEngineDropdownList = document.getElementById('search-engine-dropdown-list');
|
110
|
+
searchEngineRefreshBtn = document.getElementById('search_engine-refresh');
|
111
|
+
|
112
|
+
// Other form elements
|
113
|
+
iterationsInput = document.getElementById('iterations');
|
114
|
+
questionsPerIterationInput = document.getElementById('questions_per_iteration');
|
115
|
+
advancedToggle = document.querySelector('.advanced-options-toggle');
|
116
|
+
advancedPanel = document.querySelector('.advanced-options-panel');
|
117
|
+
|
118
|
+
// First, try to load settings from localStorage for immediate display
|
119
|
+
const lastProvider = localStorage.getItem('lastUsedProvider');
|
120
|
+
const lastModel = localStorage.getItem('lastUsedModel');
|
121
|
+
const lastSearchEngine = localStorage.getItem('lastUsedSearchEngine');
|
122
|
+
|
123
|
+
console.log('Local storage values:', { provider: lastProvider, model: lastModel, searchEngine: lastSearchEngine });
|
124
|
+
|
125
|
+
// Apply local storage values if available
|
126
|
+
if (lastProvider && modelProviderSelect) {
|
127
|
+
console.log('Setting provider from localStorage:', lastProvider);
|
128
|
+
modelProviderSelect.value = lastProvider;
|
129
|
+
// Show/hide endpoint container as needed
|
130
|
+
if (endpointContainer) {
|
131
|
+
endpointContainer.style.display = lastProvider === 'OPENAI_ENDPOINT' ? 'block' : 'none';
|
132
|
+
}
|
133
|
+
}
|
134
|
+
|
135
|
+
// Initialize the UI first (immediate operations)
|
136
|
+
setupEventListeners();
|
137
|
+
populateModelProviders();
|
138
|
+
initializeDropdowns();
|
139
|
+
|
140
|
+
// Set initial state of the advanced options panel based on localStorage
|
141
|
+
const savedState = localStorage.getItem('advancedOptionsOpen') === 'true';
|
142
|
+
if (savedState && advancedPanel) {
|
143
|
+
advancedPanel.style.display = 'block';
|
144
|
+
advancedPanel.classList.add('expanded');
|
145
|
+
if (advancedToggle) {
|
146
|
+
advancedToggle.classList.add('open');
|
147
|
+
const icon = advancedToggle.querySelector('i');
|
148
|
+
if (icon) icon.className = 'fas fa-chevron-up';
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
152
|
+
// Then load data asynchronously (don't block UI)
|
153
|
+
Promise.all([
|
154
|
+
loadModelOptions(false),
|
155
|
+
loadSearchEngineOptions(false)
|
156
|
+
]).then(([modelData, searchEngineData]) => {
|
157
|
+
console.log('Data loaded successfully');
|
158
|
+
|
159
|
+
// After loading model data, update the UI with the loaded data
|
160
|
+
const currentProvider = modelProviderSelect ? modelProviderSelect.value : (lastProvider || 'OLLAMA');
|
161
|
+
updateModelOptionsForProvider(currentProvider, false);
|
162
|
+
|
163
|
+
// Update search engine options
|
164
|
+
if (searchEngineData && Array.isArray(searchEngineData)) {
|
165
|
+
searchEngineOptions = searchEngineData;
|
166
|
+
console.log('Loaded search engines:', searchEngineData.length);
|
167
|
+
|
168
|
+
// Force search engine dropdown to update with new data
|
169
|
+
if (searchEngineDropdownList && window.setupCustomDropdown) {
|
170
|
+
// Recreate the dropdown with the new data
|
171
|
+
const searchDropdownInstance = window.setupCustomDropdown(
|
172
|
+
searchEngineInput,
|
173
|
+
searchEngineDropdownList,
|
174
|
+
() => searchEngineOptions.length > 0 ? searchEngineOptions : [{ value: '', label: 'No search engines available' }],
|
175
|
+
(value, item) => {
|
176
|
+
console.log('Search engine selected:', value, item);
|
177
|
+
selectedSearchEngineValue = value;
|
178
|
+
|
179
|
+
// Update the input field
|
180
|
+
if (item) {
|
181
|
+
searchEngineInput.value = item.label;
|
182
|
+
} else {
|
183
|
+
searchEngineInput.value = value;
|
184
|
+
}
|
185
|
+
|
186
|
+
// Only save if not initializing
|
187
|
+
if (!isInitializing) {
|
188
|
+
saveSearchEngineSettings(value);
|
189
|
+
}
|
190
|
+
},
|
191
|
+
false,
|
192
|
+
'No search engines available.'
|
193
|
+
);
|
194
|
+
|
195
|
+
// If we have a last selected search engine, try to select it
|
196
|
+
if (lastSearchEngine) {
|
197
|
+
console.log('Trying to select last search engine:', lastSearchEngine);
|
198
|
+
// Find the matching engine
|
199
|
+
const matchingEngine = searchEngineOptions.find(engine =>
|
200
|
+
engine.value === lastSearchEngine || engine.id === lastSearchEngine);
|
201
|
+
|
202
|
+
if (matchingEngine) {
|
203
|
+
console.log('Found matching search engine:', matchingEngine);
|
204
|
+
searchEngineInput.value = matchingEngine.label;
|
205
|
+
selectedSearchEngineValue = matchingEngine.value;
|
206
|
+
|
207
|
+
// Update hidden input if exists
|
208
|
+
const hiddenInput = document.getElementById('search_engine_hidden');
|
209
|
+
if (hiddenInput) {
|
210
|
+
hiddenInput.value = matchingEngine.value;
|
211
|
+
}
|
212
|
+
}
|
213
|
+
}
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
// Finally, load settings after data is available
|
218
|
+
loadSettings();
|
219
|
+
}).catch(error => {
|
220
|
+
console.error('Failed to load options:', error);
|
221
|
+
|
222
|
+
// Still load settings even if data loading fails
|
223
|
+
loadSettings();
|
224
|
+
|
225
|
+
if (window.ui && window.ui.showAlert) {
|
226
|
+
window.ui.showAlert('Some options could not be loaded. Using defaults instead.', 'warning');
|
227
|
+
}
|
228
|
+
});
|
229
|
+
}
|
230
|
+
|
231
|
+
/**
|
232
|
+
* Initialize custom dropdowns for model and search engine
|
233
|
+
*/
|
234
|
+
function initializeDropdowns() {
|
235
|
+
// Check if the custom dropdown script is loaded
|
236
|
+
if (typeof window.setupCustomDropdown !== 'function') {
|
237
|
+
console.error('Custom dropdown script is not loaded');
|
238
|
+
// Display an error message
|
239
|
+
if (window.ui && window.ui.showAlert) {
|
240
|
+
window.ui.showAlert('Failed to initialize dropdowns. Please reload the page.', 'error');
|
241
|
+
}
|
242
|
+
return;
|
243
|
+
}
|
244
|
+
|
245
|
+
console.log('Initializing dropdowns with setupCustomDropdown');
|
246
|
+
|
247
|
+
// Set up model dropdown
|
248
|
+
if (modelInput && modelDropdownList) {
|
249
|
+
// Clear any existing dropdown setup
|
250
|
+
modelDropdownList.innerHTML = '';
|
251
|
+
|
252
|
+
const modelDropdownInstance = window.setupCustomDropdown(
|
253
|
+
modelInput,
|
254
|
+
modelDropdownList,
|
255
|
+
() => {
|
256
|
+
console.log('Getting model options from dropdown:', modelOptions);
|
257
|
+
return modelOptions.length > 0 ? modelOptions : [{ value: '', label: 'No models available' }];
|
258
|
+
},
|
259
|
+
(value, item) => {
|
260
|
+
console.log('Model selected:', value, item);
|
261
|
+
selectedModelValue = value;
|
262
|
+
|
263
|
+
// Update the input field with the selected model's label or value
|
264
|
+
if (item) {
|
265
|
+
modelInput.value = item.label;
|
266
|
+
} else {
|
267
|
+
modelInput.value = value;
|
268
|
+
}
|
269
|
+
|
270
|
+
const isCustomValue = !item;
|
271
|
+
showCustomModelWarning(isCustomValue);
|
272
|
+
|
273
|
+
// Save selected model to settings - only if not initializing
|
274
|
+
if (!isInitializing) {
|
275
|
+
saveModelSettings(value);
|
276
|
+
}
|
277
|
+
},
|
278
|
+
true, // Allow custom values
|
279
|
+
'No models available. Type to enter a custom model name.'
|
280
|
+
);
|
281
|
+
|
282
|
+
// Initialize model refresh button
|
283
|
+
if (modelRefreshBtn) {
|
284
|
+
modelRefreshBtn.addEventListener('click', function() {
|
285
|
+
const icon = modelRefreshBtn.querySelector('i');
|
286
|
+
|
287
|
+
// Add loading class to button
|
288
|
+
modelRefreshBtn.classList.add('loading');
|
289
|
+
|
290
|
+
// Force refresh of model options
|
291
|
+
loadModelOptions(true).then(() => {
|
292
|
+
// Remove loading class
|
293
|
+
modelRefreshBtn.classList.remove('loading');
|
294
|
+
|
295
|
+
// Ensure the current provider's models are loaded
|
296
|
+
const currentProvider = modelProviderSelect ? modelProviderSelect.value : 'OLLAMA';
|
297
|
+
updateModelOptionsForProvider(currentProvider, false);
|
298
|
+
|
299
|
+
// Force dropdown update
|
300
|
+
const event = new Event('click', { bubbles: true });
|
301
|
+
modelInput.dispatchEvent(event);
|
302
|
+
}).catch(error => {
|
303
|
+
console.error('Error refreshing models:', error);
|
304
|
+
|
305
|
+
// Remove loading class
|
306
|
+
modelRefreshBtn.classList.remove('loading');
|
307
|
+
|
308
|
+
if (window.ui && window.ui.showAlert) {
|
309
|
+
window.ui.showAlert('Failed to refresh models: ' + error.message, 'error');
|
310
|
+
}
|
311
|
+
});
|
312
|
+
});
|
313
|
+
}
|
314
|
+
}
|
315
|
+
|
316
|
+
// Set up search engine dropdown
|
317
|
+
if (searchEngineInput && searchEngineDropdownList) {
|
318
|
+
// Clear any existing dropdown setup
|
319
|
+
searchEngineDropdownList.innerHTML = '';
|
320
|
+
|
321
|
+
// Add loading state to search engine input
|
322
|
+
if (searchEngineInput.parentNode) {
|
323
|
+
searchEngineInput.parentNode.classList.add('loading');
|
324
|
+
}
|
325
|
+
|
326
|
+
const searchDropdownInstance = window.setupCustomDropdown(
|
327
|
+
searchEngineInput,
|
328
|
+
searchEngineDropdownList,
|
329
|
+
() => {
|
330
|
+
// Log available search engines for debugging
|
331
|
+
console.log('Getting search engine options:', searchEngineOptions);
|
332
|
+
return searchEngineOptions.length > 0 ? searchEngineOptions : [{ value: '', label: 'No search engines available' }];
|
333
|
+
},
|
334
|
+
(value, item) => {
|
335
|
+
console.log('Search engine selected:', value, item);
|
336
|
+
selectedSearchEngineValue = value;
|
337
|
+
|
338
|
+
// Update the input field with the selected search engine's label or value
|
339
|
+
if (item) {
|
340
|
+
searchEngineInput.value = item.label;
|
341
|
+
} else {
|
342
|
+
searchEngineInput.value = value;
|
343
|
+
}
|
344
|
+
|
345
|
+
// Save search engine selection to settings - only if not initializing
|
346
|
+
if (!isInitializing) {
|
347
|
+
saveSearchEngineSettings(value);
|
348
|
+
}
|
349
|
+
},
|
350
|
+
false, // Don't allow custom values
|
351
|
+
'No search engines available.'
|
352
|
+
);
|
353
|
+
|
354
|
+
// Initialize search engine refresh button
|
355
|
+
if (searchEngineRefreshBtn) {
|
356
|
+
searchEngineRefreshBtn.addEventListener('click', function() {
|
357
|
+
const icon = searchEngineRefreshBtn.querySelector('i');
|
358
|
+
|
359
|
+
// Add loading class to button
|
360
|
+
searchEngineRefreshBtn.classList.add('loading');
|
361
|
+
|
362
|
+
// Force refresh of search engine options
|
363
|
+
loadSearchEngineOptions(true).then(() => {
|
364
|
+
// Remove loading class
|
365
|
+
searchEngineRefreshBtn.classList.remove('loading');
|
366
|
+
|
367
|
+
// Force dropdown update
|
368
|
+
const event = new Event('click', { bubbles: true });
|
369
|
+
searchEngineInput.dispatchEvent(event);
|
370
|
+
}).catch(error => {
|
371
|
+
console.error('Error refreshing search engines:', error);
|
372
|
+
|
373
|
+
// Remove loading class
|
374
|
+
searchEngineRefreshBtn.classList.remove('loading');
|
375
|
+
|
376
|
+
if (window.ui && window.ui.showAlert) {
|
377
|
+
window.ui.showAlert('Failed to refresh search engines: ' + error.message, 'error');
|
378
|
+
}
|
379
|
+
});
|
380
|
+
});
|
381
|
+
}
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
/**
|
386
|
+
* Setup event listeners
|
387
|
+
*/
|
388
|
+
function setupEventListeners() {
|
389
|
+
if (!form || !startBtn) return;
|
390
|
+
|
391
|
+
// INITIALIZE ADVANCED OPTIONS FIRST - before any async operations
|
392
|
+
// Advanced options toggle - make immediately responsive
|
393
|
+
if (advancedToggle && advancedPanel) {
|
394
|
+
// Set initial state based on localStorage, relying only on CSS classes
|
395
|
+
const savedState = localStorage.getItem('advancedOptionsOpen') === 'true';
|
396
|
+
|
397
|
+
if (savedState) {
|
398
|
+
advancedToggle.classList.add('open', 'expanded');
|
399
|
+
advancedToggle.classList.toggle('expanded', true);
|
400
|
+
|
401
|
+
// Update icon immediately
|
402
|
+
const icon = advancedToggle.querySelector('i');
|
403
|
+
if (icon) {
|
404
|
+
icon.className = 'fas fa-chevron-up';
|
405
|
+
}
|
406
|
+
} else {
|
407
|
+
advancedToggle.classList.remove('open', 'expanded');
|
408
|
+
advancedToggle.classList.toggle('expanded', false);
|
409
|
+
// Ensure icon is correct
|
410
|
+
const icon = advancedToggle.querySelector('i');
|
411
|
+
if (icon) {
|
412
|
+
icon.className = 'fas fa-chevron-down';
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
// Add the click listener
|
417
|
+
advancedToggle.addEventListener('click', function() {
|
418
|
+
// Toggle classes for both approaches
|
419
|
+
const isOpen = advancedToggle.classList.toggle('open');
|
420
|
+
advancedToggle.classList.toggle('expanded', isOpen);
|
421
|
+
|
422
|
+
// Save state to localStorage
|
423
|
+
localStorage.setItem('advancedOptionsOpen', isOpen);
|
424
|
+
|
425
|
+
// Update icon
|
426
|
+
const icon = this.querySelector('i');
|
427
|
+
if (icon) {
|
428
|
+
icon.className = isOpen ? 'fas fa-chevron-up' : 'fas fa-chevron-down';
|
429
|
+
}
|
430
|
+
|
431
|
+
// Animate using CSS transitions based on the 'expanded' class
|
432
|
+
// No need for direct style manipulation or timeouts here if CSS is set up correctly
|
433
|
+
// Example: CSS might have transition on max-height and opacity
|
434
|
+
// If explicit display toggle is needed for transition logic:
|
435
|
+
if (isOpen) {
|
436
|
+
advancedPanel.style.display = 'block'; // Show before transition starts
|
437
|
+
// Force reflow to ensure display:block is applied before transition
|
438
|
+
advancedPanel.offsetHeight;
|
439
|
+
advancedPanel.classList.add('expanded'); // Add class to trigger transition
|
440
|
+
} else {
|
441
|
+
advancedPanel.classList.remove('expanded'); // Remove class to trigger transition
|
442
|
+
// Optionally hide with display:none after transition ends (using event listener)
|
443
|
+
advancedPanel.addEventListener('transitionend', () => {
|
444
|
+
if (!advancedPanel.classList.contains('expanded')) {
|
445
|
+
advancedPanel.style.display = 'none';
|
446
|
+
}
|
447
|
+
}, { once: true });
|
448
|
+
}
|
449
|
+
});
|
450
|
+
}
|
451
|
+
|
452
|
+
// Form submission
|
453
|
+
form.addEventListener('submit', handleResearchSubmit);
|
454
|
+
|
455
|
+
// Mode selection
|
456
|
+
modeOptions.forEach(mode => {
|
457
|
+
mode.addEventListener('click', function() {
|
458
|
+
modeOptions.forEach(m => m.classList.remove('active'));
|
459
|
+
this.classList.add('active');
|
460
|
+
});
|
461
|
+
});
|
462
|
+
|
463
|
+
// Model provider change
|
464
|
+
if (modelProviderSelect) {
|
465
|
+
modelProviderSelect.addEventListener('change', function() {
|
466
|
+
const provider = this.value;
|
467
|
+
console.log('Model provider changed to:', provider);
|
468
|
+
|
469
|
+
// Show custom endpoint input if OpenAI endpoint is selected
|
470
|
+
if (endpointContainer) {
|
471
|
+
endpointContainer.style.display = provider === 'OPENAI_ENDPOINT' ? 'block' : 'none';
|
472
|
+
}
|
473
|
+
|
474
|
+
// Update model options based on provider
|
475
|
+
updateModelOptionsForProvider(provider, true);
|
476
|
+
|
477
|
+
// Save provider change to database
|
478
|
+
saveProviderSetting(provider);
|
479
|
+
|
480
|
+
// Also update any settings form with the same provider
|
481
|
+
const settingsProviderInputs = document.querySelectorAll('input[data-key="llm.provider"]');
|
482
|
+
settingsProviderInputs.forEach(input => {
|
483
|
+
if (input !== modelProviderSelect) {
|
484
|
+
input.value = provider;
|
485
|
+
const hiddenInput = document.getElementById('llm.provider_hidden');
|
486
|
+
if (hiddenInput) {
|
487
|
+
hiddenInput.value = provider;
|
488
|
+
// Trigger change event
|
489
|
+
const event = new Event('change', { bubbles: true });
|
490
|
+
hiddenInput.dispatchEvent(event);
|
491
|
+
}
|
492
|
+
}
|
493
|
+
});
|
494
|
+
});
|
495
|
+
}
|
496
|
+
|
497
|
+
// Load options data from APIs
|
498
|
+
Promise.all([
|
499
|
+
loadModelOptions(false),
|
500
|
+
loadSearchEngineOptions(false)
|
501
|
+
]).then(() => {
|
502
|
+
// After loading data, initialize dropdowns
|
503
|
+
const currentProvider = modelProviderSelect ? modelProviderSelect.value : 'OLLAMA';
|
504
|
+
updateModelOptionsForProvider(currentProvider, false);
|
505
|
+
}).catch(error => {
|
506
|
+
console.error('Failed to load options:', error);
|
507
|
+
if (window.ui && window.ui.showAlert) {
|
508
|
+
window.ui.showAlert('Failed to load model options. Please check your connection and try again.', 'error');
|
509
|
+
}
|
510
|
+
});
|
511
|
+
}
|
512
|
+
|
513
|
+
/**
|
514
|
+
* Show or hide warning about custom model entries
|
515
|
+
* @param {boolean} show - Whether to show the warning
|
516
|
+
*/
|
517
|
+
function showCustomModelWarning(show) {
|
518
|
+
let warningEl = document.getElementById('custom-model-warning');
|
519
|
+
|
520
|
+
if (!warningEl && show) {
|
521
|
+
warningEl = document.createElement('div');
|
522
|
+
warningEl.id = 'custom-model-warning';
|
523
|
+
warningEl.className = 'model-warning';
|
524
|
+
warningEl.textContent = 'Custom model name entered. Make sure it exists in your provider.';
|
525
|
+
const parent = modelDropdown.closest('.form-group');
|
526
|
+
if (parent) {
|
527
|
+
parent.appendChild(warningEl);
|
528
|
+
}
|
529
|
+
}
|
530
|
+
|
531
|
+
if (warningEl) {
|
532
|
+
warningEl.style.display = show ? 'block' : 'none';
|
533
|
+
}
|
534
|
+
}
|
535
|
+
|
536
|
+
/**
|
537
|
+
* Populate model provider dropdown
|
538
|
+
*/
|
539
|
+
function populateModelProviders() {
|
540
|
+
if (!modelProviderSelect) return;
|
541
|
+
|
542
|
+
// Clear existing options
|
543
|
+
modelProviderSelect.innerHTML = '';
|
544
|
+
|
545
|
+
// Add options
|
546
|
+
MODEL_PROVIDERS.forEach(provider => {
|
547
|
+
const option = document.createElement('option');
|
548
|
+
option.value = provider.value;
|
549
|
+
option.textContent = provider.label;
|
550
|
+
modelProviderSelect.appendChild(option);
|
551
|
+
});
|
552
|
+
|
553
|
+
// Default to Ollama
|
554
|
+
modelProviderSelect.value = 'OLLAMA';
|
555
|
+
|
556
|
+
// Initial update of model options
|
557
|
+
updateModelOptionsForProvider('OLLAMA');
|
558
|
+
}
|
559
|
+
|
560
|
+
/**
|
561
|
+
* Update model options based on selected provider
|
562
|
+
* @param {string} provider - The selected provider
|
563
|
+
* @param {boolean} resetSelectedModel - Whether to reset the selected model
|
564
|
+
* @returns {Promise} - A promise that resolves when the model options are updated
|
565
|
+
*/
|
566
|
+
function updateModelOptionsForProvider(provider, resetSelectedModel = false) {
|
567
|
+
return new Promise((resolve) => {
|
568
|
+
// Convert provider to uppercase for consistent comparison
|
569
|
+
const providerUpper = provider.toUpperCase();
|
570
|
+
console.log('Filtering models for provider:', providerUpper, 'resetSelectedModel:', resetSelectedModel);
|
571
|
+
|
572
|
+
// If models aren't loaded yet, return early - they'll be loaded when available
|
573
|
+
const allModels = getCachedData(CACHE_KEYS.MODELS);
|
574
|
+
if (!allModels || !Array.isArray(allModels)) {
|
575
|
+
console.log('No model data loaded yet, will populate when available');
|
576
|
+
// Load models then try again
|
577
|
+
loadModelOptions(false).then(() => {
|
578
|
+
updateModelOptionsForProvider(provider, resetSelectedModel)
|
579
|
+
.then(resolve)
|
580
|
+
.catch(() => resolve([]));
|
581
|
+
}).catch(() => resolve([]));
|
582
|
+
return;
|
583
|
+
}
|
584
|
+
|
585
|
+
console.log('Filtering models for provider:', providerUpper, 'from', allModels.length, 'models');
|
586
|
+
|
587
|
+
// Filter models based on provider
|
588
|
+
let models = [];
|
589
|
+
|
590
|
+
// Special handling for OLLAMA provider - don't do strict filtering
|
591
|
+
if (providerUpper === 'OLLAMA') {
|
592
|
+
console.log('Searching for Ollama models...');
|
593
|
+
|
594
|
+
// First attempt: get models with provider explicitly set to OLLAMA
|
595
|
+
models = allModels.filter(model => {
|
596
|
+
if (!model || typeof model !== 'object') return false;
|
597
|
+
// Check if provider is set to OLLAMA
|
598
|
+
const modelProvider = (model.provider || '').toUpperCase();
|
599
|
+
return modelProvider === 'OLLAMA';
|
600
|
+
});
|
601
|
+
|
602
|
+
console.log(`Found ${models.length} models with provider="OLLAMA"`);
|
603
|
+
|
604
|
+
// If we didn't find enough models, look for models with Ollama in the name or id
|
605
|
+
if (models.length < 2) {
|
606
|
+
console.log('Searching more broadly for Ollama models');
|
607
|
+
models = allModels.filter(model => {
|
608
|
+
if (!model || typeof model !== 'object') return false;
|
609
|
+
|
610
|
+
// Skip provider options that are not actual models
|
611
|
+
if (model.value && !model.id && !model.name) return false;
|
612
|
+
|
613
|
+
// Check various properties that might indicate this is an Ollama model
|
614
|
+
const modelProvider = (model.provider || '').toUpperCase();
|
615
|
+
const modelName = (model.name || model.label || '').toLowerCase();
|
616
|
+
const modelId = (model.id || model.value || '').toLowerCase();
|
617
|
+
|
618
|
+
// Include if: provider is OLLAMA OR name contains "ollama" OR id is one of common Ollama models
|
619
|
+
return modelProvider === 'OLLAMA' ||
|
620
|
+
modelName.includes('ollama') ||
|
621
|
+
modelId.includes('llama') ||
|
622
|
+
modelId.includes('mistral') ||
|
623
|
+
modelId.includes('gemma');
|
624
|
+
});
|
625
|
+
|
626
|
+
console.log(`Broader search found ${models.length} possible Ollama models`);
|
627
|
+
}
|
628
|
+
|
629
|
+
// If we still don't have enough models, look for any that might be LLMs
|
630
|
+
if (models.length < 2) {
|
631
|
+
console.log('Still few models found, trying any model with likely LLM names');
|
632
|
+
// Add models that look like they could be LLMs (if they're not already included)
|
633
|
+
const moreModels = allModels.filter(model => {
|
634
|
+
if (!model || typeof model !== 'object') return false;
|
635
|
+
if (models.some(m => m.id === model.id || m.value === model.value)) return false; // Skip if already included
|
636
|
+
|
637
|
+
const modelId = (model.id || model.value || '').toLowerCase();
|
638
|
+
const modelName = (model.name || model.label || '').toLowerCase();
|
639
|
+
|
640
|
+
// Include common LLM name patterns
|
641
|
+
return modelId.includes('gpt') ||
|
642
|
+
modelId.includes('llama') ||
|
643
|
+
modelId.includes('mistral') ||
|
644
|
+
modelId.includes('gemma') ||
|
645
|
+
modelId.includes('claude') ||
|
646
|
+
modelName.includes('llm') ||
|
647
|
+
modelName.includes('model');
|
648
|
+
});
|
649
|
+
|
650
|
+
console.log(`Found ${moreModels.length} additional possible LLM models`);
|
651
|
+
models = [...models, ...moreModels];
|
652
|
+
}
|
653
|
+
|
654
|
+
// If we STILL have few or no models, use our fallbacks
|
655
|
+
if (models.length < 2) {
|
656
|
+
console.log('No Ollama models found, using fallbacks');
|
657
|
+
models = [
|
658
|
+
{ id: 'llama3', name: 'Llama 3 (Ollama)', provider: 'OLLAMA' },
|
659
|
+
{ id: 'mistral', name: 'Mistral (Ollama)', provider: 'OLLAMA' },
|
660
|
+
{ id: 'gemma:latest', name: 'Gemma (Ollama)', provider: 'OLLAMA' }
|
661
|
+
];
|
662
|
+
usingFallbackModels = true;
|
663
|
+
}
|
664
|
+
} else if (providerUpper === 'ANTHROPIC') {
|
665
|
+
// Filter Anthropic models
|
666
|
+
models = allModels.filter(model => {
|
667
|
+
if (!model || typeof model !== 'object') return false;
|
668
|
+
|
669
|
+
// Skip provider options
|
670
|
+
if (model.value && !model.id && !model.name) return false;
|
671
|
+
|
672
|
+
// Check provider, name, or ID for Anthropic indicators
|
673
|
+
const modelProvider = (model.provider || '').toUpperCase();
|
674
|
+
const modelName = (model.name || model.label || '').toLowerCase();
|
675
|
+
const modelId = (model.id || model.value || '').toLowerCase();
|
676
|
+
|
677
|
+
return modelProvider === 'ANTHROPIC' ||
|
678
|
+
modelName.includes('claude') ||
|
679
|
+
modelId.includes('claude');
|
680
|
+
});
|
681
|
+
|
682
|
+
// Add fallbacks if necessary
|
683
|
+
if (models.length === 0) {
|
684
|
+
console.log('No Anthropic models found, using fallbacks');
|
685
|
+
models = [
|
686
|
+
{ id: 'claude-3-5-sonnet-latest', name: 'Claude 3.5 Sonnet (Anthropic)', provider: 'ANTHROPIC' },
|
687
|
+
{ id: 'claude-3-opus-20240229', name: 'Claude 3 Opus (Anthropic)', provider: 'ANTHROPIC' },
|
688
|
+
{ id: 'claude-3-sonnet-20240229', name: 'Claude 3 Sonnet (Anthropic)', provider: 'ANTHROPIC' }
|
689
|
+
];
|
690
|
+
usingFallbackModels = true;
|
691
|
+
}
|
692
|
+
} else if (providerUpper === 'OPENAI') {
|
693
|
+
// Filter OpenAI models
|
694
|
+
models = allModels.filter(model => {
|
695
|
+
if (!model || typeof model !== 'object') return false;
|
696
|
+
|
697
|
+
// Skip provider options
|
698
|
+
if (model.value && !model.id && !model.name) return false;
|
699
|
+
|
700
|
+
// Check provider, name, or ID for OpenAI indicators
|
701
|
+
const modelProvider = (model.provider || '').toUpperCase();
|
702
|
+
const modelName = (model.name || model.label || '').toLowerCase();
|
703
|
+
const modelId = (model.id || model.value || '').toLowerCase();
|
704
|
+
|
705
|
+
return modelProvider === 'OPENAI' ||
|
706
|
+
modelName.includes('gpt') ||
|
707
|
+
modelId.includes('gpt');
|
708
|
+
});
|
709
|
+
|
710
|
+
// Add fallbacks if necessary
|
711
|
+
if (models.length === 0) {
|
712
|
+
console.log('No OpenAI models found, using fallbacks');
|
713
|
+
models = [
|
714
|
+
{ id: 'gpt-4o', name: 'GPT-4o (OpenAI)', provider: 'OPENAI' },
|
715
|
+
{ id: 'gpt-4', name: 'GPT-4 (OpenAI)', provider: 'OPENAI' },
|
716
|
+
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo (OpenAI)', provider: 'OPENAI' }
|
717
|
+
];
|
718
|
+
usingFallbackModels = true;
|
719
|
+
}
|
720
|
+
} else if (providerUpper === 'OPENAI_ENDPOINT') {
|
721
|
+
// For custom endpoints, show a mix of models as examples
|
722
|
+
models = allModels.filter(model => {
|
723
|
+
if (!model || typeof model !== 'object') return false;
|
724
|
+
|
725
|
+
// Skip provider options
|
726
|
+
if (model.value && !model.id && !model.name) return false;
|
727
|
+
|
728
|
+
// Include OpenAI and Anthropic models as examples
|
729
|
+
const modelProvider = (model.provider || '').toUpperCase();
|
730
|
+
return modelProvider === 'OPENAI' || modelProvider === 'ANTHROPIC';
|
731
|
+
});
|
732
|
+
|
733
|
+
// Add fallbacks if necessary
|
734
|
+
if (models.length === 0) {
|
735
|
+
console.log('No models found for custom endpoint, using fallbacks');
|
736
|
+
models = [
|
737
|
+
{ id: 'gpt-4o', name: 'GPT-4o (via Custom Endpoint)', provider: 'OPENAI_ENDPOINT' },
|
738
|
+
{ id: 'claude-3-5-sonnet-latest', name: 'Claude 3.5 Sonnet (via Custom Endpoint)', provider: 'OPENAI_ENDPOINT' }
|
739
|
+
];
|
740
|
+
usingFallbackModels = true;
|
741
|
+
}
|
742
|
+
} else {
|
743
|
+
// Standard filtering for other providers
|
744
|
+
models = allModels.filter(model => {
|
745
|
+
if (!model || typeof model !== 'object') return false;
|
746
|
+
|
747
|
+
// Skip provider options (they have value but no id)
|
748
|
+
if (model.value && !model.id && !model.name) return false;
|
749
|
+
|
750
|
+
const modelProvider = model.provider ? model.provider.toUpperCase() : '';
|
751
|
+
return modelProvider === providerUpper;
|
752
|
+
});
|
753
|
+
|
754
|
+
// If we found no models for this provider, add fallbacks
|
755
|
+
if (models.length === 0) {
|
756
|
+
console.log(`No models found for provider ${provider}, using generic fallbacks`);
|
757
|
+
models = [
|
758
|
+
{ id: 'model1', name: `Model 1 (${providerUpper})`, provider: providerUpper },
|
759
|
+
{ id: 'model2', name: `Model 2 (${providerUpper})`, provider: providerUpper }
|
760
|
+
];
|
761
|
+
usingFallbackModels = true;
|
762
|
+
}
|
763
|
+
}
|
764
|
+
|
765
|
+
console.log('Filtered models for provider', provider, ':', models.length, 'models');
|
766
|
+
|
767
|
+
// Format models for dropdown
|
768
|
+
modelOptions = models.map(model => {
|
769
|
+
const label = model.name || model.label || model.id || model.value || 'Unknown model';
|
770
|
+
const value = model.id || model.value || '';
|
771
|
+
return { value, label, provider: model.provider };
|
772
|
+
});
|
773
|
+
|
774
|
+
console.log(`Updated model options for provider ${provider}: ${modelOptions.length} models`);
|
775
|
+
|
776
|
+
// Check for stored last model before deciding what to select
|
777
|
+
let lastSelectedModel = localStorage.getItem('lastUsedModel');
|
778
|
+
|
779
|
+
// Also check the database setting
|
780
|
+
fetch('/research/settings/api/llm.model', {
|
781
|
+
method: 'GET',
|
782
|
+
headers: {
|
783
|
+
'Content-Type': 'application/json'
|
784
|
+
}
|
785
|
+
})
|
786
|
+
.then(response => response.json())
|
787
|
+
.then(data => {
|
788
|
+
if (data && data.setting && data.setting.value) {
|
789
|
+
const dbModelValue = data.setting.value;
|
790
|
+
console.log('Found model in database:', dbModelValue);
|
791
|
+
|
792
|
+
// Use the database value if it exists and matches the current provider
|
793
|
+
const dbModelMatch = modelOptions.find(model => model.value === dbModelValue);
|
794
|
+
|
795
|
+
if (dbModelMatch) {
|
796
|
+
console.log('Found matching model in filtered options:', dbModelMatch);
|
797
|
+
lastSelectedModel = dbModelValue;
|
798
|
+
}
|
799
|
+
}
|
800
|
+
|
801
|
+
// Continue with model selection
|
802
|
+
selectModelBasedOnProvider(resetSelectedModel, lastSelectedModel);
|
803
|
+
resolve(modelOptions);
|
804
|
+
})
|
805
|
+
.catch(error => {
|
806
|
+
console.error('Error fetching model from database:', error);
|
807
|
+
// Continue with model selection using localStorage
|
808
|
+
selectModelBasedOnProvider(resetSelectedModel, lastSelectedModel);
|
809
|
+
resolve(modelOptions);
|
810
|
+
});
|
811
|
+
});
|
812
|
+
}
|
813
|
+
|
814
|
+
/**
|
815
|
+
* Select a model based on the current provider and saved preferences
|
816
|
+
* @param {boolean} resetSelectedModel - Whether to reset the selected model
|
817
|
+
* @param {string} lastSelectedModel - The last selected model from localStorage or database
|
818
|
+
*/
|
819
|
+
function selectModelBasedOnProvider(resetSelectedModel, lastSelectedModel) {
|
820
|
+
if (resetSelectedModel) {
|
821
|
+
if (modelInput) {
|
822
|
+
// Try to select last used model first if it's available
|
823
|
+
if (lastSelectedModel) {
|
824
|
+
const matchingModel = modelOptions.find(model => model.value === lastSelectedModel);
|
825
|
+
if (matchingModel) {
|
826
|
+
modelInput.value = matchingModel.label;
|
827
|
+
selectedModelValue = matchingModel.value;
|
828
|
+
console.log('Selected previously used model:', selectedModelValue);
|
829
|
+
|
830
|
+
// Update any hidden input if it exists
|
831
|
+
const hiddenInput = document.getElementById('model_hidden');
|
832
|
+
if (hiddenInput) {
|
833
|
+
hiddenInput.value = selectedModelValue;
|
834
|
+
}
|
835
|
+
|
836
|
+
// Only save to settings if we're not initializing
|
837
|
+
if (!isInitializing) {
|
838
|
+
saveModelSettings(selectedModelValue);
|
839
|
+
}
|
840
|
+
return;
|
841
|
+
}
|
842
|
+
}
|
843
|
+
|
844
|
+
// If no matching model, clear and select first available
|
845
|
+
modelInput.value = '';
|
846
|
+
selectedModelValue = '';
|
847
|
+
}
|
848
|
+
}
|
849
|
+
|
850
|
+
// Select first available model if no selection and models are available
|
851
|
+
if ((!selectedModelValue || selectedModelValue === '') && modelOptions.length > 0 && modelInput) {
|
852
|
+
// Try to find last used model first
|
853
|
+
if (lastSelectedModel) {
|
854
|
+
const matchingModel = modelOptions.find(model => model.value === lastSelectedModel);
|
855
|
+
if (matchingModel) {
|
856
|
+
modelInput.value = matchingModel.label;
|
857
|
+
selectedModelValue = matchingModel.value;
|
858
|
+
console.log('Selected previously used model:', selectedModelValue);
|
859
|
+
|
860
|
+
// Update any hidden input if it exists
|
861
|
+
const hiddenInput = document.getElementById('model_hidden');
|
862
|
+
if (hiddenInput) {
|
863
|
+
hiddenInput.value = selectedModelValue;
|
864
|
+
}
|
865
|
+
|
866
|
+
// Only save to settings if we're not initializing
|
867
|
+
if (!isInitializing) {
|
868
|
+
saveModelSettings(selectedModelValue);
|
869
|
+
}
|
870
|
+
return;
|
871
|
+
}
|
872
|
+
}
|
873
|
+
|
874
|
+
// If no match found, select first available
|
875
|
+
modelInput.value = modelOptions[0].label;
|
876
|
+
selectedModelValue = modelOptions[0].value;
|
877
|
+
console.log('Auto-selected first available model:', selectedModelValue);
|
878
|
+
|
879
|
+
// Update any hidden input if it exists
|
880
|
+
const hiddenInput = document.getElementById('model_hidden');
|
881
|
+
if (hiddenInput) {
|
882
|
+
hiddenInput.value = selectedModelValue;
|
883
|
+
}
|
884
|
+
|
885
|
+
// Only save to settings if we're not initializing
|
886
|
+
if (!isInitializing) {
|
887
|
+
saveModelSettings(selectedModelValue);
|
888
|
+
}
|
889
|
+
}
|
890
|
+
}
|
891
|
+
|
892
|
+
/**
|
893
|
+
* Check if Ollama is running and available
|
894
|
+
* @returns {Promise<boolean>} True if Ollama is running
|
895
|
+
*/
|
896
|
+
async function isOllamaRunning() {
|
897
|
+
try {
|
898
|
+
// Use the API endpoint with proper timeout handling
|
899
|
+
const controller = new AbortController();
|
900
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
901
|
+
|
902
|
+
const response = await fetch('/research/settings/api/ollama-status', {
|
903
|
+
signal: controller.signal
|
904
|
+
});
|
905
|
+
|
906
|
+
clearTimeout(timeoutId);
|
907
|
+
|
908
|
+
if (response.ok) {
|
909
|
+
const data = await response.json();
|
910
|
+
return data.running === true;
|
911
|
+
}
|
912
|
+
return false;
|
913
|
+
} catch (error) {
|
914
|
+
console.error('Ollama check failed:', error.name === 'AbortError' ? 'Request timed out' : error);
|
915
|
+
return false;
|
916
|
+
}
|
917
|
+
}
|
918
|
+
|
919
|
+
/**
|
920
|
+
* Get the currently selected model value
|
921
|
+
* @returns {string} The selected model value
|
922
|
+
*/
|
923
|
+
function getSelectedModel() {
|
924
|
+
console.log('Getting selected model...');
|
925
|
+
console.log('- selectedModelValue:', selectedModelValue);
|
926
|
+
console.log('- modelInput value:', modelInput ? modelInput.value : 'modelInput not found');
|
927
|
+
console.log('- modelInput exists:', !!modelInput);
|
928
|
+
|
929
|
+
// First try the stored selected value from dropdown
|
930
|
+
if (selectedModelValue) {
|
931
|
+
console.log('Using selectedModelValue:', selectedModelValue);
|
932
|
+
return selectedModelValue;
|
933
|
+
}
|
934
|
+
|
935
|
+
// Then try the input field value
|
936
|
+
if (modelInput && modelInput.value.trim()) {
|
937
|
+
console.log('Using modelInput value:', modelInput.value.trim());
|
938
|
+
return modelInput.value.trim();
|
939
|
+
}
|
940
|
+
|
941
|
+
// Finally, check if there's a hidden input with the model value
|
942
|
+
const hiddenModelInput = document.getElementById('model_hidden');
|
943
|
+
if (hiddenModelInput && hiddenModelInput.value) {
|
944
|
+
console.log('Using hidden input value:', hiddenModelInput.value);
|
945
|
+
return hiddenModelInput.value;
|
946
|
+
}
|
947
|
+
|
948
|
+
console.log('No model value found, returning empty string');
|
949
|
+
return "";
|
950
|
+
}
|
951
|
+
|
952
|
+
/**
|
953
|
+
* Check if Ollama is running and the selected model is available
|
954
|
+
* @returns {Promise<{success: boolean, error: string, solution: string}>} Result of the check
|
955
|
+
*/
|
956
|
+
async function checkOllamaModel() {
|
957
|
+
const isRunning = await isOllamaRunning();
|
958
|
+
|
959
|
+
if (!isRunning) {
|
960
|
+
return {
|
961
|
+
success: false,
|
962
|
+
error: "Ollama service is not running.",
|
963
|
+
solution: "Please start Ollama and try again. If you've recently updated, you may need to run database migration with 'python -m src.local_deep_research.migrate_db'."
|
964
|
+
};
|
965
|
+
}
|
966
|
+
|
967
|
+
// Get the currently selected model
|
968
|
+
const model = getSelectedModel();
|
969
|
+
|
970
|
+
if (!model) {
|
971
|
+
return {
|
972
|
+
success: false,
|
973
|
+
error: "No model selected.",
|
974
|
+
solution: "Please select or enter a valid model name."
|
975
|
+
};
|
976
|
+
}
|
977
|
+
|
978
|
+
// Check if the model is available in Ollama
|
979
|
+
try {
|
980
|
+
const controller = new AbortController();
|
981
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
982
|
+
|
983
|
+
const response = await fetch(`/research/api/check/ollama_model?model=${encodeURIComponent(model)}`, {
|
984
|
+
signal: controller.signal
|
985
|
+
});
|
986
|
+
|
987
|
+
clearTimeout(timeoutId);
|
988
|
+
|
989
|
+
if (!response.ok) {
|
990
|
+
return {
|
991
|
+
success: false,
|
992
|
+
error: "Error checking model availability.",
|
993
|
+
solution: "Please check your Ollama installation and try again."
|
994
|
+
};
|
995
|
+
}
|
996
|
+
|
997
|
+
const data = await response.json();
|
998
|
+
|
999
|
+
if (data.available) {
|
1000
|
+
return {
|
1001
|
+
success: true
|
1002
|
+
};
|
1003
|
+
} else {
|
1004
|
+
return {
|
1005
|
+
success: false,
|
1006
|
+
error: data.message || "The selected model is not available in Ollama.",
|
1007
|
+
solution: "Please pull the model first using 'ollama pull " + model + "' or select a different model."
|
1008
|
+
};
|
1009
|
+
}
|
1010
|
+
} catch (error) {
|
1011
|
+
console.error("Error checking Ollama model:", error);
|
1012
|
+
return {
|
1013
|
+
success: false,
|
1014
|
+
error: "Error checking model availability: " + error.message,
|
1015
|
+
solution: "Please check your Ollama installation and try again."
|
1016
|
+
};
|
1017
|
+
}
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
// Load settings from the database
|
1021
|
+
function loadSettings() {
|
1022
|
+
console.log('Loading settings from database...');
|
1023
|
+
let numApiCallsPending = 2;
|
1024
|
+
|
1025
|
+
// Fetch the current settings from the settings API
|
1026
|
+
fetch('/research/settings/api/llm', {
|
1027
|
+
method: 'GET',
|
1028
|
+
headers: {
|
1029
|
+
'Content-Type': 'application/json'
|
1030
|
+
}
|
1031
|
+
})
|
1032
|
+
.then(response => {
|
1033
|
+
if (!response.ok) {
|
1034
|
+
throw new Error(`API error: ${response.status}`);
|
1035
|
+
}
|
1036
|
+
return response.json();
|
1037
|
+
})
|
1038
|
+
.then(data => {
|
1039
|
+
console.log('Loaded settings from database:', data);
|
1040
|
+
|
1041
|
+
// If we have a settings object in the response
|
1042
|
+
if (data && data.settings) {
|
1043
|
+
// Find the provider and model settings
|
1044
|
+
const providerSetting = data.settings.value["provider"];
|
1045
|
+
const modelSetting = data.settings.value["model"];
|
1046
|
+
const customEndpointUrl = data.settings.value["openai_endpoint_url"];
|
1047
|
+
|
1048
|
+
// Update provider dropdown if we have a valid provider
|
1049
|
+
if (providerSetting && modelProviderSelect) {
|
1050
|
+
const providerValue = providerSetting.toUpperCase();
|
1051
|
+
console.log('Setting provider to:', providerValue);
|
1052
|
+
|
1053
|
+
// Find the matching option in the dropdown
|
1054
|
+
const matchingOption = Array.from(modelProviderSelect.options).find(
|
1055
|
+
option => option.value.toUpperCase() === providerValue
|
1056
|
+
);
|
1057
|
+
|
1058
|
+
if (matchingOption) {
|
1059
|
+
console.log('Found matching provider option:', matchingOption.value);
|
1060
|
+
modelProviderSelect.value = matchingOption.value;
|
1061
|
+
// Also save to localStorage
|
1062
|
+
localStorage.setItem('lastUsedProvider', matchingOption.value);
|
1063
|
+
} else {
|
1064
|
+
// If no match, try to find case-insensitive or partial match
|
1065
|
+
const caseInsensitiveMatch = Array.from(modelProviderSelect.options).find(
|
1066
|
+
option => option.value.toUpperCase().includes(providerValue) ||
|
1067
|
+
providerValue.includes(option.value.toUpperCase())
|
1068
|
+
);
|
1069
|
+
|
1070
|
+
if (caseInsensitiveMatch) {
|
1071
|
+
console.log('Found case-insensitive provider match:', caseInsensitiveMatch.value);
|
1072
|
+
modelProviderSelect.value = caseInsensitiveMatch.value;
|
1073
|
+
// Also save to localStorage
|
1074
|
+
localStorage.setItem('lastUsedProvider', caseInsensitiveMatch.value);
|
1075
|
+
} else {
|
1076
|
+
console.warn(`No matching provider option found for '${providerValue}'`);
|
1077
|
+
}
|
1078
|
+
}
|
1079
|
+
|
1080
|
+
// Display endpoint container if using custom endpoint
|
1081
|
+
if (endpointContainer) {
|
1082
|
+
endpointContainer.style.display =
|
1083
|
+
providerValue === 'OPENAI_ENDPOINT' ? 'block' : 'none';
|
1084
|
+
}
|
1085
|
+
}
|
1086
|
+
|
1087
|
+
// Update the custom endpoint URl if we have one.
|
1088
|
+
if (customEndpointUrl && customEndpointInput) {
|
1089
|
+
console.log('Setting endpoint URL to:', customEndpointUrl);
|
1090
|
+
customEndpointInput.value = customEndpointUrl;
|
1091
|
+
}
|
1092
|
+
|
1093
|
+
// Load model options based on the current provider
|
1094
|
+
const currentProvider = modelProviderSelect ? modelProviderSelect.value : 'OLLAMA';
|
1095
|
+
updateModelOptionsForProvider(currentProvider, false).then(() => {
|
1096
|
+
// Update model selection if we have a valid model
|
1097
|
+
if (modelSetting && modelInput) {
|
1098
|
+
const modelValue = modelSetting;
|
1099
|
+
console.log('Setting model to:', modelValue);
|
1100
|
+
|
1101
|
+
// Save to localStorage
|
1102
|
+
localStorage.setItem('lastUsedModel', modelValue);
|
1103
|
+
|
1104
|
+
// Find the model in our loaded options
|
1105
|
+
const matchingModel = modelOptions.find(m =>
|
1106
|
+
m.value === modelValue || m.id === modelValue
|
1107
|
+
);
|
1108
|
+
|
1109
|
+
if (matchingModel) {
|
1110
|
+
console.log('Found matching model in options:', matchingModel);
|
1111
|
+
|
1112
|
+
// Set the input field value
|
1113
|
+
modelInput.value = matchingModel.label || modelValue;
|
1114
|
+
selectedModelValue = modelValue;
|
1115
|
+
|
1116
|
+
// Also update hidden input if it exists
|
1117
|
+
const hiddenInput = document.getElementById('model_hidden');
|
1118
|
+
if (hiddenInput) {
|
1119
|
+
hiddenInput.value = modelValue;
|
1120
|
+
}
|
1121
|
+
} else {
|
1122
|
+
// If no matching model found, just set the raw value
|
1123
|
+
console.warn(`No matching model found for '${modelValue}'`);
|
1124
|
+
modelInput.value = modelValue;
|
1125
|
+
selectedModelValue = modelValue;
|
1126
|
+
|
1127
|
+
// Also update hidden input if it exists
|
1128
|
+
const hiddenInput = document.getElementById('model_hidden');
|
1129
|
+
if (hiddenInput) {
|
1130
|
+
hiddenInput.value = modelValue;
|
1131
|
+
}
|
1132
|
+
}
|
1133
|
+
}
|
1134
|
+
});
|
1135
|
+
|
1136
|
+
|
1137
|
+
}
|
1138
|
+
|
1139
|
+
// If all the calls to the settings API are finished, we're no
|
1140
|
+
// longer initializing.
|
1141
|
+
numApiCallsPending--;
|
1142
|
+
isInitializing = (numApiCallsPending === 0);
|
1143
|
+
})
|
1144
|
+
.catch(error => {
|
1145
|
+
console.error('Error loading settings:', error);
|
1146
|
+
|
1147
|
+
// Fallback to localStorage if database fetch fails
|
1148
|
+
fallbackToLocalStorageSettings();
|
1149
|
+
|
1150
|
+
// Even if there's an error, we're done initializing
|
1151
|
+
numApiCallsPending--;
|
1152
|
+
isInitializing = (numApiCallsPending === 0);
|
1153
|
+
});
|
1154
|
+
|
1155
|
+
fetch('/research/settings/api/search.tool', {
|
1156
|
+
method: 'GET',
|
1157
|
+
headers: {
|
1158
|
+
'Content-Type': 'application/json'
|
1159
|
+
}
|
1160
|
+
})
|
1161
|
+
.then(response => {
|
1162
|
+
if (!response.ok) {
|
1163
|
+
throw new Error(`API error: ${response.status}`);
|
1164
|
+
}
|
1165
|
+
return response.json();
|
1166
|
+
})
|
1167
|
+
.then(data => {
|
1168
|
+
console.log('Loaded settings from database:', data);
|
1169
|
+
|
1170
|
+
// If we have a settings object in the response
|
1171
|
+
if (data && data.settings) {
|
1172
|
+
// Find the provider and model settings
|
1173
|
+
const searchEngineSetting = data.settings;
|
1174
|
+
|
1175
|
+
// Update search engine if we have a valid value
|
1176
|
+
if (searchEngineSetting && searchEngineSetting.value && searchEngineInput) {
|
1177
|
+
const engineValue = searchEngineSetting.value;
|
1178
|
+
console.log('Setting search engine to:', engineValue);
|
1179
|
+
|
1180
|
+
// Save to localStorage
|
1181
|
+
localStorage.setItem('lastUsedSearchEngine', engineValue);
|
1182
|
+
|
1183
|
+
// Find the engine in our loaded options
|
1184
|
+
const matchingEngine = searchEngineOptions.find(e =>
|
1185
|
+
e.value === engineValue || e.id === engineValue
|
1186
|
+
);
|
1187
|
+
|
1188
|
+
if (matchingEngine) {
|
1189
|
+
console.log('Found matching search engine in options:', matchingEngine);
|
1190
|
+
|
1191
|
+
// Set the input field value
|
1192
|
+
searchEngineInput.value = matchingEngine.label || engineValue;
|
1193
|
+
selectedSearchEngineValue = engineValue;
|
1194
|
+
|
1195
|
+
// Also update hidden input if it exists
|
1196
|
+
const hiddenInput = document.getElementById('search_engine_hidden');
|
1197
|
+
if (hiddenInput) {
|
1198
|
+
hiddenInput.value = engineValue;
|
1199
|
+
}
|
1200
|
+
} else {
|
1201
|
+
// If no matching engine found, just set the raw value
|
1202
|
+
console.warn(`No matching search engine found for '${engineValue}'`);
|
1203
|
+
searchEngineInput.value = engineValue;
|
1204
|
+
selectedSearchEngineValue = engineValue;
|
1205
|
+
|
1206
|
+
// Also update hidden input if it exists
|
1207
|
+
const hiddenInput = document.getElementById('search_engine_hidden');
|
1208
|
+
if (hiddenInput) {
|
1209
|
+
hiddenInput.value = engineValue;
|
1210
|
+
}
|
1211
|
+
}
|
1212
|
+
}
|
1213
|
+
}
|
1214
|
+
|
1215
|
+
// If all the calls to the settings API are finished, we're no
|
1216
|
+
// longer initializing.
|
1217
|
+
numApiCallsPending--;
|
1218
|
+
isInitializing = (numApiCallsPending === 0);
|
1219
|
+
|
1220
|
+
})
|
1221
|
+
.catch(error => {
|
1222
|
+
console.error('Error loading settings:', error);
|
1223
|
+
|
1224
|
+
// Fallback to localStorage if database fetch fails
|
1225
|
+
fallbackToLocalStorageSettings();
|
1226
|
+
|
1227
|
+
// Even if there's an error, we're done initializing
|
1228
|
+
numApiCallsPending--;
|
1229
|
+
isInitializing = (numApiCallsPending === 0);
|
1230
|
+
});
|
1231
|
+
}
|
1232
|
+
|
1233
|
+
// Add a fallback function to use localStorage settings
|
1234
|
+
function fallbackToLocalStorageSettings() {
|
1235
|
+
const provider = localStorage.getItem('lastUsedProvider');
|
1236
|
+
const model = localStorage.getItem('lastUsedModel');
|
1237
|
+
const searchEngine = localStorage.getItem('lastUsedSearchEngine');
|
1238
|
+
|
1239
|
+
console.log('Falling back to localStorage settings:', { provider, model, searchEngine });
|
1240
|
+
|
1241
|
+
if (provider && modelProviderSelect) {
|
1242
|
+
modelProviderSelect.value = provider;
|
1243
|
+
// Show/hide custom endpoint input if needed
|
1244
|
+
if (endpointContainer) {
|
1245
|
+
endpointContainer.style.display =
|
1246
|
+
provider === 'OPENAI_ENDPOINT' ? 'block' : 'none';
|
1247
|
+
}
|
1248
|
+
}
|
1249
|
+
|
1250
|
+
const currentProvider = modelProviderSelect ? modelProviderSelect.value : 'OLLAMA';
|
1251
|
+
updateModelOptionsForProvider(currentProvider, !model);
|
1252
|
+
|
1253
|
+
if (model && modelInput) {
|
1254
|
+
const matchingModel = modelOptions.find(m => m.value === model);
|
1255
|
+
if (matchingModel) {
|
1256
|
+
modelInput.value = matchingModel.label;
|
1257
|
+
} else {
|
1258
|
+
modelInput.value = model;
|
1259
|
+
}
|
1260
|
+
selectedModelValue = model;
|
1261
|
+
|
1262
|
+
// Update hidden input if it exists
|
1263
|
+
const hiddenInput = document.getElementById('model_hidden');
|
1264
|
+
if (hiddenInput) {
|
1265
|
+
hiddenInput.value = model;
|
1266
|
+
}
|
1267
|
+
}
|
1268
|
+
|
1269
|
+
if (searchEngine && searchEngineInput) {
|
1270
|
+
const matchingEngine = searchEngineOptions.find(e => e.value === searchEngine);
|
1271
|
+
if (matchingEngine) {
|
1272
|
+
searchEngineInput.value = matchingEngine.label;
|
1273
|
+
} else {
|
1274
|
+
searchEngineInput.value = searchEngine;
|
1275
|
+
}
|
1276
|
+
selectedSearchEngineValue = searchEngine;
|
1277
|
+
|
1278
|
+
// Update hidden input if it exists
|
1279
|
+
const hiddenInput = document.getElementById('search_engine_hidden');
|
1280
|
+
if (hiddenInput) {
|
1281
|
+
hiddenInput.value = searchEngine;
|
1282
|
+
}
|
1283
|
+
}
|
1284
|
+
}
|
1285
|
+
|
1286
|
+
/**
|
1287
|
+
* Load model options from API or cache
|
1288
|
+
*/
|
1289
|
+
function loadModelOptions(forceRefresh = false) {
|
1290
|
+
return new Promise((resolve, reject) => {
|
1291
|
+
// Check cache first if not forcing refresh
|
1292
|
+
if (!forceRefresh) {
|
1293
|
+
const cachedData = getCachedData(CACHE_KEYS.MODELS);
|
1294
|
+
const cacheTimestamp = getCachedData(CACHE_KEYS.CACHE_TIMESTAMP);
|
1295
|
+
|
1296
|
+
// Use cache if it exists and isn't expired
|
1297
|
+
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp < CACHE_EXPIRATION)) {
|
1298
|
+
console.log('Using cached model data');
|
1299
|
+
resolve(cachedData);
|
1300
|
+
return;
|
1301
|
+
}
|
1302
|
+
}
|
1303
|
+
|
1304
|
+
// Add loading class to parent
|
1305
|
+
if (modelInput && modelInput.parentNode) {
|
1306
|
+
modelInput.parentNode.classList.add('loading');
|
1307
|
+
}
|
1308
|
+
|
1309
|
+
// Fetch from API if cache is invalid or refresh is forced
|
1310
|
+
fetch('/research/settings/api/available-models')
|
1311
|
+
.then(response => {
|
1312
|
+
if (!response.ok) {
|
1313
|
+
throw new Error(`API error: ${response.status}`);
|
1314
|
+
}
|
1315
|
+
return response.json();
|
1316
|
+
})
|
1317
|
+
.then(data => {
|
1318
|
+
// Remove loading class
|
1319
|
+
if (modelInput && modelInput.parentNode) {
|
1320
|
+
modelInput.parentNode.classList.remove('loading');
|
1321
|
+
}
|
1322
|
+
|
1323
|
+
if (data && data.providers) {
|
1324
|
+
console.log('Got model data from API:', data);
|
1325
|
+
|
1326
|
+
// Format the data for our dropdown
|
1327
|
+
const formattedModels = formatModelsFromAPI(data);
|
1328
|
+
|
1329
|
+
// Cache the data
|
1330
|
+
cacheData(CACHE_KEYS.MODELS, formattedModels);
|
1331
|
+
cacheData(CACHE_KEYS.CACHE_TIMESTAMP, Date.now());
|
1332
|
+
|
1333
|
+
// Also cache with the settings.js cache keys for cross-component sharing
|
1334
|
+
cacheData('deepResearch.availableModels', formattedModels);
|
1335
|
+
cacheData('deepResearch.cacheTimestamp', Date.now());
|
1336
|
+
|
1337
|
+
resolve(formattedModels);
|
1338
|
+
} else {
|
1339
|
+
throw new Error('Invalid model data format');
|
1340
|
+
}
|
1341
|
+
})
|
1342
|
+
.catch(error => {
|
1343
|
+
console.error('Error loading models:', error);
|
1344
|
+
|
1345
|
+
// Remove loading class on error
|
1346
|
+
if (modelInput && modelInput.parentNode) {
|
1347
|
+
modelInput.parentNode.classList.remove('loading');
|
1348
|
+
}
|
1349
|
+
|
1350
|
+
// Use cached data if available, even if expired
|
1351
|
+
const cachedData = getCachedData(CACHE_KEYS.MODELS);
|
1352
|
+
if (cachedData) {
|
1353
|
+
console.log('Using expired cached model data due to API error');
|
1354
|
+
resolve(cachedData);
|
1355
|
+
} else {
|
1356
|
+
// Use fallback data if no cache available
|
1357
|
+
console.log('Using fallback model data');
|
1358
|
+
const fallbackModels = getFallbackModels();
|
1359
|
+
cacheData(CACHE_KEYS.MODELS, fallbackModels);
|
1360
|
+
cacheData('deepResearch.availableModels', fallbackModels);
|
1361
|
+
resolve(fallbackModels);
|
1362
|
+
}
|
1363
|
+
});
|
1364
|
+
});
|
1365
|
+
}
|
1366
|
+
|
1367
|
+
// Format models from API response
|
1368
|
+
function formatModelsFromAPI(data) {
|
1369
|
+
const formatted = [];
|
1370
|
+
|
1371
|
+
// Process provider options
|
1372
|
+
if (data.provider_options) {
|
1373
|
+
data.provider_options.forEach(provider => {
|
1374
|
+
formatted.push({
|
1375
|
+
...provider,
|
1376
|
+
isProvider: true // Flag to identify provider options
|
1377
|
+
});
|
1378
|
+
});
|
1379
|
+
}
|
1380
|
+
|
1381
|
+
// Process Ollama models
|
1382
|
+
if (data.providers && data.providers.ollama_models) {
|
1383
|
+
data.providers.ollama_models.forEach(model => {
|
1384
|
+
formatted.push({
|
1385
|
+
...model,
|
1386
|
+
id: model.value,
|
1387
|
+
provider: 'OLLAMA'
|
1388
|
+
});
|
1389
|
+
});
|
1390
|
+
}
|
1391
|
+
|
1392
|
+
// Process OpenAI models
|
1393
|
+
if (data.providers && data.providers.openai_models) {
|
1394
|
+
data.providers.openai_models.forEach(model => {
|
1395
|
+
formatted.push({
|
1396
|
+
...model,
|
1397
|
+
id: model.value,
|
1398
|
+
provider: 'OPENAI'
|
1399
|
+
});
|
1400
|
+
});
|
1401
|
+
}
|
1402
|
+
|
1403
|
+
// Process Anthropic models
|
1404
|
+
if (data.providers && data.providers.anthropic_models) {
|
1405
|
+
data.providers.anthropic_models.forEach(model => {
|
1406
|
+
formatted.push({
|
1407
|
+
...model,
|
1408
|
+
id: model.value,
|
1409
|
+
provider: 'ANTHROPIC'
|
1410
|
+
});
|
1411
|
+
});
|
1412
|
+
}
|
1413
|
+
|
1414
|
+
return formatted;
|
1415
|
+
}
|
1416
|
+
|
1417
|
+
// Get fallback models if API fails
|
1418
|
+
function getFallbackModels() {
|
1419
|
+
return [
|
1420
|
+
// Ollama models
|
1421
|
+
{ id: 'llama3', value: 'llama3', label: 'Llama 3 (Ollama)', provider: 'OLLAMA' },
|
1422
|
+
{ id: 'mistral', value: 'mistral', label: 'Mistral (Ollama)', provider: 'OLLAMA' },
|
1423
|
+
{ id: 'gemma:latest', value: 'gemma:latest', label: 'Gemma (Ollama)', provider: 'OLLAMA' },
|
1424
|
+
|
1425
|
+
// OpenAI models
|
1426
|
+
{ id: 'gpt-4o', value: 'gpt-4o', label: 'GPT-4o (OpenAI)', provider: 'OPENAI' },
|
1427
|
+
{ id: 'gpt-4', value: 'gpt-4', label: 'GPT-4 (OpenAI)', provider: 'OPENAI' },
|
1428
|
+
{ id: 'gpt-3.5-turbo', value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (OpenAI)', provider: 'OPENAI' },
|
1429
|
+
|
1430
|
+
// Anthropic models
|
1431
|
+
{ id: 'claude-3-5-sonnet-latest', value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (Anthropic)', provider: 'ANTHROPIC' },
|
1432
|
+
{ id: 'claude-3-opus-20240229', value: 'claude-3-opus-20240229', label: 'Claude 3 Opus (Anthropic)', provider: 'ANTHROPIC' },
|
1433
|
+
{ id: 'claude-3-sonnet-20240229', value: 'claude-3-sonnet-20240229', label: 'Claude 3 Sonnet (Anthropic)', provider: 'ANTHROPIC' }
|
1434
|
+
];
|
1435
|
+
}
|
1436
|
+
|
1437
|
+
// Cache and retrieve data in localStorage
|
1438
|
+
function cacheData(key, data) {
|
1439
|
+
try {
|
1440
|
+
localStorage.setItem(key, JSON.stringify(data));
|
1441
|
+
} catch (e) {
|
1442
|
+
console.error('Error caching data:', e);
|
1443
|
+
}
|
1444
|
+
}
|
1445
|
+
|
1446
|
+
function getCachedData(key) {
|
1447
|
+
try {
|
1448
|
+
const item = localStorage.getItem(key);
|
1449
|
+
return item ? JSON.parse(item) : null;
|
1450
|
+
} catch (e) {
|
1451
|
+
console.error('Error retrieving cached data:', e);
|
1452
|
+
return null;
|
1453
|
+
}
|
1454
|
+
}
|
1455
|
+
|
1456
|
+
// Load search engine options
|
1457
|
+
function loadSearchEngineOptions(forceRefresh = false) {
|
1458
|
+
return new Promise((resolve, reject) => {
|
1459
|
+
// Check cache first if not forcing refresh
|
1460
|
+
if (!forceRefresh) {
|
1461
|
+
const cachedData = getCachedData(CACHE_KEYS.SEARCH_ENGINES);
|
1462
|
+
const cacheTimestamp = getCachedData(CACHE_KEYS.CACHE_TIMESTAMP);
|
1463
|
+
|
1464
|
+
// Use cache if it exists and isn't expired
|
1465
|
+
if (cachedData && cacheTimestamp && (Date.now() - cacheTimestamp < CACHE_EXPIRATION)) {
|
1466
|
+
console.log('Using cached search engine data');
|
1467
|
+
searchEngineOptions = cachedData; // Ensure the global variable is updated
|
1468
|
+
resolve(cachedData);
|
1469
|
+
return;
|
1470
|
+
}
|
1471
|
+
}
|
1472
|
+
|
1473
|
+
// Add loading class to parent
|
1474
|
+
if (searchEngineInput && searchEngineInput.parentNode) {
|
1475
|
+
searchEngineInput.parentNode.classList.add('loading');
|
1476
|
+
}
|
1477
|
+
|
1478
|
+
console.log('Fetching search engines from API...');
|
1479
|
+
|
1480
|
+
// Fetch from API
|
1481
|
+
fetch('/research/settings/api/available-search-engines')
|
1482
|
+
.then(response => {
|
1483
|
+
if (!response.ok) {
|
1484
|
+
throw new Error(`API error: ${response.status}`);
|
1485
|
+
}
|
1486
|
+
return response.json();
|
1487
|
+
})
|
1488
|
+
.then(data => {
|
1489
|
+
// Remove loading class
|
1490
|
+
if (searchEngineInput && searchEngineInput.parentNode) {
|
1491
|
+
searchEngineInput.parentNode.classList.remove('loading');
|
1492
|
+
}
|
1493
|
+
|
1494
|
+
// Log the entire response to debug
|
1495
|
+
console.log('Search engine API response:', data);
|
1496
|
+
|
1497
|
+
// Extract engines from the data based on the actual response format
|
1498
|
+
let formattedEngines = [];
|
1499
|
+
|
1500
|
+
// Handle the case where API returns {engine_options, engines}
|
1501
|
+
if (data && data.engine_options) {
|
1502
|
+
console.log('Processing engine_options:', data.engine_options.length + ' options');
|
1503
|
+
|
1504
|
+
// Map the engine options to our dropdown format
|
1505
|
+
formattedEngines = data.engine_options.map(engine => ({
|
1506
|
+
value: engine.value || engine.id || '',
|
1507
|
+
label: engine.label || engine.name || engine.value || '',
|
1508
|
+
type: engine.type || 'search'
|
1509
|
+
}));
|
1510
|
+
}
|
1511
|
+
// Also try adding engines from engines object if it exists
|
1512
|
+
if (data && data.engines) {
|
1513
|
+
console.log('Processing engines object:', Object.keys(data.engines).length + ' engine types');
|
1514
|
+
|
1515
|
+
// Handle each type of engine in the engines object
|
1516
|
+
Object.keys(data.engines).forEach(engineType => {
|
1517
|
+
const enginesOfType = data.engines[engineType];
|
1518
|
+
if (Array.isArray(enginesOfType)) {
|
1519
|
+
console.log(`Processing ${engineType} engines:`, enginesOfType.length + ' engines');
|
1520
|
+
|
1521
|
+
// Map each engine to our dropdown format
|
1522
|
+
const typeEngines = enginesOfType.map(engine => ({
|
1523
|
+
value: engine.value || engine.id || '',
|
1524
|
+
label: engine.label || engine.name || engine.value || '',
|
1525
|
+
type: engineType
|
1526
|
+
}));
|
1527
|
+
|
1528
|
+
// Add to our formatted engines array
|
1529
|
+
formattedEngines = [...formattedEngines, ...typeEngines];
|
1530
|
+
}
|
1531
|
+
});
|
1532
|
+
}
|
1533
|
+
// Handle classic format with search_engines array
|
1534
|
+
else if (data && data.search_engines) {
|
1535
|
+
console.log('Processing search_engines array:', data.search_engines.length + ' engines');
|
1536
|
+
formattedEngines = data.search_engines.map(engine => ({
|
1537
|
+
value: engine.id || engine.value || '',
|
1538
|
+
label: engine.name || engine.label || '',
|
1539
|
+
type: engine.type || 'search'
|
1540
|
+
}));
|
1541
|
+
}
|
1542
|
+
// Handle direct array format
|
1543
|
+
else if (data && Array.isArray(data)) {
|
1544
|
+
console.log('Processing direct array:', data.length + ' engines');
|
1545
|
+
formattedEngines = data.map(engine => ({
|
1546
|
+
value: engine.id || engine.value || '',
|
1547
|
+
label: engine.name || engine.label || '',
|
1548
|
+
type: engine.type || 'search'
|
1549
|
+
}));
|
1550
|
+
}
|
1551
|
+
|
1552
|
+
console.log('Final formatted search engines:', formattedEngines);
|
1553
|
+
|
1554
|
+
if (formattedEngines.length > 0) {
|
1555
|
+
// Cache the data
|
1556
|
+
cacheData(CACHE_KEYS.SEARCH_ENGINES, formattedEngines);
|
1557
|
+
|
1558
|
+
// Update global searchEngineOptions
|
1559
|
+
searchEngineOptions = formattedEngines;
|
1560
|
+
|
1561
|
+
resolve(formattedEngines);
|
1562
|
+
} else {
|
1563
|
+
throw new Error('No valid search engines found in API response');
|
1564
|
+
}
|
1565
|
+
})
|
1566
|
+
.catch(error => {
|
1567
|
+
console.error('Error loading search engines:', error);
|
1568
|
+
|
1569
|
+
// Remove loading class on error
|
1570
|
+
if (searchEngineInput && searchEngineInput.parentNode) {
|
1571
|
+
searchEngineInput.parentNode.classList.remove('loading');
|
1572
|
+
}
|
1573
|
+
|
1574
|
+
// Use cached data if available, even if expired
|
1575
|
+
const cachedData = getCachedData(CACHE_KEYS.SEARCH_ENGINES);
|
1576
|
+
if (cachedData) {
|
1577
|
+
console.log('Using expired cached search engine data due to API error');
|
1578
|
+
searchEngineOptions = cachedData;
|
1579
|
+
resolve(cachedData);
|
1580
|
+
} else {
|
1581
|
+
// Use fallback data if no cache available
|
1582
|
+
console.log('Using fallback search engine data');
|
1583
|
+
const fallbackEngines = [
|
1584
|
+
{ value: 'google', label: 'Google Search' },
|
1585
|
+
{ value: 'duckduckgo', label: 'DuckDuckGo' },
|
1586
|
+
{ value: 'bing', label: 'Bing Search' }
|
1587
|
+
];
|
1588
|
+
searchEngineOptions = fallbackEngines;
|
1589
|
+
cacheData(CACHE_KEYS.SEARCH_ENGINES, fallbackEngines);
|
1590
|
+
resolve(fallbackEngines);
|
1591
|
+
}
|
1592
|
+
});
|
1593
|
+
});
|
1594
|
+
}
|
1595
|
+
|
1596
|
+
// Save model settings to database
|
1597
|
+
function saveModelSettings(modelValue) {
|
1598
|
+
// Save selection to localStorage for persistence between sessions
|
1599
|
+
localStorage.setItem('lastUsedModel', modelValue);
|
1600
|
+
|
1601
|
+
// Update any hidden input with the same settings key that might exist in other forms
|
1602
|
+
const hiddenInputs = document.querySelectorAll('input[id$="_hidden"][name="llm.model"]');
|
1603
|
+
hiddenInputs.forEach(input => {
|
1604
|
+
input.value = modelValue;
|
1605
|
+
});
|
1606
|
+
|
1607
|
+
// Save to the database using the settings API
|
1608
|
+
fetch('/research/settings/api/llm.model', {
|
1609
|
+
method: 'PUT',
|
1610
|
+
headers: {
|
1611
|
+
'Content-Type': 'application/json',
|
1612
|
+
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
1613
|
+
},
|
1614
|
+
body: JSON.stringify({ value: modelValue })
|
1615
|
+
})
|
1616
|
+
.then(response => response.json())
|
1617
|
+
.then(data => {
|
1618
|
+
console.log('Model setting saved to database:', data);
|
1619
|
+
|
1620
|
+
// Optionally show a notification if there's UI notification support
|
1621
|
+
if (window.ui && window.ui.showMessage) {
|
1622
|
+
window.ui.showMessage(`Model updated to: ${modelValue}`, 'success', 2000);
|
1623
|
+
}
|
1624
|
+
})
|
1625
|
+
.catch(error => {
|
1626
|
+
console.error('Error saving model setting to database:', error);
|
1627
|
+
|
1628
|
+
// Show error notification if available
|
1629
|
+
if (window.ui && window.ui.showMessage) {
|
1630
|
+
window.ui.showMessage(`Error updating model: ${error.message}`, 'error', 3000);
|
1631
|
+
}
|
1632
|
+
});
|
1633
|
+
}
|
1634
|
+
|
1635
|
+
// Save search engine settings to database
|
1636
|
+
function saveSearchEngineSettings(engineValue) {
|
1637
|
+
// Save to localStorage
|
1638
|
+
localStorage.setItem('lastUsedSearchEngine', engineValue);
|
1639
|
+
|
1640
|
+
// Update any hidden input with the same settings key that might exist in other forms
|
1641
|
+
const hiddenInputs = document.querySelectorAll('input[id$="_hidden"][name="search.tool"]');
|
1642
|
+
hiddenInputs.forEach(input => {
|
1643
|
+
input.value = engineValue;
|
1644
|
+
});
|
1645
|
+
|
1646
|
+
// Save to the database using the settings API
|
1647
|
+
fetch('/research/settings/api/search.tool', {
|
1648
|
+
method: 'PUT',
|
1649
|
+
headers: {
|
1650
|
+
'Content-Type': 'application/json',
|
1651
|
+
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
1652
|
+
},
|
1653
|
+
body: JSON.stringify({ value: engineValue })
|
1654
|
+
})
|
1655
|
+
.then(response => response.json())
|
1656
|
+
.then(data => {
|
1657
|
+
console.log('Search engine setting saved to database:', data);
|
1658
|
+
|
1659
|
+
// Optionally show a notification
|
1660
|
+
if (window.ui && window.ui.showMessage) {
|
1661
|
+
window.ui.showMessage(`Search engine updated to: ${engineValue}`, 'success', 2000);
|
1662
|
+
}
|
1663
|
+
})
|
1664
|
+
.catch(error => {
|
1665
|
+
console.error('Error saving search engine setting to database:', error);
|
1666
|
+
|
1667
|
+
// Show error notification if available
|
1668
|
+
if (window.ui && window.ui.showMessage) {
|
1669
|
+
window.ui.showMessage(`Error updating search engine: ${error.message}`, 'error', 3000);
|
1670
|
+
}
|
1671
|
+
});
|
1672
|
+
}
|
1673
|
+
|
1674
|
+
// Save provider setting to database
|
1675
|
+
function saveProviderSetting(providerValue) {
|
1676
|
+
// Save to localStorage
|
1677
|
+
localStorage.setItem('lastUsedProvider', providerValue);
|
1678
|
+
|
1679
|
+
// Update any hidden input with the same settings key that might exist in other forms
|
1680
|
+
const hiddenInputs = document.querySelectorAll('input[id$="_hidden"][name="llm.provider"]');
|
1681
|
+
hiddenInputs.forEach(input => {
|
1682
|
+
input.value = providerValue;
|
1683
|
+
});
|
1684
|
+
|
1685
|
+
// Save to the database using the settings API
|
1686
|
+
fetch('/research/settings/api/llm.provider', {
|
1687
|
+
method: 'PUT',
|
1688
|
+
headers: {
|
1689
|
+
'Content-Type': 'application/json',
|
1690
|
+
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
1691
|
+
},
|
1692
|
+
body: JSON.stringify({ value: providerValue.toLowerCase() })
|
1693
|
+
})
|
1694
|
+
.then(response => response.json())
|
1695
|
+
.then(data => {
|
1696
|
+
console.log('Provider setting saved to database:', data);
|
1697
|
+
|
1698
|
+
// Optionally show a notification
|
1699
|
+
if (window.ui && window.ui.showMessage) {
|
1700
|
+
window.ui.showMessage(`Provider updated to: ${providerValue}`, 'success', 2000);
|
1701
|
+
}
|
1702
|
+
})
|
1703
|
+
.catch(error => {
|
1704
|
+
console.error('Error saving provider setting to database:', error);
|
1705
|
+
|
1706
|
+
// Show error notification if available
|
1707
|
+
if (window.ui && window.ui.showMessage) {
|
1708
|
+
window.ui.showMessage(`Error updating provider: ${error.message}`, 'error', 3000);
|
1709
|
+
}
|
1710
|
+
});
|
1711
|
+
}
|
1712
|
+
|
1713
|
+
// Research form submission handler
|
1714
|
+
function handleResearchSubmit(event) {
|
1715
|
+
event.preventDefault();
|
1716
|
+
console.log('Research form submitted');
|
1717
|
+
|
1718
|
+
// Disable the submit button to prevent multiple submissions
|
1719
|
+
startBtn.disabled = true;
|
1720
|
+
startBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Starting...';
|
1721
|
+
|
1722
|
+
// Get the selected research mode
|
1723
|
+
const selectedMode = document.querySelector('.mode-option.active');
|
1724
|
+
const mode = selectedMode ? selectedMode.getAttribute('data-mode') : 'quick';
|
1725
|
+
|
1726
|
+
// Get values from form fields
|
1727
|
+
const query = queryInput.value.trim();
|
1728
|
+
const modelProvider = modelProviderSelect ? modelProviderSelect.value : '';
|
1729
|
+
|
1730
|
+
// Get values from hidden inputs for custom dropdowns
|
1731
|
+
const model = document.querySelector('#model_hidden') ?
|
1732
|
+
document.querySelector('#model_hidden').value : '';
|
1733
|
+
const searchEngine = document.querySelector('#search_engine_hidden') ?
|
1734
|
+
document.querySelector('#search_engine_hidden').value : '';
|
1735
|
+
|
1736
|
+
// Get other form values
|
1737
|
+
const customEndpoint = customEndpointInput ? customEndpointInput.value : '';
|
1738
|
+
const iterations = iterationsInput ? parseInt(iterationsInput.value, 10) : 2;
|
1739
|
+
const questionsPerIteration = questionsPerIterationInput ?
|
1740
|
+
parseInt(questionsPerIterationInput.value, 10) : 3;
|
1741
|
+
const enableNotifications = notificationToggle ? notificationToggle.checked : true;
|
1742
|
+
|
1743
|
+
// Validate the query
|
1744
|
+
if (!query) {
|
1745
|
+
// Show error if query is empty
|
1746
|
+
showAlert('Please enter a research query.', 'error');
|
1747
|
+
|
1748
|
+
// Re-enable the button
|
1749
|
+
startBtn.disabled = false;
|
1750
|
+
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
1751
|
+
return;
|
1752
|
+
}
|
1753
|
+
|
1754
|
+
// Prepare the data for submission
|
1755
|
+
const formData = {
|
1756
|
+
query: query,
|
1757
|
+
mode: mode,
|
1758
|
+
model_provider: modelProvider,
|
1759
|
+
model: model,
|
1760
|
+
custom_endpoint: customEndpoint,
|
1761
|
+
search_engine: searchEngine,
|
1762
|
+
iterations: iterations,
|
1763
|
+
questions_per_iteration: questionsPerIteration
|
1764
|
+
};
|
1765
|
+
|
1766
|
+
console.log('Submitting research with data:', formData);
|
1767
|
+
|
1768
|
+
// Get CSRF token from meta tag
|
1769
|
+
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
|
1770
|
+
|
1771
|
+
// Submit the form data to the backend
|
1772
|
+
fetch('/research/api/start_research', {
|
1773
|
+
method: 'POST',
|
1774
|
+
headers: {
|
1775
|
+
'Content-Type': 'application/json',
|
1776
|
+
'X-CSRFToken': csrfToken
|
1777
|
+
},
|
1778
|
+
body: JSON.stringify(formData)
|
1779
|
+
})
|
1780
|
+
.then(response => response.json())
|
1781
|
+
.then(data => {
|
1782
|
+
if (data.status === 'success') {
|
1783
|
+
console.log('Research started successfully:', data);
|
1784
|
+
|
1785
|
+
// Store research preferences in localStorage
|
1786
|
+
localStorage.setItem('lastResearchMode', mode);
|
1787
|
+
localStorage.setItem('lastModelProvider', modelProvider);
|
1788
|
+
localStorage.setItem('lastModel', model);
|
1789
|
+
localStorage.setItem('lastSearchEngine', searchEngine);
|
1790
|
+
localStorage.setItem('enableNotifications', enableNotifications);
|
1791
|
+
|
1792
|
+
// Redirect to the progress page
|
1793
|
+
window.location.href = `/research/progress/${data.research_id}`;
|
1794
|
+
} else {
|
1795
|
+
// Show error message
|
1796
|
+
showAlert(data.message || 'Failed to start research.', 'error');
|
1797
|
+
|
1798
|
+
// Re-enable the button
|
1799
|
+
startBtn.disabled = false;
|
1800
|
+
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
1801
|
+
}
|
1802
|
+
})
|
1803
|
+
.catch(error => {
|
1804
|
+
console.error('Error starting research:', error);
|
1805
|
+
|
1806
|
+
// Show error message
|
1807
|
+
showAlert('An error occurred while starting research. Please try again.', 'error');
|
1808
|
+
|
1809
|
+
// Re-enable the button
|
1810
|
+
startBtn.disabled = false;
|
1811
|
+
startBtn.innerHTML = '<i class="fas fa-rocket"></i> Start Research';
|
1812
|
+
});
|
1813
|
+
}
|
1814
|
+
|
1815
|
+
/**
|
1816
|
+
* Show an alert message
|
1817
|
+
* @param {string} message - The message to show
|
1818
|
+
* @param {string} type - The alert type (success, error, warning, info)
|
1819
|
+
*/
|
1820
|
+
function showAlert(message, type = 'info') {
|
1821
|
+
const alertContainer = document.getElementById('research-alert');
|
1822
|
+
if (!alertContainer) return;
|
1823
|
+
|
1824
|
+
// Clear any existing alerts
|
1825
|
+
alertContainer.innerHTML = '';
|
1826
|
+
|
1827
|
+
// Create the alert element
|
1828
|
+
const alert = document.createElement('div');
|
1829
|
+
alert.className = `alert alert-${type}`;
|
1830
|
+
alert.innerHTML = `
|
1831
|
+
<i class="fas ${type === 'success' ? 'fa-check-circle' :
|
1832
|
+
type === 'error' ? 'fa-exclamation-circle' :
|
1833
|
+
type === 'warning' ? 'fa-exclamation-triangle' :
|
1834
|
+
'fa-info-circle'}"></i>
|
1835
|
+
${message}
|
1836
|
+
<span class="alert-close">×</span>
|
1837
|
+
`;
|
1838
|
+
|
1839
|
+
// Add click handler for the close button
|
1840
|
+
const closeBtn = alert.querySelector('.alert-close');
|
1841
|
+
if (closeBtn) {
|
1842
|
+
closeBtn.addEventListener('click', () => {
|
1843
|
+
alert.remove();
|
1844
|
+
alertContainer.style.display = 'none';
|
1845
|
+
});
|
1846
|
+
}
|
1847
|
+
|
1848
|
+
// Add to the container and show it
|
1849
|
+
alertContainer.appendChild(alert);
|
1850
|
+
alertContainer.style.display = 'block';
|
1851
|
+
|
1852
|
+
// Auto-hide after 5 seconds
|
1853
|
+
setTimeout(() => {
|
1854
|
+
if (alertContainer.contains(alert)) {
|
1855
|
+
alert.remove();
|
1856
|
+
if (alertContainer.children.length === 0) {
|
1857
|
+
alertContainer.style.display = 'none';
|
1858
|
+
}
|
1859
|
+
}
|
1860
|
+
}, 5000);
|
1861
|
+
}
|
1862
|
+
|
1863
|
+
// Initialize research component when DOM is loaded
|
1864
|
+
document.addEventListener('DOMContentLoaded', initializeResearch);
|
1865
|
+
})();
|