local-deep-research 0.1.26__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. local_deep_research/__init__.py +23 -22
  2. local_deep_research/__main__.py +16 -0
  3. local_deep_research/advanced_search_system/__init__.py +7 -0
  4. local_deep_research/advanced_search_system/filters/__init__.py +8 -0
  5. local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
  6. local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
  7. local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
  8. local_deep_research/advanced_search_system/findings/repository.py +452 -0
  9. local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
  10. local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
  11. local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
  12. local_deep_research/advanced_search_system/questions/__init__.py +1 -0
  13. local_deep_research/advanced_search_system/questions/base_question.py +64 -0
  14. local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
  15. local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
  16. local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
  17. local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
  18. local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
  19. local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
  20. local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
  21. local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
  22. local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
  23. local_deep_research/advanced_search_system/tools/__init__.py +1 -0
  24. local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
  25. local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
  26. local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
  27. local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
  28. local_deep_research/api/__init__.py +5 -5
  29. local_deep_research/api/research_functions.py +96 -84
  30. local_deep_research/app.py +8 -0
  31. local_deep_research/citation_handler.py +25 -16
  32. local_deep_research/{config.py → config/config_files.py} +102 -110
  33. local_deep_research/config/llm_config.py +472 -0
  34. local_deep_research/config/search_config.py +77 -0
  35. local_deep_research/defaults/__init__.py +10 -5
  36. local_deep_research/defaults/main.toml +2 -2
  37. local_deep_research/defaults/search_engines.toml +60 -34
  38. local_deep_research/main.py +121 -19
  39. local_deep_research/migrate_db.py +147 -0
  40. local_deep_research/report_generator.py +72 -44
  41. local_deep_research/search_system.py +147 -283
  42. local_deep_research/setup_data_dir.py +35 -0
  43. local_deep_research/test_migration.py +178 -0
  44. local_deep_research/utilities/__init__.py +0 -0
  45. local_deep_research/utilities/db_utils.py +49 -0
  46. local_deep_research/{utilties → utilities}/enums.py +2 -2
  47. local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
  48. local_deep_research/utilities/search_utilities.py +242 -0
  49. local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
  50. local_deep_research/web/__init__.py +0 -1
  51. local_deep_research/web/app.py +86 -1709
  52. local_deep_research/web/app_factory.py +289 -0
  53. local_deep_research/web/database/README.md +70 -0
  54. local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
  55. local_deep_research/web/database/migrations.py +447 -0
  56. local_deep_research/web/database/models.py +117 -0
  57. local_deep_research/web/database/schema_upgrade.py +107 -0
  58. local_deep_research/web/models/database.py +294 -0
  59. local_deep_research/web/models/settings.py +94 -0
  60. local_deep_research/web/routes/api_routes.py +559 -0
  61. local_deep_research/web/routes/history_routes.py +354 -0
  62. local_deep_research/web/routes/research_routes.py +715 -0
  63. local_deep_research/web/routes/settings_routes.py +1592 -0
  64. local_deep_research/web/services/research_service.py +947 -0
  65. local_deep_research/web/services/resource_service.py +149 -0
  66. local_deep_research/web/services/settings_manager.py +669 -0
  67. local_deep_research/web/services/settings_service.py +187 -0
  68. local_deep_research/web/services/socket_service.py +210 -0
  69. local_deep_research/web/static/css/custom_dropdown.css +277 -0
  70. local_deep_research/web/static/css/settings.css +1223 -0
  71. local_deep_research/web/static/css/styles.css +525 -48
  72. local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
  73. local_deep_research/web/static/js/components/detail.js +348 -0
  74. local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
  75. local_deep_research/web/static/js/components/fallback/ui.js +215 -0
  76. local_deep_research/web/static/js/components/history.js +487 -0
  77. local_deep_research/web/static/js/components/logpanel.js +949 -0
  78. local_deep_research/web/static/js/components/progress.js +1107 -0
  79. local_deep_research/web/static/js/components/research.js +1865 -0
  80. local_deep_research/web/static/js/components/results.js +766 -0
  81. local_deep_research/web/static/js/components/settings.js +3981 -0
  82. local_deep_research/web/static/js/components/settings_sync.js +106 -0
  83. local_deep_research/web/static/js/main.js +226 -0
  84. local_deep_research/web/static/js/services/api.js +253 -0
  85. local_deep_research/web/static/js/services/audio.js +31 -0
  86. local_deep_research/web/static/js/services/formatting.js +119 -0
  87. local_deep_research/web/static/js/services/pdf.js +622 -0
  88. local_deep_research/web/static/js/services/socket.js +882 -0
  89. local_deep_research/web/static/js/services/ui.js +546 -0
  90. local_deep_research/web/templates/base.html +72 -0
  91. local_deep_research/web/templates/components/custom_dropdown.html +47 -0
  92. local_deep_research/web/templates/components/log_panel.html +32 -0
  93. local_deep_research/web/templates/components/mobile_nav.html +22 -0
  94. local_deep_research/web/templates/components/settings_form.html +299 -0
  95. local_deep_research/web/templates/components/sidebar.html +21 -0
  96. local_deep_research/web/templates/pages/details.html +73 -0
  97. local_deep_research/web/templates/pages/history.html +51 -0
  98. local_deep_research/web/templates/pages/progress.html +57 -0
  99. local_deep_research/web/templates/pages/research.html +139 -0
  100. local_deep_research/web/templates/pages/results.html +59 -0
  101. local_deep_research/web/templates/settings_dashboard.html +78 -192
  102. local_deep_research/web/utils/__init__.py +0 -0
  103. local_deep_research/web/utils/formatters.py +76 -0
  104. local_deep_research/web_search_engines/engines/full_search.py +18 -16
  105. local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
  106. local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
  107. local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
  108. local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
  109. local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
  110. local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
  111. local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
  112. local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
  113. local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
  114. local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
  115. local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
  116. local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
  117. local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
  118. local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
  119. local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
  120. local_deep_research/web_search_engines/search_engine_base.py +174 -99
  121. local_deep_research/web_search_engines/search_engine_factory.py +192 -102
  122. local_deep_research/web_search_engines/search_engines_config.py +22 -15
  123. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
  124. local_deep_research-0.2.0.dist-info/RECORD +135 -0
  125. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
  126. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
  127. local_deep_research/defaults/llm_config.py +0 -338
  128. local_deep_research/utilties/search_utilities.py +0 -114
  129. local_deep_research/web/static/js/app.js +0 -3763
  130. local_deep_research/web/templates/api_keys_config.html +0 -82
  131. local_deep_research/web/templates/collections_config.html +0 -90
  132. local_deep_research/web/templates/index.html +0 -348
  133. local_deep_research/web/templates/llm_config.html +0 -120
  134. local_deep_research/web/templates/main_config.html +0 -89
  135. local_deep_research/web/templates/search_engines_config.html +0 -154
  136. local_deep_research/web/templates/settings.html +0 -519
  137. local_deep_research-0.1.26.dist-info/RECORD +0 -61
  138. local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
  139. /local_deep_research/{utilties → config}/__init__.py +0 -0
  140. {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,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">&times;</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
+ })();