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,3981 @@
1
+ /**
2
+ * Settings component for managing application settings
3
+ */
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // DOM elements and global variables
8
+ let settingsForm;
9
+ let settingsContent;
10
+ let settingsSearch;
11
+ let settingsTabs;
12
+ let settingsAlert;
13
+ let resetButton;
14
+ let rawConfigToggle;
15
+ let rawConfigSection;
16
+ let rawConfigEditor;
17
+ let originalSettings = {};
18
+ let allSettings = [];
19
+ let activeTab = 'all';
20
+ let saveTimer = null;
21
+ let pendingSaves = new Set();
22
+
23
+ // Model and search engine dropdown variables
24
+ let modelOptions = [];
25
+ let searchEngineOptions = [];
26
+
27
+ // Store save timers for each setting key
28
+ let saveTimers = {};
29
+ let pendingSaveData = {};
30
+
31
+ // Cache keys - same as research.js for shared caching
32
+ const CACHE_KEYS = {
33
+ MODELS: 'deepResearch.availableModels',
34
+ SEARCH_ENGINES: 'deepResearch.searchEngines',
35
+ CACHE_TIMESTAMP: 'deepResearch.cacheTimestamp'
36
+ };
37
+
38
+ // Cache expiration time (24 hours in milliseconds)
39
+ const CACHE_EXPIRATION = 24 * 60 * 60 * 1000;
40
+
41
+ /**
42
+ * Helper function to generate custom dropdown HTML (similar to Jinja macro)
43
+ * @param {object} params - Parameters for the dropdown
44
+ * @returns {string} HTML string for the custom dropdown input part
45
+ */
46
+ function renderCustomDropdownHTML(params) {
47
+ // Basic structure with input and list container
48
+ let dropdownHTML = `
49
+ <div class="custom-dropdown" id="${params.dropdown_id}">
50
+ <input type="text"
51
+ id="${params.input_id}"
52
+ data-key="${params.data_setting_key || params.input_id}"
53
+ class="custom-dropdown-input"
54
+ placeholder="${params.placeholder}"
55
+ autocomplete="off"
56
+ aria-haspopup="listbox">
57
+ <!-- Hidden input that will be included in form submission -->
58
+ <input type="hidden" name="${params.input_id}" id="${params.input_id}_hidden" value="">
59
+ <div class="custom-dropdown-list" id="${params.dropdown_id}-list"></div>
60
+ </div>
61
+ `;
62
+
63
+ // Add refresh button if needed
64
+ const refreshButtonHTML = params.show_refresh ? `
65
+ <button type="button"
66
+ class="custom-dropdown-refresh-btn dropdown-refresh-button"
67
+ id="${params.input_id}-refresh"
68
+ aria-label="${params.refresh_aria_label || 'Refresh options'}">
69
+ <i class="fas fa-sync-alt"></i>
70
+ </button>
71
+ ` : '';
72
+
73
+ // Wrap with refresh container if needed
74
+ if (params.show_refresh) {
75
+ dropdownHTML = `
76
+ <div class="custom-dropdown-with-refresh">
77
+ ${dropdownHTML} ${refreshButtonHTML}
78
+ </div>
79
+ `;
80
+ }
81
+
82
+ // Note: This returns only the input element part. Label and help text are handled outside.
83
+ return dropdownHTML;
84
+ }
85
+
86
+ /**
87
+ * Set up refresh buttons for model and search engine dropdowns
88
+ */
89
+ function setupRefreshButtons() {
90
+ console.log('Setting up refresh buttons...');
91
+
92
+ // Handle model refresh button
93
+ const modelRefreshBtn = document.getElementById('llm.model-refresh');
94
+ if (modelRefreshBtn) {
95
+ console.log('Found and set up model refresh button:', modelRefreshBtn.id);
96
+ modelRefreshBtn.addEventListener('click', function() {
97
+ const icon = modelRefreshBtn.querySelector('i');
98
+ if (icon) icon.className = 'fas fa-spinner fa-spin';
99
+ modelRefreshBtn.classList.add('loading');
100
+
101
+ // Reset the initialization flag to allow reinitializing the dropdown
102
+ window.modelDropdownsInitialized = false;
103
+
104
+ // Force refresh models and reinitialize
105
+ fetchModelProviders(true)
106
+ .then(() => {
107
+ if (icon) icon.className = 'fas fa-sync-alt';
108
+ modelRefreshBtn.classList.remove('loading');
109
+
110
+ // Re-initialize model dropdowns with the new data
111
+ initializeModelDropdowns();
112
+
113
+ // Show success message
114
+ showAlert('Model list refreshed', 'success');
115
+ })
116
+ .catch(error => {
117
+ console.error('Error refreshing models:', error);
118
+ if (icon) icon.className = 'fas fa-sync-alt';
119
+ modelRefreshBtn.classList.remove('loading');
120
+ showAlert('Failed to refresh models', 'error');
121
+ });
122
+ });
123
+ } else {
124
+ console.log('Could not find model refresh button');
125
+ }
126
+
127
+ // Handle search engine refresh button
128
+ const searchEngineRefreshBtn = document.getElementById('search.tool-refresh');
129
+ if (searchEngineRefreshBtn) {
130
+ console.log('Found and set up search engine refresh button:', searchEngineRefreshBtn.id);
131
+ searchEngineRefreshBtn.addEventListener('click', function() {
132
+ const icon = searchEngineRefreshBtn.querySelector('i');
133
+ if (icon) icon.className = 'fas fa-spinner fa-spin';
134
+ searchEngineRefreshBtn.classList.add('loading');
135
+
136
+ // Reset the initialization flag to allow reinitializing the dropdown
137
+ window.searchEngineDropdownInitialized = false;
138
+
139
+ // Force refresh search engines and reinitialize
140
+ fetchSearchEngines(true)
141
+ .then(() => {
142
+ if (icon) icon.className = 'fas fa-sync-alt';
143
+ searchEngineRefreshBtn.classList.remove('loading');
144
+
145
+ // Re-initialize search engine dropdowns with the new data
146
+ initializeSearchEngineDropdowns();
147
+
148
+ // Show success message
149
+ showAlert('Search engine list refreshed', 'success');
150
+ })
151
+ .catch(error => {
152
+ console.error('Error refreshing search engines:', error);
153
+ if (icon) icon.className = 'fas fa-sync-alt';
154
+ searchEngineRefreshBtn.classList.remove('loading');
155
+ showAlert('Failed to refresh search engines', 'error');
156
+ });
157
+ });
158
+ } else {
159
+ console.log('Could not find search engine refresh button');
160
+
161
+ // Try to create refresh button if it doesn't exist for search engine
162
+ createRefreshButton('search.tool', fetchSearchEngines);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Cache data in localStorage with timestamp
168
+ * @param {string} key - The cache key
169
+ * @param {Object} data - The data to cache
170
+ */
171
+ function cacheData(key, data) {
172
+ try {
173
+ // Store the data
174
+ localStorage.setItem(key, JSON.stringify(data));
175
+
176
+ // Update or set the timestamp
177
+ let timestamps;
178
+ try {
179
+ timestamps = JSON.parse(localStorage.getItem(CACHE_KEYS.CACHE_TIMESTAMP) || '{}');
180
+ // Ensure timestamps is an object, not a number or other type
181
+ if (typeof timestamps !== 'object' || timestamps === null) {
182
+ timestamps = {};
183
+ }
184
+ } catch (e) {
185
+ // If parsing fails, start with a new object
186
+ timestamps = {};
187
+ }
188
+
189
+ timestamps[key] = Date.now();
190
+ localStorage.setItem(CACHE_KEYS.CACHE_TIMESTAMP, JSON.stringify(timestamps));
191
+
192
+ console.log(`Cached data for ${key}`);
193
+ } catch (error) {
194
+ console.error('Error caching data:', error);
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Get cached data if it exists and is not expired
200
+ * @param {string} key - The cache key
201
+ * @returns {Object|null} The cached data or null if not found or expired
202
+ */
203
+ function getCachedData(key) {
204
+ try {
205
+ // Get timestamps
206
+ let timestamps;
207
+ try {
208
+ timestamps = JSON.parse(localStorage.getItem(CACHE_KEYS.CACHE_TIMESTAMP) || '{}');
209
+ // Ensure timestamps is an object, not a number or other type
210
+ if (typeof timestamps !== 'object' || timestamps === null) {
211
+ timestamps = {};
212
+ }
213
+ } catch (e) {
214
+ // If parsing fails, start with an empty object
215
+ timestamps = {};
216
+ }
217
+
218
+ const timestamp = timestamps[key];
219
+
220
+ // Check if data exists and is not expired
221
+ if (timestamp && (Date.now() - timestamp < CACHE_EXPIRATION)) {
222
+ try {
223
+ const data = JSON.parse(localStorage.getItem(key));
224
+ return data;
225
+ } catch (e) {
226
+ console.error('Error parsing cached data:', e);
227
+ return null;
228
+ }
229
+ }
230
+ } catch (error) {
231
+ console.error('Error getting cached data:', error);
232
+ }
233
+ return null;
234
+ }
235
+
236
+ /**
237
+ * Initialize auto-save handlers for settings inputs
238
+ */
239
+ function initAutoSaveHandlers() {
240
+ // Only run this for the main settings dashboard
241
+ if (!settingsContent) return;
242
+
243
+ // Get all inputs in settings form
244
+ const inputs = settingsForm.querySelectorAll('input, textarea, select');
245
+
246
+ // Set up event handlers for each input
247
+ inputs.forEach(input => {
248
+ // Skip if this is a button or submit input
249
+ if (input.type === 'button' || input.type === 'submit') return;
250
+
251
+ // Set data-key attribute from name if not already set
252
+ if (!input.getAttribute('data-key') && input.getAttribute('name')) {
253
+ input.setAttribute('data-key', input.getAttribute('name'));
254
+ }
255
+
256
+ // Remove existing listeners to avoid duplicates
257
+ input.removeEventListener('input', handleInputChange);
258
+ input.removeEventListener('keydown', handleInputChange);
259
+ input.removeEventListener('blur', handleInputChange);
260
+ input.removeEventListener('change', handleInputChange);
261
+
262
+ // For checkboxes, we use change event
263
+ if (input.type === 'checkbox') {
264
+ input.addEventListener('change', function(e) {
265
+ // For checkboxes, pass custom event type parameter to avoid issues
266
+ handleInputChange(e, 'change');
267
+ });
268
+ }
269
+ // For selects, we use change event
270
+ else if (input.tagName.toLowerCase() === 'select') {
271
+ input.addEventListener('change', function(e) {
272
+ // Create a custom parameter instead of modifying e.type
273
+ handleInputChange(e, 'change');
274
+ });
275
+
276
+ input.addEventListener('blur', function(e) {
277
+ // Create a custom parameter instead of modifying e.type
278
+ handleInputChange(e, 'blur');
279
+ });
280
+ }
281
+ // For text, number, etc. we monitor for changes but only save
282
+ // on blur or Enter. We don't do anything with custom drop-downs
283
+ // (we use the hidden input instead).
284
+ else if (!input.classList.contains("custom-dropdown-input")) {
285
+ // Listen for input events to track changes and validate in real-time
286
+ input.addEventListener('input', function(e) {
287
+ // Create a custom parameter instead of modifying e.type
288
+ handleInputChange(e, 'input');
289
+ });
290
+
291
+ // Handle Enter key press for immediate saving
292
+ input.addEventListener('keydown', function(e) {
293
+ if (e.key === 'Enter') {
294
+ // Create a custom parameter instead of modifying e.type
295
+ handleInputChange(e, 'keydown');
296
+ }
297
+ });
298
+
299
+ // Save on blur if changes were made.
300
+ if (input.id.endsWith("_hidden")) {
301
+ // We can't use this for custom dropdowns, because it
302
+ // will fire before the value has been changed, causing
303
+ // it to read the wrong value.
304
+ input.addEventListener('change', function(e) {
305
+ // Create a custom parameter instead of modifying e.type
306
+ handleInputChange(e, 'change');
307
+ });
308
+ } else {
309
+ input.addEventListener('blur', function(e) {
310
+ // Create a custom parameter instead of modifying e.type
311
+ handleInputChange(e, 'blur');
312
+ });
313
+ }
314
+ }
315
+ });
316
+
317
+ // Set up special handlers for JSON property controls
318
+ const jsonPropertyControls = settingsForm.querySelectorAll('.json-property-control');
319
+
320
+ jsonPropertyControls.forEach(control => {
321
+ // Remove existing listeners
322
+ control.removeEventListener('change', updateJsonFromControls);
323
+ control.removeEventListener('input', updateJsonFromControls);
324
+ control.removeEventListener('keydown', updateJsonFromControls);
325
+ control.removeEventListener('blur', updateJsonFromControls);
326
+
327
+ if (control.type === 'checkbox') {
328
+ control.addEventListener('change', function(e) {
329
+ updateJsonFromControls(control, true); // true = force save
330
+ });
331
+ } else {
332
+ control.addEventListener('input', function(e) {
333
+ updateJsonFromControls(control, false); // false = don't save yet
334
+ });
335
+
336
+ // Handle Enter key for JSON property controls
337
+ control.addEventListener('keydown', function(e) {
338
+ if (e.key === 'Enter' && !e.shiftKey) {
339
+ e.preventDefault();
340
+ updateJsonFromControls(control, true); // true = force save
341
+ control.blur();
342
+ }
343
+ });
344
+
345
+ control.addEventListener('blur', function(e) {
346
+ updateJsonFromControls(control, true); // true = force save
347
+ });
348
+ }
349
+ });
350
+
351
+ // If the raw JSON editor is visible, set up its event handlers
352
+ if (rawConfigEditor) {
353
+ rawConfigEditor.addEventListener('input', handleRawJsonInput);
354
+ rawConfigEditor.addEventListener('blur', function(e) {
355
+ if (rawConfigEditor.getAttribute('data-modified') === 'true') {
356
+ handleRawJsonInput(e, true); // Force save on blur
357
+ }
358
+ });
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Handle input change for autosave
364
+ * @param {Event} e - The input change event
365
+ * @param {string} [customEventType] - Optional event type parameter
366
+ */
367
+ function handleInputChange(e, customEventType) {
368
+ // --- MODIFICATION START: Simplified handleInputChange ---
369
+ const input = e.target;
370
+ const eventType = customEventType || e.type;
371
+ const key = input.dataset.key || input.name; // Get key using data-key first
372
+
373
+ if (!key || input.disabled) return;
374
+
375
+ let value;
376
+ let shouldSaveImmediately = false;
377
+
378
+ // Handle hidden inputs for custom dropdowns
379
+ if (input.type === 'hidden' && input.id.endsWith('_hidden')) {
380
+ value = input.value;
381
+ shouldSaveImmediately = true; // Save immediately on hidden input change
382
+ console.log(`[Hidden Input Change] Key: ${key}, Value: ${value}`);
383
+ }
384
+ // Handle checkboxes
385
+ else if (input.type === 'checkbox') {
386
+ value = input.checked;
387
+ shouldSaveImmediately = true; // Checkboxes save immediately
388
+ }
389
+ // Handle standard selects
390
+ else if (input.tagName.toLowerCase() === 'select') {
391
+ value = input.value;
392
+ // Save on change or blur if changed
393
+ if (eventType === 'change' || eventType === 'blur') {
394
+ shouldSaveImmediately = true;
395
+ }
396
+ }
397
+ // Handle range/slider (save on change/input or blur)
398
+ else if (input.type === 'range') {
399
+ value = input.value;
400
+ if (eventType === 'change' || eventType === 'input' || eventType === 'blur') {
401
+ shouldSaveImmediately = true;
402
+ }
403
+ }
404
+ // Handle other inputs (text, number, textarea) - Save on Enter or Blur
405
+ else {
406
+ value = input.value;
407
+ // Basic validation for number
408
+ if (input.type === 'number') {
409
+ try {
410
+ const numValue = parseFloat(value);
411
+ const min = input.min ? parseFloat(input.min) : null;
412
+ const max = input.max ? parseFloat(input.max) : null;
413
+ if ((min !== null && numValue < min) || (max !== null && numValue > max)) {
414
+ markInvalidInput(input, `Value must be between ${min ?? '-∞'} and ${max ?? '∞'}`);
415
+ return; // Don't save invalid number
416
+ }
417
+ value = numValue; // Use parsed number
418
+ } catch {
419
+ markInvalidInput(input, 'Invalid number');
420
+ return;
421
+ }
422
+ }
423
+ // Save on Enter or Blur
424
+ if ((eventType === 'keydown' && e.key === 'Enter' && !e.shiftKey) || eventType === 'blur') {
425
+ shouldSaveImmediately = true;
426
+ if (eventType === 'keydown') e.preventDefault(); // Prevent form submission on enter
427
+ }
428
+ }
429
+
430
+ // Clear previous errors
431
+ markInvalidInput(input, null);
432
+
433
+ // Compare with original value
434
+ const originalValue = originalSettings.hasOwnProperty(key) ? originalSettings[key] : undefined;
435
+ const hasChanged = !areValuesEqual(value, originalValue);
436
+
437
+ console.log(`[Input Change] Key: ${key}, Value: ${value}, Event: ${eventType}, Changed: ${hasChanged}, Save Immediately: ${shouldSaveImmediately}`);
438
+
439
+ if (hasChanged) {
440
+ // Mark parent item as modified
441
+ const item = input.closest('.settings-item');
442
+ if (item) item.classList.add('settings-modified');
443
+
444
+ // Save if needed
445
+ if (shouldSaveImmediately) {
446
+ const formData = { [key]: value };
447
+ submitSettingsData(formData, input); // Direct submit might be better than debouncing here
448
+
449
+ // If saved on Enter, blur the input
450
+ if (eventType === 'keydown' && e.key === 'Enter') {
451
+ input.blur();
452
+ }
453
+ }
454
+ } else {
455
+ // If blur event and no changes, remove modified indicator maybe?
456
+ if (eventType === 'blur') {
457
+ const item = input.closest('.settings-item');
458
+ if (item) item.classList.remove('settings-modified');
459
+ }
460
+ }
461
+ // --- MODIFICATION END ---
462
+ }
463
+
464
+ /**
465
+ * Compare two values for equality, handling different types
466
+ * @param {any} value1 - First value
467
+ * @param {any} value2 - Second value
468
+ * @returns {boolean} - Whether the values are equal
469
+ */
470
+ function areValuesEqual(value1, value2) {
471
+ // Handle null/undefined
472
+ if (value1 === null && value2 === null) return true;
473
+ if (value1 === undefined && value2 === undefined) return true;
474
+ if (value1 === null && value2 === undefined) return true;
475
+ if (value1 === undefined && value2 === null) return true;
476
+
477
+ // If one is null/undefined but the other isn't
478
+ if ((value1 === null || value1 === undefined) && (value2 !== null && value2 !== undefined)) return false;
479
+ if ((value2 === null || value2 === undefined) && (value1 !== null && value1 !== undefined)) return false;
480
+
481
+ // Handle different types
482
+ const type1 = typeof value1;
483
+ const type2 = typeof value2;
484
+
485
+ // If types are different, they're not equal
486
+ // Except for numbers and strings that might be equivalent
487
+ if (type1 !== type2) {
488
+ // Special case for numeric strings vs numbers
489
+ if ((type1 === 'number' && type2 === 'string') || (type1 === 'string' && type2 === 'number')) {
490
+ return String(value1) === String(value2);
491
+ }
492
+ return false;
493
+ }
494
+
495
+ // Handle objects (including arrays)
496
+ if (type1 === 'object') {
497
+ // Handle arrays
498
+ if (Array.isArray(value1) && Array.isArray(value2)) {
499
+ if (value1.length !== value2.length) return false;
500
+ return JSON.stringify(value1) === JSON.stringify(value2);
501
+ }
502
+
503
+ // Handle objects
504
+ return JSON.stringify(value1) === JSON.stringify(value2);
505
+ }
506
+
507
+ // Handle primitives
508
+ return value1 === value2;
509
+ }
510
+
511
+ /**
512
+ * Handle input to raw JSON fields for validation
513
+ * @param {Event} e - The input event
514
+ */
515
+ function handleRawJsonInput(e) {
516
+ const input = e.target;
517
+
518
+ try {
519
+ // Try to parse the JSON
520
+ JSON.parse(input.value);
521
+
522
+ // Valid JSON, remove any error styling
523
+ const settingsItem = input.closest('.settings-item');
524
+ if (settingsItem) {
525
+ settingsItem.classList.remove('settings-error');
526
+
527
+ // Remove any error message
528
+ const errorMsg = settingsItem.querySelector('.settings-error-message');
529
+ if (errorMsg) {
530
+ errorMsg.remove();
531
+ }
532
+ }
533
+ input.classList.remove('settings-error');
534
+ } catch (e) {
535
+ // Invalid JSON, mark as error but don't prevent typing
536
+ input.classList.add('settings-error');
537
+
538
+ // Don't show error message while actively typing, only on blur
539
+ input.addEventListener('blur', function onBlur() {
540
+ try {
541
+ JSON.parse(input.value);
542
+ // Valid JSON on blur, clear any error
543
+ markInvalidInput(input, null);
544
+ } catch (e) {
545
+ // Still invalid on blur, show error
546
+ markInvalidInput(input, 'Invalid JSON format: ' + e.message);
547
+ }
548
+ // Remove this blur handler after it runs once
549
+ input.removeEventListener('blur', onBlur);
550
+ }, { once: true });
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Mark an input as invalid with error styling
556
+ * @param {HTMLElement} input - The input element
557
+ * @param {string|null} errorMessage - The error message or null to clear error
558
+ */
559
+ function markInvalidInput(input, errorMessage) {
560
+ const settingsItem = input.closest('.settings-item');
561
+ if (!settingsItem) return;
562
+
563
+ // Clear existing error message
564
+ const existingMsg = settingsItem.querySelector('.settings-error-message');
565
+ if (existingMsg) {
566
+ existingMsg.remove();
567
+ }
568
+
569
+ if (errorMessage) {
570
+ // Add error class
571
+ settingsItem.classList.add('settings-error');
572
+ input.classList.add('settings-error');
573
+
574
+ // Create error message
575
+ const errorMsg = document.createElement('div');
576
+ errorMsg.className = 'settings-error-message';
577
+ errorMsg.textContent = errorMessage;
578
+ settingsItem.appendChild(errorMsg);
579
+ } else {
580
+ // Remove error class
581
+ settingsItem.classList.remove('settings-error');
582
+ input.classList.remove('settings-error');
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Schedule a debounced save operation
588
+ * @param {Object} formData - The form data to save
589
+ * @param {HTMLElement} sourceElement - The element that triggered the save
590
+ */
591
+ function scheduleSave(formData, sourceElement) {
592
+ // Merge the form data with any existing pending save data
593
+ Object.entries(formData).forEach(([key, value]) => {
594
+ pendingSaveData[key] = value;
595
+
596
+ // Clear any existing timer for this specific key
597
+ if (saveTimers[key]) {
598
+ clearTimeout(saveTimers[key]);
599
+ }
600
+
601
+ // Set loading state on the source element
602
+ if (sourceElement) {
603
+ sourceElement.classList.add('saving');
604
+ }
605
+
606
+ // Create a new timer for this specific key
607
+ saveTimers[key] = setTimeout(() => {
608
+ // Create a single-key form data object with just this setting
609
+ const singleSettingData = { [key]: pendingSaveData[key] };
610
+
611
+ // Submit just this setting's data
612
+ submitSettingsData(singleSettingData, sourceElement);
613
+
614
+ // Clear this key from pending saves
615
+ delete pendingSaveData[key];
616
+ delete saveTimers[key];
617
+ }, 800); // 800ms debounce
618
+ });
619
+ }
620
+
621
+ /**
622
+ * Initialize expanded JSON controls
623
+ * This sets up event listeners for the individual form controls that represent JSON properties
624
+ */
625
+ function initExpandedJsonControls() {
626
+ // Find all JSON property controls
627
+ document.querySelectorAll('.json-property-control').forEach(control => {
628
+ // When the control changes, update the hidden JSON field
629
+ control.addEventListener('change', function() {
630
+ updateJsonFromControls(this);
631
+ });
632
+
633
+ // For text and number inputs, also listen for input events
634
+ if (control.tagName === 'INPUT' && (control.type === 'text' || control.type === 'number')) {
635
+ control.addEventListener('input', function() {
636
+ updateJsonFromControls(this);
637
+ });
638
+ }
639
+ });
640
+ }
641
+
642
+ /**
643
+ * Update JSON data from individual controls
644
+ * @param {HTMLElement} changedControl - The control that triggered the update
645
+ * @param {boolean} forceSave - Whether to force an update to the server
646
+ */
647
+ function updateJsonFromControls(changedControl, forceSave = false) {
648
+ const parentKey = changedControl.dataset.parentKey;
649
+ const property = changedControl.dataset.property;
650
+
651
+ if (!parentKey || !property) return;
652
+
653
+ // Find all controls for this parent JSON
654
+ const controls = document.querySelectorAll(`.json-property-control[data-parent-key="${parentKey}"]`);
655
+
656
+ // Create an object to hold the JSON data
657
+ const jsonData = {};
658
+
659
+ // Populate the object with values from all controls
660
+ controls.forEach(control => {
661
+ const prop = control.dataset.property;
662
+ let value = null;
663
+
664
+ if (control.type === 'checkbox') {
665
+ value = control.checked;
666
+ } else if (control.type === 'number') {
667
+ value = parseFloat(control.value);
668
+ } else if (control.tagName === 'SELECT') {
669
+ value = control.value;
670
+ } else {
671
+ value = control.value;
672
+ // Try to convert to number if it's numeric
673
+ if (!isNaN(value) && value !== '') {
674
+ value = parseFloat(value);
675
+ }
676
+ }
677
+
678
+ jsonData[prop] = value;
679
+ });
680
+
681
+ // Find the hidden input that stores the original JSON
682
+ const originalInput = document.getElementById(`${parentKey.replace(/\./g, '-')}_original`);
683
+ let originalJson = {};
684
+
685
+ if (originalInput) {
686
+ // Get the original JSON
687
+ try {
688
+ originalJson = JSON.parse(originalInput.value);
689
+ } catch (e) {
690
+ console.error('Error parsing original JSON:', e);
691
+ // Create an empty object if parsing fails
692
+ originalJson = {};
693
+ }
694
+ }
695
+
696
+ // Check if there's actually a change before saving
697
+ const hasChanged = !areObjectsEqual(jsonData, originalJson);
698
+
699
+ // Mark the parent container as modified if there's a change
700
+ const settingItem = changedControl.closest('.settings-item');
701
+ if (settingItem && hasChanged) {
702
+ settingItem.classList.add('settings-modified');
703
+ }
704
+
705
+ // Update the UI even if we're not saving to the server
706
+ if (originalInput) {
707
+ // Update the original JSON with new values
708
+ Object.assign(originalJson, jsonData);
709
+ originalInput.value = JSON.stringify(originalJson);
710
+ }
711
+
712
+ // Also update any textarea that might display this JSON
713
+ const jsonTextarea = document.getElementById(parentKey.replace(/\./g, '-'));
714
+ if (jsonTextarea && jsonTextarea.tagName === 'TEXTAREA') {
715
+ jsonTextarea.value = JSON.stringify(jsonData, null, 2);
716
+ }
717
+
718
+ // If we have a raw config editor, update it as well
719
+ if (rawConfigEditor) {
720
+ try {
721
+ const rawConfig = JSON.parse(rawConfigEditor.value);
722
+ const parts = parentKey.split('.');
723
+ const prefix = parts[0]; // app, llm, search, etc.
724
+
725
+ if (rawConfig[prefix]) {
726
+ const subKey = parentKey.substring(prefix.length + 1);
727
+ rawConfig[prefix][subKey] = jsonData;
728
+ rawConfigEditor.value = JSON.stringify(rawConfig, null, 2);
729
+ }
730
+ } catch (e) {
731
+ console.log('Error updating raw config:', e);
732
+ }
733
+ }
734
+
735
+ // Only save to the server if forced or there's a change
736
+ if ((forceSave && hasChanged) || (changedControl.type === 'checkbox' && hasChanged)) {
737
+ // Auto-save this setting
738
+ const formData = {};
739
+ formData[parentKey] = jsonData;
740
+ submitSettingsData(formData, changedControl);
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Compare two objects for equality
746
+ * @param {Object} obj1 - First object
747
+ * @param {Object} obj2 - Second object
748
+ * @returns {boolean} - Whether the objects are equal
749
+ */
750
+ function areObjectsEqual(obj1, obj2) {
751
+ // Get the keys of both objects
752
+ const keys1 = Object.keys(obj1);
753
+ const keys2 = Object.keys(obj2);
754
+
755
+ // If the number of keys is different, they're not equal
756
+ if (keys1.length !== keys2.length) return false;
757
+
758
+ // Check each key/value pair
759
+ for (const key of keys1) {
760
+ // If the key doesn't exist in obj2, not equal
761
+ if (!obj2.hasOwnProperty(key)) return false;
762
+
763
+ // If the values are not equal, not equal
764
+ if (!areValuesEqual(obj1[key], obj2[key])) return false;
765
+ }
766
+
767
+ // All keys/values match
768
+ return true;
769
+ }
770
+
771
+ /**
772
+ * Initialize specific settings page form handlers
773
+ */
774
+ function initSpecificSettingsForm() {
775
+ // Get the form ID to determine which specific page we're on
776
+ const specificForm = document.getElementById('report-settings-form') ||
777
+ document.getElementById('llm-settings-form') ||
778
+ document.getElementById('search-settings-form') ||
779
+ document.getElementById('app-settings-form');
780
+
781
+ if (specificForm) {
782
+ // Add form submission handler
783
+ specificForm.addEventListener('submit', function(e) {
784
+ // Handle checkbox values
785
+ const checkboxes = specificForm.querySelectorAll('input[type="checkbox"]');
786
+ checkboxes.forEach(checkbox => {
787
+ if (!checkbox.checked) {
788
+ // Create a hidden input for unchecked boxes
789
+ const hidden = document.createElement('input');
790
+ hidden.type = 'hidden';
791
+ hidden.name = checkbox.name;
792
+ hidden.value = 'false';
793
+ specificForm.appendChild(hidden);
794
+ }
795
+ });
796
+
797
+ // Check for validation errors in JSON textareas
798
+ let hasInvalidJson = false;
799
+
800
+ document.querySelectorAll('.json-content').forEach(textarea => {
801
+ try {
802
+ // Try to parse JSON to validate
803
+ JSON.parse(textarea.value);
804
+ } catch (e) {
805
+ // If it's not valid JSON, show an error
806
+ e.preventDefault();
807
+ hasInvalidJson = true;
808
+
809
+ // Find the closest settings-item
810
+ const settingsItem = textarea.closest('.settings-item');
811
+ if (settingsItem) {
812
+ settingsItem.classList.add('settings-error');
813
+
814
+ // Add error message if it doesn't exist
815
+ let errorMsg = settingsItem.querySelector('.settings-error-message');
816
+ if (!errorMsg) {
817
+ errorMsg = document.createElement('div');
818
+ errorMsg.className = 'settings-error-message';
819
+ settingsItem.appendChild(errorMsg);
820
+ }
821
+ errorMsg.textContent = 'Invalid JSON format';
822
+ }
823
+ }
824
+ });
825
+
826
+ // Handle JSON from expanded controls
827
+ document.querySelectorAll('input[id$="_original"]').forEach(input => {
828
+ if (input.name.endsWith('_original')) {
829
+ const actualName = input.name.replace('_original', '');
830
+
831
+ // Create a hidden input with the actual name
832
+ const hiddenInput = document.createElement('input');
833
+ hiddenInput.type = 'hidden';
834
+ hiddenInput.name = actualName;
835
+ hiddenInput.value = input.value;
836
+ specificForm.appendChild(hiddenInput);
837
+ }
838
+ });
839
+
840
+ if (hasInvalidJson) {
841
+ e.preventDefault();
842
+ return false;
843
+ }
844
+ });
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Initialize range inputs to display their values
850
+ */
851
+ function initRangeInputs() {
852
+ const rangeInputs = document.querySelectorAll('input[type="range"]');
853
+
854
+ rangeInputs.forEach(range => {
855
+ const valueDisplay = document.getElementById(`${range.id}-value`) || range.nextElementSibling;
856
+
857
+ if (valueDisplay &&
858
+ (valueDisplay.classList.contains('settings-range-value') ||
859
+ valueDisplay.classList.contains('range-value'))) {
860
+ // Set initial value
861
+ valueDisplay.textContent = range.value;
862
+
863
+ // Update on input change
864
+ range.addEventListener('input', () => {
865
+ valueDisplay.textContent = range.value;
866
+ });
867
+ }
868
+ });
869
+ }
870
+
871
+ /**
872
+ * Initialize accordion behavior
873
+ */
874
+ function initAccordions() {
875
+ document.querySelectorAll('.settings-section-header').forEach(header => {
876
+ const targetId = header.dataset.target;
877
+ const target = document.getElementById(targetId);
878
+
879
+ if (target) {
880
+ // Set initial state - expanded
881
+ header.classList.remove('collapsed');
882
+ target.style.display = 'block';
883
+
884
+ header.addEventListener('click', () => {
885
+ header.classList.toggle('collapsed');
886
+ target.style.display = header.classList.contains('collapsed') ? 'none' : 'block';
887
+
888
+ // Rotate chevron icon
889
+ const icon = header.querySelector('.settings-toggle-icon i');
890
+ if (icon) {
891
+ icon.style.transform = header.classList.contains('collapsed') ? 'rotate(-90deg)' : '';
892
+ }
893
+ });
894
+ }
895
+ });
896
+ }
897
+
898
+ /**
899
+ * Format JSON in textareas
900
+ */
901
+ function initJsonFormatting() {
902
+ document.querySelectorAll('.json-content').forEach(textarea => {
903
+ const value = textarea.value.trim();
904
+
905
+ if (value && (value.startsWith('{') || value.startsWith('['))) {
906
+ try {
907
+ const formatted = JSON.stringify(JSON.parse(value), null, 2);
908
+ textarea.value = formatted;
909
+ } catch (e) {
910
+ // Not valid JSON, leave as is
911
+ console.log('Error formatting JSON:', e);
912
+ }
913
+ }
914
+
915
+ // Add event listener to format on input
916
+ textarea.addEventListener('input', function() {
917
+ if (this.value.trim() && (this.value.trim().startsWith('{') || this.value.trim().startsWith('['))) {
918
+ try {
919
+ const obj = JSON.parse(this.value);
920
+ const formatted = JSON.stringify(obj, null, 2);
921
+
922
+ // Only update if actually different (to avoid cursor jumping)
923
+ if (this.value !== formatted) {
924
+ // Remember cursor position
925
+ const selectionStart = this.selectionStart;
926
+ const selectionEnd = this.selectionEnd;
927
+
928
+ this.value = formatted;
929
+
930
+ // Try to restore cursor
931
+ this.setSelectionRange(selectionStart, selectionEnd);
932
+ }
933
+ } catch (e) {
934
+ // Invalid JSON, just leave it alone
935
+ }
936
+ }
937
+ });
938
+ });
939
+
940
+ // Convert text inputs with JSON content to textareas
941
+ document.querySelectorAll('.settings-input').forEach(input => {
942
+ const value = input.value.trim();
943
+
944
+ // Skip if the value is "[object Object]" which isn't valid JSON
945
+ if (value === "[object Object]") {
946
+ // Replace with an empty object
947
+ input.value = "{}";
948
+ console.log('Fixed [object Object] string in input:', input.name);
949
+ return;
950
+ }
951
+
952
+ if (value && (value.startsWith('{') || value.startsWith('['))) {
953
+ try {
954
+ // Try to parse as JSON to validate
955
+ JSON.parse(value);
956
+
957
+ // Create a new textarea
958
+ const textarea = document.createElement('textarea');
959
+ textarea.id = input.id;
960
+ textarea.name = input.name;
961
+ textarea.className = 'settings-textarea json-content';
962
+ textarea.disabled = input.disabled;
963
+
964
+ try {
965
+ textarea.value = JSON.stringify(JSON.parse(value), null, 2);
966
+ } catch (e) {
967
+ textarea.value = value;
968
+ }
969
+
970
+ // Replace the input with textarea
971
+ input.parentNode.replaceChild(textarea, input);
972
+ } catch (e) {
973
+ // Not valid JSON, leave as is
974
+ console.log('Error converting JSON input to textarea:', e);
975
+ }
976
+ }
977
+ });
978
+ }
979
+
980
+ /**
981
+ * Load settings from the API
982
+ */
983
+ function loadSettings() {
984
+ // Only run this for the main settings dashboard
985
+ if (!settingsContent) return;
986
+
987
+ fetch('/research/settings/all_settings')
988
+ .then(response => response.json())
989
+ .then(data => {
990
+ if (data.status === 'success') {
991
+ // Process settings to handle object values and check for corruption
992
+ allSettings = processSettings(data.settings);
993
+
994
+ // Store original values
995
+ allSettings.forEach(setting => {
996
+ originalSettings[setting.key] = setting.value;
997
+ });
998
+
999
+ // Render settings by tab
1000
+ renderSettingsByTab(activeTab);
1001
+
1002
+ // Initialize auto-save handlers
1003
+ setTimeout(initAutoSaveHandlers, 300);
1004
+
1005
+ // Initialize the dropdowns after the settings are loaded
1006
+ if (activeTab === 'llm' || activeTab === 'all') {
1007
+ setTimeout(initializeModelDropdowns, 300);
1008
+ }
1009
+ if (activeTab === 'search' || activeTab === 'all') {
1010
+ setTimeout(initializeSearchEngineDropdowns, 300);
1011
+ }
1012
+
1013
+ // Prepare the raw JSON editor if it exists
1014
+ prepareRawJsonEditor();
1015
+
1016
+ // Initialize expanded JSON controls
1017
+ setTimeout(() => {
1018
+ initExpandedJsonControls();
1019
+ }, 100);
1020
+ } else {
1021
+ showAlert('Error loading settings: ' + data.message, 'error');
1022
+ }
1023
+ })
1024
+ .catch(error => {
1025
+ showAlert('Error loading settings: ' + error, 'error');
1026
+ });
1027
+ }
1028
+
1029
+ /**
1030
+ * Format category names to be more user-friendly
1031
+ * @param {string} key - The setting key
1032
+ * @param {string} category - The category name
1033
+ * @returns {string} - The formatted category name
1034
+ */
1035
+ function formatCategoryName(key, category) {
1036
+ // Special cases for known categories
1037
+ if (category === 'app_interface') return 'App Interface';
1038
+ if (category === 'app_parameters') return 'App Parameters';
1039
+ if (category === 'llm_general') return 'LLM General';
1040
+ if (category === 'llm_parameters') return 'LLM Parameters';
1041
+ if (category === 'report_parameters') return 'Report Parameters';
1042
+ if (category === 'search_general') return 'Search General';
1043
+ if (category === 'search_parameters') return 'Search Parameters';
1044
+
1045
+ // Remove any underscores and capitalize each word
1046
+ let formattedCategory = category.replace(/_/g, ' ');
1047
+
1048
+ // Capitalize first letter of each word
1049
+ formattedCategory = formattedCategory.split(' ')
1050
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1051
+ .join(' ');
1052
+
1053
+ return formattedCategory;
1054
+ }
1055
+
1056
+ /**
1057
+ * Organize settings to avoid duplicate group names and improve organization
1058
+ * @param {Array} settings - The settings array
1059
+ * @param {string} tab - The current tab
1060
+ * @returns {Object} - The organized settings
1061
+ */
1062
+ function organizeSettings(settings, tab) {
1063
+ // Create a mapping of types
1064
+ const typeMap = {
1065
+ 'app': 'Application',
1066
+ 'llm': 'Language Models',
1067
+ 'search': 'Search Engines',
1068
+ 'report': 'Reports'
1069
+ };
1070
+
1071
+ // Define settings that should only appear in specific tabs
1072
+ const tabSpecificSettings = {
1073
+ 'llm': [
1074
+ 'llamacpp_f16_kv',
1075
+ 'provider',
1076
+ 'model',
1077
+ 'temperature',
1078
+ 'max_tokens',
1079
+ 'openai_endpoint_url',
1080
+ 'lmstudio_url',
1081
+ 'llamacpp_model_path',
1082
+ 'llamacpp_n_batch',
1083
+ 'llamacpp_n_gpu_layers',
1084
+ 'api_key'
1085
+ ],
1086
+ 'search': [
1087
+ 'iterations',
1088
+ 'max_filtered_results',
1089
+ 'max_results',
1090
+ 'quality_check_urls',
1091
+ 'questions_per_iteration',
1092
+ 'research_iterations',
1093
+ 'region',
1094
+ 'search_engine',
1095
+ 'searches_per_section',
1096
+ 'skip_relevance_filter',
1097
+ 'safe_search',
1098
+ 'search_language',
1099
+ 'time_period',
1100
+ 'tool',
1101
+ 'snippets_only'
1102
+ ],
1103
+ 'report': [
1104
+ 'enable_fact_checking',
1105
+ 'knowledge_accumulation',
1106
+ 'knowledge_accumulation_context_limit',
1107
+ 'output_dir',
1108
+ 'detailed_citations'
1109
+ ],
1110
+ 'app': [
1111
+ 'debug',
1112
+ 'host',
1113
+ 'port',
1114
+ 'enable_notifications',
1115
+ 'web_interface',
1116
+ 'enable_web',
1117
+ 'dark_mode',
1118
+ 'default_theme',
1119
+ 'theme'
1120
+ ]
1121
+ };
1122
+
1123
+ // Priority settings that should appear at the top of each tab
1124
+ const prioritySettings = {
1125
+ 'app': ['enable_web', 'enable_notifications', 'web_interface', 'theme', 'default_theme', 'dark_mode', 'debug', 'host', 'port'],
1126
+ 'llm': ['provider', 'model', 'temperature', 'max_tokens', 'api_key', 'openai_endpoint_url', 'lmstudio_url', 'llamacpp_model_path'],
1127
+ 'search': ['tool', 'search_engine', 'iterations', 'questions_per_iteration', 'research_iterations', 'max_results', 'region'],
1128
+ 'report': ['enable_fact_checking', 'knowledge_accumulation', 'output_dir', 'detailed_citations']
1129
+ };
1130
+
1131
+ // Group by prefix and category
1132
+ const grouped = {};
1133
+
1134
+ // Filter settings based on current tab
1135
+ const filteredSettings = settings.filter(setting => {
1136
+ const parts = setting.key.split('.');
1137
+ const prefix = parts[0]; // app, llm, search, etc.
1138
+ const subKey = parts[1]; // The actual key name without prefix
1139
+
1140
+ // Filter out nested settings like app.llm, app.search, app.general, app.web, etc.
1141
+ if (prefix === 'app' && (subKey === 'llm' || subKey === 'search' || subKey === 'general' || subKey === 'web')) {
1142
+ return false;
1143
+ }
1144
+
1145
+ // Filter out fact checking duplicates - only keep in report tab
1146
+ if (prefix !== 'report' && subKey === 'enable_fact_checking') {
1147
+ return false;
1148
+ }
1149
+
1150
+ // Filter out knowledge_accumulation duplicates - only keep in report tab
1151
+ if (prefix !== 'report' && (subKey === 'knowledge_accumulation' || subKey === 'knowledge_accumulation_context_limit')) {
1152
+ return false;
1153
+ }
1154
+
1155
+ // If we're on a specific tab, only show settings for that tab
1156
+ if (tab !== 'all') {
1157
+ // Only show settings in tab-specific lists for that tab
1158
+ if (tab === prefix) {
1159
+ // For tab-specific settings, make sure they're in the list
1160
+ if (tabSpecificSettings[tab] && tabSpecificSettings[tab].includes(subKey)) {
1161
+ return true;
1162
+ }
1163
+ // For settings not in any tab-specific list, allow showing them in their own tab
1164
+ for (const otherTab in tabSpecificSettings) {
1165
+ if (otherTab !== tab && tabSpecificSettings[otherTab].includes(subKey)) {
1166
+ return false;
1167
+ }
1168
+ }
1169
+ return true;
1170
+ }
1171
+ return false;
1172
+ }
1173
+
1174
+ // For "all" tab, filter out duplicates and specialized settings
1175
+ // Check if this setting belongs exclusively to a specific tab
1176
+ for (const tabName in tabSpecificSettings) {
1177
+ if (tabSpecificSettings[tabName].includes(subKey) && prefix !== tabName) {
1178
+ // Don't show this setting if it belongs to a different tab
1179
+ return false;
1180
+ }
1181
+ }
1182
+
1183
+ // Include all remaining settings in the "all" tab
1184
+ return true;
1185
+ });
1186
+
1187
+ // First pass: group settings by prefix and category
1188
+ filteredSettings.forEach(setting => {
1189
+ const parts = setting.key.split('.');
1190
+ const prefix = parts[0]; // app, llm, search, etc.
1191
+ const subKey = parts[1]; // The setting key without prefix
1192
+
1193
+ // Create namespace if needed
1194
+ if (!grouped[prefix]) {
1195
+ grouped[prefix] = {};
1196
+ }
1197
+
1198
+ // Use category or create one based on subkey
1199
+ let category = setting.category || 'general';
1200
+
1201
+ // Format the category name to be user-friendly
1202
+ category = formatCategoryName(prefix, category);
1203
+
1204
+ // For duplicate "general" categories, prefix with the type
1205
+ if (category.toLowerCase() === 'general') {
1206
+ category = `${typeMap[prefix] || prefix.charAt(0).toUpperCase() + prefix.slice(1)} General`;
1207
+ }
1208
+
1209
+ // Create category array if needed
1210
+ if (!grouped[prefix][category]) {
1211
+ grouped[prefix][category] = [];
1212
+ }
1213
+
1214
+ // Add setting to category
1215
+ grouped[prefix][category].push(setting);
1216
+ });
1217
+
1218
+ // Second pass: sort settings within each category by priority and sort categories
1219
+ for (const prefix in grouped) {
1220
+ // Get existing categories for this prefix
1221
+ const categories = Object.keys(grouped[prefix]);
1222
+
1223
+ // --- MODIFICATION START: Prioritize categories containing specific dropdowns ---
1224
+ // Identify high-priority categories
1225
+ const highPriorityCategories = [];
1226
+ const otherCategories = [];
1227
+ const priorityKeysForPrefix = prioritySettings[prefix] || [];
1228
+ const highestPriorityKeys = ['provider', 'model', 'tool']; // Keys whose *containing category* should be first
1229
+
1230
+ categories.forEach(category => {
1231
+ const containsHighestPriority = grouped[prefix][category].some(setting => {
1232
+ const subKey = setting.key.split('.')[1];
1233
+ // Ensure the setting key itself is also in the general priority list for the prefix
1234
+ return highestPriorityKeys.includes(subKey) && priorityKeysForPrefix.includes(subKey);
1235
+ });
1236
+ if (containsHighestPriority) {
1237
+ highPriorityCategories.push(category);
1238
+ } else {
1239
+ otherCategories.push(category);
1240
+ }
1241
+ });
1242
+
1243
+ // Sort the high-priority categories (e.g., alphabetically or by specific order if needed)
1244
+ highPriorityCategories.sort((a, b) => {
1245
+ // Simple sort for now, could be more specific if needed
1246
+ // Example: ensure "Provider" comes before "Model" if both are high priority
1247
+ const order = ['Provider', 'Model', 'Tool'];
1248
+ const aIndex = order.findIndex(word => a.includes(word));
1249
+ const bIndex = order.findIndex(word => b.includes(word));
1250
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1251
+ if (aIndex !== -1) return -1;
1252
+ if (bIndex !== -1) return 1;
1253
+ return a.localeCompare(b);
1254
+ });
1255
+
1256
+ // Sort other categories based on existing logic (e.g., using categoryOrder)
1257
+ const categoryOrder = ['General', 'Interface', 'Connection', 'API', 'Parameters']; // Adjusted order slightly
1258
+ otherCategories.sort((a, b) => {
1259
+ const aIndex = categoryOrder.findIndex(word => a.includes(word));
1260
+ const bIndex = categoryOrder.findIndex(word => b.includes(word));
1261
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1262
+ if (aIndex !== -1) return -1;
1263
+ if (bIndex !== -1) return 1;
1264
+ return a.localeCompare(b);
1265
+ });
1266
+
1267
+ // Combine sorted categories
1268
+ const sortedCategoryNames = [...highPriorityCategories, ...otherCategories];
1269
+
1270
+ // Create new object with sorted categories and sorted settings within each
1271
+ const sortedPrefixedCategories = {};
1272
+ sortedCategoryNames.forEach(category => {
1273
+ sortedPrefixedCategories[category] = grouped[prefix][category];
1274
+
1275
+ // Sort settings within this category (existing logic seems okay)
1276
+ sortedPrefixedCategories[category].sort((a, b) => {
1277
+ const aKey = a.key.split('.')[1];
1278
+ const bKey = b.key.split('.')[1];
1279
+ const priorities = prioritySettings[prefix] || [];
1280
+ const aIndex = priorities.indexOf(aKey);
1281
+ const bIndex = priorities.indexOf(bKey);
1282
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
1283
+ if (aIndex !== -1) return -1;
1284
+ if (bIndex !== -1) return 1;
1285
+ return aKey.localeCompare(bKey);
1286
+ });
1287
+ });
1288
+
1289
+ // Replace original categories with sorted ones
1290
+ grouped[prefix] = sortedPrefixedCategories;
1291
+ // --- MODIFICATION END ---
1292
+ }
1293
+
1294
+ return grouped;
1295
+ }
1296
+
1297
+ /**
1298
+ * Render settings based on active tab
1299
+ * @param {string} tab - The active tab
1300
+ */
1301
+ function renderSettingsByTab(tab) {
1302
+ // Only run this for the main settings dashboard
1303
+ if (!settingsContent) return;
1304
+
1305
+ // Reset dropdown initialization state when switching tabs
1306
+ window.modelDropdownsInitialized = false;
1307
+ window.searchEngineDropdownInitialized = false;
1308
+
1309
+ // Filter settings by tab
1310
+ let filteredSettings = allSettings;
1311
+
1312
+ if (tab !== 'all') {
1313
+ filteredSettings = allSettings.filter(setting => setting.key.startsWith(tab + '.'));
1314
+ }
1315
+
1316
+ // Organize settings to avoid duplicate groups
1317
+ const groupedSettings = organizeSettings(filteredSettings, tab);
1318
+
1319
+ // Build HTML
1320
+ let html = '';
1321
+
1322
+ // Define the order for the types in "all" tab
1323
+ const typeOrder = ['llm', 'search', 'report', 'app'];
1324
+ const prefixTypes = Object.keys(groupedSettings);
1325
+
1326
+ // Sort prefixes by the defined order for the "all" tab
1327
+ if (tab === 'all') {
1328
+ prefixTypes.sort((a, b) => {
1329
+ const aIndex = typeOrder.indexOf(a);
1330
+ const bIndex = typeOrder.indexOf(b);
1331
+
1332
+ // If both are in the ordered list, sort by that order
1333
+ if (aIndex !== -1 && bIndex !== -1) {
1334
+ return aIndex - bIndex;
1335
+ }
1336
+
1337
+ // If only one is in the list, it comes first
1338
+ if (aIndex !== -1) return -1;
1339
+ if (bIndex !== -1) return 1;
1340
+
1341
+ // Alphabetically for anything else
1342
+ return a.localeCompare(b);
1343
+ });
1344
+ }
1345
+
1346
+ // For each type (app, llm, search, etc.)
1347
+ for (const type of prefixTypes) {
1348
+ if (tab !== 'all' && type !== tab) continue;
1349
+
1350
+ // For each category in this type
1351
+ for (const category in groupedSettings[type]) {
1352
+ const sectionId = `section-${type}-${category.replace(/\s+/g, '-').toLowerCase()}`;
1353
+
1354
+ html += `
1355
+ <div class="settings-section">
1356
+ <div class="settings-section-header" data-target="${sectionId}">
1357
+ <div class="settings-section-title" title="${category}">
1358
+ ${category}
1359
+ </div>
1360
+ <div class="settings-toggle-icon">
1361
+ <i class="fas fa-chevron-down"></i>
1362
+ </div>
1363
+ </div>
1364
+ <div id="${sectionId}" class="settings-section-body">
1365
+ `;
1366
+
1367
+ // Add all settings in this category
1368
+ groupedSettings[type][category].forEach(setting => {
1369
+ html += renderSettingItem(setting);
1370
+ });
1371
+
1372
+ html += `
1373
+ </div>
1374
+ </div>
1375
+ `;
1376
+ }
1377
+ }
1378
+
1379
+ if (html === '') {
1380
+ html = '<div class="empty-state"><p>No settings found for this category</p></div>';
1381
+ }
1382
+
1383
+ // Update the content
1384
+ settingsContent.innerHTML = html;
1385
+
1386
+ // Check if the element exists immediately after setting innerHTML
1387
+ console.log('Checking for llm.model after render:', document.getElementById('llm.model'));
1388
+
1389
+ // Initialize accordion behavior
1390
+ initAccordions();
1391
+
1392
+ // Initialize JSON handling
1393
+ initJsonFormatting();
1394
+
1395
+ // Initialize range inputs
1396
+ initRangeInputs();
1397
+
1398
+ // Initialize expanded JSON controls
1399
+ setTimeout(() => {
1400
+ initExpandedJsonControls();
1401
+ }, 100);
1402
+
1403
+ // Initialize dropdowns AFTER content is rendered
1404
+ initializeModelDropdowns();
1405
+ initializeSearchEngineDropdowns();
1406
+ // Also initialize the main setup which finds all dropdowns
1407
+ setupCustomDropdowns();
1408
+ // Setup provider change listener after rendering
1409
+ setupProviderChangeListener();
1410
+ }
1411
+
1412
+ /**
1413
+ * Render a single setting item
1414
+ * @param {Object} setting - The setting object
1415
+ * @returns {string} - The HTML for the setting item
1416
+ */
1417
+ function renderSettingItem(setting) {
1418
+ // Log the setting being processed
1419
+ console.log('Processing Setting:', setting.key, 'UI Element:', setting.ui_element);
1420
+
1421
+ const settingId = `setting-${setting.key.replace(/\./g, '-')}`;
1422
+ let inputElement = '';
1423
+
1424
+ // Generate the appropriate input element based on UI element type
1425
+ switch(setting.ui_element) {
1426
+ case 'textarea':
1427
+ // Check if it's JSON
1428
+ let isJson = false;
1429
+ let jsonClass = '';
1430
+
1431
+ if (typeof setting.value === 'string' &&
1432
+ (setting.value.startsWith('{') || setting.value.startsWith('['))) {
1433
+ isJson = true;
1434
+ jsonClass = ' json-content';
1435
+
1436
+ // Try to format the JSON for better display
1437
+ try {
1438
+ setting.value = JSON.stringify(JSON.parse(setting.value), null, 2);
1439
+ } catch (e) {
1440
+ // If parsing fails, keep the original value
1441
+ console.log('Error formatting JSON:', e);
1442
+ }
1443
+
1444
+ // If it's an object (not an array), render individual controls
1445
+ if (setting.value.startsWith('{')) {
1446
+ try {
1447
+ const jsonObj = JSON.parse(setting.value);
1448
+ return renderExpandedJsonControls(setting, settingId, jsonObj);
1449
+ } catch (e) {
1450
+ console.log('Error parsing JSON for controls:', e);
1451
+ }
1452
+ }
1453
+ }
1454
+
1455
+ inputElement = `
1456
+ <textarea id="${settingId}" name="${setting.key}"
1457
+ class="settings-textarea${jsonClass}"
1458
+ ${!setting.editable ? 'disabled' : ''}
1459
+ >${setting.value !== null ? setting.value : ''}</textarea>
1460
+ `;
1461
+ break;
1462
+
1463
+ case 'select':
1464
+ // Handle specific keys that should use custom dropdowns
1465
+ if (setting.key === 'llm.provider') {
1466
+ const dropdownParams = {
1467
+ input_id: setting.key,
1468
+ dropdown_id: settingId + "-dropdown",
1469
+ placeholder: "Select a provider",
1470
+ label: null, // Label handled outside
1471
+ help_text: setting.description || null,
1472
+ allow_custom: false,
1473
+ show_refresh: true, // Set to true for provider
1474
+ data_setting_key: setting.key
1475
+ };
1476
+ inputElement = renderCustomDropdownHTML(dropdownParams);
1477
+ } else if (setting.key === 'search.tool') {
1478
+ const dropdownParams = {
1479
+ input_id: setting.key,
1480
+ dropdown_id: settingId + "-dropdown",
1481
+ placeholder: "Select a search tool",
1482
+ label: null,
1483
+ help_text: setting.description || null,
1484
+ allow_custom: false,
1485
+ show_refresh: false, // No refresh for search tool
1486
+ data_setting_key: setting.key
1487
+ };
1488
+ inputElement = renderCustomDropdownHTML(dropdownParams);
1489
+ } else if (setting.key === 'llm.model') { // ADD THIS ELSE IF
1490
+ // Handle llm.model specifically within the 'select' case
1491
+ const dropdownParams = {
1492
+ input_id: setting.key,
1493
+ dropdown_id: settingId + "-dropdown",
1494
+ placeholder: "Select or enter a model",
1495
+ label: null,
1496
+ help_text: setting.description || null,
1497
+ allow_custom: true, // Allow custom for model
1498
+ show_refresh: true, // Show refresh for model
1499
+ refresh_aria_label: "Refresh model list",
1500
+ data_setting_key: setting.key
1501
+ };
1502
+ inputElement = renderCustomDropdownHTML(dropdownParams);
1503
+ } else {
1504
+ // Standard select for other keys
1505
+ inputElement = `
1506
+ <select id="${settingId}" name="${setting.key}"
1507
+ class="settings-select form-control"
1508
+ ${!setting.editable ? 'disabled' : ''}
1509
+ >
1510
+ `;
1511
+ if (setting.options) {
1512
+ setting.options.forEach(option => {
1513
+ const selected = option.value === setting.value ? 'selected' : '';
1514
+ inputElement += `<option value="${option.value}" ${selected}>${option.label || option.value}</option>`;
1515
+ });
1516
+ }
1517
+ inputElement += `</select>`;
1518
+ }
1519
+ break;
1520
+
1521
+ case 'checkbox':
1522
+ const checked = setting.value === true || setting.value === 'true' ? 'checked' : '';
1523
+ inputElement = `
1524
+ <div class="settings-checkbox-container">
1525
+ <label class="checkbox-label" for="${settingId}">
1526
+ <input type="checkbox" id="${settingId}" name="${setting.key}"
1527
+ class="settings-checkbox"
1528
+ ${checked}
1529
+ ${!setting.editable ? 'disabled' : ''}
1530
+ >
1531
+ <span class="checkbox-text">${setting.name}</span>
1532
+ </label>
1533
+ </div>
1534
+ `;
1535
+ break;
1536
+
1537
+ case 'slider':
1538
+ case 'range':
1539
+ const min = setting.min_value !== null ? setting.min_value : 0;
1540
+ const max = setting.max_value !== null ? setting.max_value : 100;
1541
+ const step = setting.step !== null ? setting.step : 1;
1542
+
1543
+ inputElement = `
1544
+ <div class="settings-range-container">
1545
+ <input type="range" id="${settingId}" name="${setting.key}"
1546
+ class="settings-range form-control"
1547
+ value="${setting.value !== null ? setting.value : min}"
1548
+ min="${min}" max="${max}" step="${step}"
1549
+ ${!setting.editable ? 'disabled' : ''}
1550
+ >
1551
+ <span class="settings-range-value">${setting.value !== null ? setting.value : min}</span>
1552
+ </div>
1553
+ `;
1554
+ break;
1555
+
1556
+ case 'number':
1557
+ const numMin = setting.min_value !== null ? setting.min_value : '';
1558
+ const numMax = setting.max_value !== null ? setting.max_value : '';
1559
+ const numStep = setting.step !== null ? setting.step : 1;
1560
+
1561
+ inputElement = `
1562
+ <input type="number" id="${settingId}" name="${setting.key}"
1563
+ class="settings-input form-control"
1564
+ value="${setting.value !== null ? setting.value : ''}"
1565
+ min="${numMin}" max="${numMax}" step="${numStep}"
1566
+ ${!setting.editable ? 'disabled' : ''}
1567
+ >
1568
+ `;
1569
+ break;
1570
+
1571
+ // Add a case for explicit custom dropdown if needed, or handle in default
1572
+ // case 'custom_dropdown':
1573
+
1574
+ default:
1575
+ // Handle llm.model here explicitly if not handled by ui_element
1576
+ if (typeof setting.value === 'string' &&
1577
+ (setting.value.startsWith('{') || setting.value.startsWith('['))) {
1578
+ // Handle JSON objects/arrays rendered as textareas if not expanded
1579
+ inputElement = `
1580
+ <textarea id="${settingId}" name="${setting.key}"
1581
+ class="settings-textarea json-content"
1582
+ ${!setting.editable ? 'disabled' : ''}
1583
+ >${setting.value}</textarea>
1584
+ `;
1585
+ } else {
1586
+ // Default to text input
1587
+ inputElement = `
1588
+ <input type="${setting.ui_element === 'password' ? 'password' : 'text'}"
1589
+ id="${settingId}" name="${setting.key}"
1590
+ class="settings-input form-control"
1591
+ value="${setting.value !== null ? setting.value : ''}"
1592
+ ${!setting.editable ? 'disabled' : ''}
1593
+ >
1594
+ `;
1595
+ }
1596
+ break;
1597
+ }
1598
+
1599
+ // Format the setting name to be more user-friendly if it contains underscores
1600
+ let settingName = setting.name;
1601
+ if (settingName.includes('_')) {
1602
+ settingName = formatCategoryName('', settingName);
1603
+ }
1604
+
1605
+ // For checkboxes, we've already handled the label in the inputElement
1606
+ if (setting.ui_element === 'checkbox') {
1607
+ return `
1608
+ <div class="settings-item form-group" data-key="${setting.key}">
1609
+ ${inputElement}
1610
+ ${setting.description ? `
1611
+ <div class="input-help">
1612
+ ${setting.description}
1613
+ </div>
1614
+ ` : ''}
1615
+ </div>
1616
+ `;
1617
+ }
1618
+
1619
+ // For non-checkbox elements, use the standard layout without info icons
1620
+ // Ensure help text is appended correctly AFTER the input element is generated
1621
+ const helpTextHTML = setting.description ? `<div class="input-help">${setting.description}</div>` : '';
1622
+
1623
+ return `
1624
+ <div class="settings-item form-group" data-key="${setting.key}">
1625
+ <div class="settings-item-header">
1626
+ <label for="${settingId}" title="${settingName}">
1627
+ ${settingName}
1628
+ </label>
1629
+ </div>
1630
+ ${inputElement}
1631
+ ${helpTextHTML}
1632
+ </div>
1633
+ `;
1634
+ }
1635
+
1636
+ /**
1637
+ * Render expanded JSON controls for a JSON object setting
1638
+ * @param {Object} setting - The setting object
1639
+ * @param {string} settingId - The ID for the setting
1640
+ * @param {Object} jsonObj - The parsed JSON object
1641
+ * @returns {string} - The HTML for the expanded JSON controls
1642
+ */
1643
+ function renderExpandedJsonControls(setting, settingId, jsonObj) {
1644
+ let html = `
1645
+ <div class="settings-item form-group" data-key="${setting.key}">
1646
+ <div class="settings-item-header">
1647
+ <label for="${settingId}" title="${setting.name}">
1648
+ ${setting.name}
1649
+ </label>
1650
+ </div>
1651
+ <div class="json-expanded-controls">
1652
+ <input type="hidden" id="${settingId}_original" name="${setting.key}_original"
1653
+ value="${JSON.stringify(jsonObj)}">
1654
+
1655
+ <div class="json-property-controls">
1656
+ `;
1657
+
1658
+ // Create individual form controls for each JSON property
1659
+ for (const key in jsonObj) {
1660
+ const value = jsonObj[key];
1661
+ const controlId = `${settingId}_${key}`;
1662
+ const formattedName = formatPropertyName(key);
1663
+ let controlHtml = '';
1664
+
1665
+ // Create appropriate control based on value type
1666
+ if (typeof value === 'boolean') {
1667
+ controlHtml = `
1668
+ <div class="json-property-item boolean-property" onclick="directToggleCheckbox('${controlId}')" data-checkboxid="${controlId}">
1669
+ <div class="checkbox-wrapper">
1670
+ <label class="checkbox-label" for="${controlId}">
1671
+ <input type="checkbox"
1672
+ id="${controlId}"
1673
+ name="${setting.key}_${key}"
1674
+ class="json-property-control"
1675
+ data-property="${key}"
1676
+ data-parent-key="${setting.key}"
1677
+ ${value ? 'checked' : ''}
1678
+ ${!setting.editable ? 'disabled' : ''}>
1679
+ <span class="checkbox-text">${formattedName}</span>
1680
+ </label>
1681
+ </div>
1682
+ </div>
1683
+ `;
1684
+ } else if (typeof value === 'number') {
1685
+ controlHtml = `
1686
+ <div class="json-property-item">
1687
+ <label for="${controlId}" class="property-label" title="${formattedName}">${formattedName}</label>
1688
+ <input type="number"
1689
+ id="${controlId}"
1690
+ name="${setting.key}_${key}"
1691
+ class="settings-input form-control json-property-control"
1692
+ data-property="${key}"
1693
+ data-parent-key="${setting.key}"
1694
+ value="${value}"
1695
+ ${!setting.editable ? 'disabled' : ''}>
1696
+ </div>
1697
+ `;
1698
+ } else if (typeof value === 'string' && (value === 'ITERATION' || value === 'NONE')) {
1699
+ controlHtml = `
1700
+ <div class="json-property-item">
1701
+ <label for="${controlId}" class="property-label" title="${formattedName}">${formattedName}</label>
1702
+ <select id="${controlId}"
1703
+ name="${setting.key}_${key}"
1704
+ class="settings-select form-control json-property-control"
1705
+ data-property="${key}"
1706
+ data-parent-key="${setting.key}"
1707
+ ${!setting.editable ? 'disabled' : ''}>
1708
+ <option value="ITERATION" ${value === 'ITERATION' ? 'selected' : ''}>Iteration</option>
1709
+ <option value="NONE" ${value === 'NONE' ? 'selected' : ''}>None</option>
1710
+ </select>
1711
+ </div>
1712
+ `;
1713
+ } else {
1714
+ controlHtml = `
1715
+ <div class="json-property-item">
1716
+ <label for="${controlId}" class="property-label" title="${formattedName}">${formattedName}</label>
1717
+ <input type="text"
1718
+ id="${controlId}"
1719
+ name="${setting.key}_${key}"
1720
+ class="settings-input form-control json-property-control"
1721
+ data-property="${key}"
1722
+ data-parent-key="${setting.key}"
1723
+ value="${value}"
1724
+ ${!setting.editable ? 'disabled' : ''}>
1725
+ </div>
1726
+ `;
1727
+ }
1728
+
1729
+ html += controlHtml;
1730
+ }
1731
+
1732
+ html += `
1733
+ </div>
1734
+ </div>
1735
+ ${setting.description ? `
1736
+ <div class="input-help">
1737
+ ${setting.description}
1738
+ </div>
1739
+ ` : ''}
1740
+ </div>
1741
+ `;
1742
+
1743
+ return html;
1744
+ }
1745
+
1746
+ /**
1747
+ * Format property name to be more user-friendly
1748
+ * @param {string} name - The property name
1749
+ * @returns {string} - The formatted property name
1750
+ */
1751
+ function formatPropertyName(name) {
1752
+ // Replace underscores with spaces and capitalize each word
1753
+ return name.split('_')
1754
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
1755
+ .join(' ');
1756
+ }
1757
+
1758
+ /**
1759
+ * Handle settings form submission (for the entire form)
1760
+ * @param {Event} e - The submit event
1761
+ */
1762
+ function handleSettingsSubmit(e) {
1763
+ e.preventDefault();
1764
+
1765
+ // Clear any previous errors
1766
+ document.querySelectorAll('.settings-error').forEach(element => {
1767
+ element.classList.remove('settings-error');
1768
+ });
1769
+
1770
+ document.querySelectorAll('.settings-error-message').forEach(element => {
1771
+ element.remove();
1772
+ });
1773
+
1774
+ // Collect form data
1775
+ const formData = {};
1776
+
1777
+ // Get values from inputs
1778
+ document.querySelectorAll('.settings-input, .settings-textarea, .settings-select, .settings-range').forEach(input => {
1779
+ // Skip inputs that are part of expanded JSON controls
1780
+ if (input.classList.contains('json-property-control')) return;
1781
+
1782
+ if (input.name) {
1783
+ // Check if value is a JSON object (textarea)
1784
+ if (input.tagName === 'TEXTAREA' && input.classList.contains('settings-textarea')) {
1785
+ try {
1786
+ const jsonValue = JSON.parse(input.value);
1787
+ formData[input.name] = jsonValue;
1788
+ } catch (e) {
1789
+ // Mark as invalid and don't include
1790
+ markInvalidInput(input, 'Invalid JSON format: ' + e.message);
1791
+ return;
1792
+ }
1793
+ } else {
1794
+ formData[input.name] = input.value;
1795
+ }
1796
+ }
1797
+ });
1798
+
1799
+ // Get values from checkboxes
1800
+ document.querySelectorAll('.settings-checkbox').forEach(checkbox => {
1801
+ // Skip checkboxes that are part of expanded JSON controls
1802
+ if (checkbox.classList.contains('json-property-control')) return;
1803
+
1804
+ if (checkbox.name) {
1805
+ formData[checkbox.name] = checkbox.checked;
1806
+ }
1807
+ });
1808
+
1809
+ // Process expanded JSON controls
1810
+ document.querySelectorAll('input[id$="_original"]').forEach(input => {
1811
+ if (input.name && input.name.endsWith('_original')) {
1812
+ const actualName = input.name.replace('_original', '');
1813
+
1814
+ // Get all controls for this setting
1815
+ const jsonData = {};
1816
+ const controls = document.querySelectorAll(`.json-property-control[data-parent-key="${actualName}"]`);
1817
+
1818
+ controls.forEach(control => {
1819
+ const propName = control.dataset.property;
1820
+
1821
+ if (propName) {
1822
+ if (control.type === 'checkbox') {
1823
+ jsonData[propName] = control.checked;
1824
+ } else if (control.tagName === 'SELECT') {
1825
+ jsonData[propName] = control.value;
1826
+ } else {
1827
+ // Attempt to convert to number if appropriate
1828
+ if (!isNaN(control.value) && control.value !== '') {
1829
+ // Check if it should be a float or int
1830
+ if (control.value.includes('.')) {
1831
+ jsonData[propName] = parseFloat(control.value);
1832
+ } else {
1833
+ jsonData[propName] = parseInt(control.value, 10);
1834
+ }
1835
+ } else {
1836
+ jsonData[propName] = control.value;
1837
+ }
1838
+ }
1839
+ }
1840
+ });
1841
+
1842
+ // Special handling for corrupted JSON values (check for empty objects, single characters, etc.)
1843
+ if (Object.keys(jsonData).length === 0) {
1844
+ // Use the original JSON if it's non-empty and valid
1845
+ try {
1846
+ const originalJson = JSON.parse(input.value);
1847
+ if (originalJson && typeof originalJson === 'object' && Object.keys(originalJson).length > 0) {
1848
+ formData[actualName] = originalJson;
1849
+ } else {
1850
+ // Skip empty JSON
1851
+ console.log(`Skipping empty JSON object for ${actualName}`);
1852
+ }
1853
+ } catch (e) {
1854
+ console.log(`Error parsing original JSON for ${actualName}:`, e);
1855
+ }
1856
+ } else {
1857
+ // Use the collected data
1858
+ formData[actualName] = jsonData;
1859
+ }
1860
+ }
1861
+ });
1862
+
1863
+ // For report nested values that might be corrupted, ensure they're proper objects
1864
+ Object.keys(formData).forEach(key => {
1865
+ // Check for various forms of corrupted data
1866
+ if (
1867
+ (typeof formData[key] === 'string' &&
1868
+ (formData[key] === '{' ||
1869
+ formData[key] === '[' ||
1870
+ formData[key] === '' ||
1871
+ formData[key] === null ||
1872
+ formData[key] === "[object Object]")) ||
1873
+ formData[key] === null
1874
+ ) {
1875
+ // This is likely a corrupted setting
1876
+ console.log(`Detected corrupted setting: ${key} with value: ${formData[key]}`);
1877
+
1878
+ if (key.startsWith('report.')) {
1879
+ // For report settings, replace with empty object
1880
+ formData[key] = {};
1881
+ } else {
1882
+ // For other settings, delete to let defaults take over
1883
+ delete formData[key];
1884
+ }
1885
+ }
1886
+ });
1887
+
1888
+ // Get raw config from editor if visible
1889
+ if (rawConfigSection.style.display !== 'none' && rawConfigEditor) {
1890
+ try {
1891
+ const rawConfig = JSON.parse(rawConfigEditor.value);
1892
+
1893
+ // Process raw config and flatten the structure
1894
+ const flattenedConfig = {};
1895
+
1896
+ // Process each namespace in the config (app, llm, search, report)
1897
+ Object.keys(rawConfig).forEach(namespace => {
1898
+ const section = rawConfig[namespace];
1899
+
1900
+ // Each key in the section should be added to form data with namespace prefix
1901
+ Object.keys(section).forEach(key => {
1902
+ const fullKey = `${namespace}.${key}`;
1903
+ flattenedConfig[fullKey] = section[key];
1904
+ });
1905
+ });
1906
+
1907
+ // Merge with form data, giving precedence to the raw JSON config
1908
+ Object.assign(formData, flattenedConfig);
1909
+ } catch (e) {
1910
+ showAlert('Invalid JSON in raw config editor: ' + e.message, 'error');
1911
+ return;
1912
+ }
1913
+ }
1914
+
1915
+ // Show saving state for the form
1916
+ if (settingsForm) {
1917
+ settingsForm.classList.add('saving');
1918
+ }
1919
+
1920
+ // Submit data to API
1921
+ submitSettingsData(formData, settingsForm);
1922
+ }
1923
+
1924
+ /**
1925
+ * Show a success indicator on an input
1926
+ * @param {HTMLElement} element - The input element
1927
+ */
1928
+ function showSaveSuccess(element) {
1929
+ if (!element) return;
1930
+
1931
+ // Add success class
1932
+ element.classList.add('save-success');
1933
+
1934
+ // Remove it after a short delay
1935
+ setTimeout(() => {
1936
+ element.classList.remove('save-success');
1937
+ }, 1500);
1938
+ }
1939
+
1940
+ /**
1941
+ * Submit settings data to the API
1942
+ * @param {Object} formData - The settings to save
1943
+ * @param {HTMLElement} sourceElement - The input element that triggered the save
1944
+ */
1945
+ function submitSettingsData(formData, sourceElement) {
1946
+ // Show loading indicator
1947
+ let loadingContainer = sourceElement;
1948
+
1949
+ // If it's a specific input element, find its container to position the spinner correctly
1950
+ if (sourceElement && sourceElement.tagName) {
1951
+ if (sourceElement.type === 'checkbox') {
1952
+ // For checkboxes, use the checkbox label
1953
+ loadingContainer = sourceElement.closest('.checkbox-label') || sourceElement;
1954
+ } else if (sourceElement.classList.contains('json-property-control')) {
1955
+ // For JSON property controls, use the property item
1956
+ loadingContainer = sourceElement.closest('.json-property-item') || sourceElement;
1957
+ } else {
1958
+ // For other inputs, use the form-group or settings-item
1959
+ loadingContainer = sourceElement.closest('.form-group') ||
1960
+ sourceElement.closest('.settings-item') ||
1961
+ sourceElement;
1962
+ }
1963
+ }
1964
+
1965
+ // Add the saving class to show the spinner
1966
+ if (loadingContainer) {
1967
+ loadingContainer.classList.add('saving');
1968
+ }
1969
+
1970
+ // Get the keys being saved for reference
1971
+ const savingKeys = Object.keys(formData);
1972
+
1973
+ // Store original values to show what changed
1974
+ const originalValues = {};
1975
+ savingKeys.forEach(key => {
1976
+ const settingObj = allSettings.find(s => s.key === key);
1977
+ originalValues[key] = settingObj ? settingObj.value : null;
1978
+ });
1979
+
1980
+ // --- ADD THIS LINE ---
1981
+ console.log('[submitSettingsData] Preparing to fetch /research/settings/save_all_settings with data:', JSON.stringify(formData));
1982
+ // --- END ADD ---
1983
+
1984
+ fetch('/research/settings/save_all_settings', {
1985
+ method: 'POST',
1986
+ headers: {
1987
+ 'Content-Type': 'application/json',
1988
+ 'X-CSRFToken': getCsrfToken()
1989
+ },
1990
+ body: JSON.stringify(formData),
1991
+ })
1992
+ .then(response => response.json())
1993
+ .then(data => {
1994
+ if (data.status === 'success') {
1995
+ // Show success indicator on the source element
1996
+ if (sourceElement) {
1997
+ showSaveSuccess(sourceElement);
1998
+ }
1999
+
2000
+ // Remove loading state
2001
+ if (loadingContainer) {
2002
+ loadingContainer.classList.remove('saving');
2003
+ }
2004
+
2005
+ // Update all settings data if it's a global change
2006
+ if (!sourceElement || savingKeys.length > 1) {
2007
+ // Update global state
2008
+ if (data.settings) {
2009
+ allSettings = processSettings(data.settings);
2010
+ }
2011
+ } else {
2012
+ // Update just the changed setting in our allSettings array
2013
+ if (savingKeys.length === 1) {
2014
+ const key = savingKeys[0];
2015
+ const settingIndex = allSettings.findIndex(s => s.key === key);
2016
+
2017
+ if (settingIndex !== -1 && data.settings) {
2018
+ // Find the updated setting in the response
2019
+ const updatedSetting = data.settings.find(s => s.key === key);
2020
+
2021
+ if (updatedSetting) {
2022
+ // Update the setting in our array
2023
+ allSettings[settingIndex] = processSettings([updatedSetting])[0];
2024
+ }
2025
+ }
2026
+ }
2027
+ }
2028
+
2029
+ // Update originalSettings cache for the saved keys
2030
+ savingKeys.forEach(key => {
2031
+ const settingIndex = allSettings.findIndex(s => s.key === key);
2032
+ if (settingIndex !== -1) {
2033
+ originalSettings[key] = allSettings[settingIndex].value;
2034
+ console.log(`Updated originalSettings cache for ${key}:`, originalSettings[key]);
2035
+ }
2036
+ });
2037
+
2038
+ // Update the raw JSON editor if it's visible
2039
+ if (rawConfigSection && rawConfigSection.style.display === 'block') {
2040
+ prepareRawJsonEditor();
2041
+ }
2042
+
2043
+ // Format a more informative message
2044
+ let successMessage = '';
2045
+ if (savingKeys.length === 1) {
2046
+ const key = savingKeys[0];
2047
+ const settingObj = allSettings.find(s => s.key === key);
2048
+ const oldValue = originalValues[key];
2049
+ const newValue = settingObj ? settingObj.value : formData[key];
2050
+
2051
+ // Format the display name for better readability
2052
+ const displayName = key.split('.').pop().replace(/_/g, ' ');
2053
+ const capitalizedName = displayName.charAt(0).toUpperCase() + displayName.slice(1);
2054
+
2055
+ // Format the values for display
2056
+ const oldDisplay = formatValueForDisplay(oldValue);
2057
+ const newDisplay = formatValueForDisplay(newValue);
2058
+
2059
+ successMessage = `${capitalizedName}: ${oldDisplay} → ${newDisplay}`;
2060
+ } else {
2061
+ // If multiple settings were updated, use the original message
2062
+ successMessage = data.message || 'Settings saved successfully';
2063
+ }
2064
+
2065
+ // Show toast notification if ui.showMessage is available
2066
+ if (window.ui && window.ui.showMessage) {
2067
+ window.ui.showMessage(successMessage, 'success', 3000);
2068
+ // We're showing toast, so we pass true to skip showing the regular alert
2069
+ showAlert(successMessage, 'success', true);
2070
+ } else {
2071
+ // Fallback to regular alert, force showing it
2072
+ showAlert(successMessage, 'success', false);
2073
+ }
2074
+ } else {
2075
+ // Show error message
2076
+ if (window.ui && window.ui.showMessage) {
2077
+ window.ui.showMessage(data.message || 'Error saving settings', 'error', 5000);
2078
+ showAlert(data.message || 'Error saving settings', 'error', true);
2079
+ } else {
2080
+ showAlert(data.message || 'Error saving settings', 'error', false);
2081
+ }
2082
+
2083
+ // Remove loading state
2084
+ if (loadingContainer) {
2085
+ loadingContainer.classList.remove('saving');
2086
+ }
2087
+ }
2088
+ })
2089
+ .catch(error => {
2090
+ console.error('Error saving settings:', error);
2091
+
2092
+ // Show error message
2093
+ if (window.ui && window.ui.showMessage) {
2094
+ window.ui.showMessage('Error saving settings: ' + error.message, 'error', 5000);
2095
+ showAlert('Error saving settings: ' + error.message, 'error', true);
2096
+ } else {
2097
+ showAlert('Error saving settings: ' + error.message, 'error', false);
2098
+ }
2099
+
2100
+ // Remove loading state
2101
+ if (loadingContainer) {
2102
+ loadingContainer.classList.remove('saving');
2103
+ }
2104
+ });
2105
+ }
2106
+
2107
+ /**
2108
+ * Format a value for display in notifications
2109
+ * @param {any} value - The value to format
2110
+ * @returns {string} - Formatted value for display
2111
+ */
2112
+ function formatValueForDisplay(value) {
2113
+ if (value === null || value === undefined) {
2114
+ return 'empty';
2115
+ } else if (typeof value === 'boolean') {
2116
+ return value ? 'enabled' : 'disabled';
2117
+ } else if (typeof value === 'object') {
2118
+ // For objects, show a simplified representation
2119
+ return '{...}';
2120
+ } else if (typeof value === 'string' && value.length > 20) {
2121
+ // Truncate long strings
2122
+ return `"${value.substring(0, 18)}..."`;
2123
+ } else if (typeof value === 'string') {
2124
+ return `"${value}"`;
2125
+ } else {
2126
+ return String(value);
2127
+ }
2128
+ }
2129
+
2130
+ /**
2131
+ * Handle search input for filtering settings
2132
+ */
2133
+ function handleSearchInput() {
2134
+ // Only run this for the main settings dashboard
2135
+ if (!settingsContent || !settingsSearch) return;
2136
+
2137
+ const searchValue = settingsSearch.value.toLowerCase();
2138
+
2139
+ if (searchValue === '') {
2140
+ // If search is empty, just re-render based on active tab
2141
+ renderSettingsByTab(activeTab);
2142
+ return;
2143
+ }
2144
+
2145
+ // Filter settings based on search
2146
+ const filteredSettings = allSettings.filter(setting => {
2147
+ return (
2148
+ setting.key.toLowerCase().includes(searchValue) ||
2149
+ setting.name.toLowerCase().includes(searchValue) ||
2150
+ (setting.description && setting.description.toLowerCase().includes(searchValue)) ||
2151
+ (setting.category && setting.category.toLowerCase().includes(searchValue))
2152
+ );
2153
+ });
2154
+
2155
+ // Organize settings to avoid duplicate groups
2156
+ const groupedSettings = organizeSettings(filteredSettings, 'all');
2157
+
2158
+ // Build HTML
2159
+ let html = '';
2160
+
2161
+ // Define the order for the types
2162
+ const typeOrder = ['app', 'llm', 'search', 'report'];
2163
+ const prefixTypes = Object.keys(groupedSettings);
2164
+
2165
+ // Sort prefixes by the defined order
2166
+ prefixTypes.sort((a, b) => {
2167
+ const aIndex = typeOrder.indexOf(a);
2168
+ const bIndex = typeOrder.indexOf(b);
2169
+
2170
+ // If both are in the ordered list, sort by that order
2171
+ if (aIndex !== -1 && bIndex !== -1) {
2172
+ return aIndex - bIndex;
2173
+ }
2174
+
2175
+ // If only one is in the list, it comes first
2176
+ if (aIndex !== -1) return -1;
2177
+ if (bIndex !== -1) return 1;
2178
+
2179
+ // Alphabetically for anything else
2180
+ return a.localeCompare(b);
2181
+ });
2182
+
2183
+ // For each type (app, llm, search, etc.)
2184
+ for (const type of prefixTypes) {
2185
+ // For each category in this type
2186
+ for (const category in groupedSettings[type]) {
2187
+ const sectionId = `section-${type}-${category.replace(/\s+/g, '-').toLowerCase()}`;
2188
+
2189
+ html += `
2190
+ <div class="settings-section">
2191
+ <div class="settings-section-header" data-target="${sectionId}">
2192
+ <div class="settings-section-title" title="${category}">
2193
+ ${category}
2194
+ </div>
2195
+ <div class="settings-toggle-icon">
2196
+ <i class="fas fa-chevron-down"></i>
2197
+ </div>
2198
+ </div>
2199
+ <div id="${sectionId}" class="settings-section-body">
2200
+ `;
2201
+
2202
+ // Add all settings in this category
2203
+ groupedSettings[type][category].forEach(setting => {
2204
+ html += renderSettingItem(setting);
2205
+ });
2206
+
2207
+ html += `
2208
+ </div>
2209
+ </div>
2210
+ `;
2211
+ }
2212
+ }
2213
+
2214
+ if (html === '') {
2215
+ html = '<div class="empty-state"><p>No settings found matching your search</p></div>';
2216
+ }
2217
+
2218
+ // Add a container for alerts that will maintain proper positioning
2219
+ html = '<div id="filtered-settings-alert" class="settings-alert-container"></div>' + html;
2220
+
2221
+ // Update the content
2222
+ settingsContent.innerHTML = html;
2223
+
2224
+ // Initialize accordion behavior - all expanded for search results
2225
+ initAccordions();
2226
+
2227
+ // Initialize JSON handling
2228
+ initJsonFormatting();
2229
+
2230
+ // Initialize range inputs
2231
+ initRangeInputs();
2232
+
2233
+ // Initialize auto-save handlers after re-rendering
2234
+ initAutoSaveHandlers();
2235
+
2236
+ // Initialize expanded JSON controls
2237
+ setTimeout(() => {
2238
+ initExpandedJsonControls();
2239
+ }, 100);
2240
+ }
2241
+
2242
+ /**
2243
+ * Handle the reset button click
2244
+ */
2245
+ function handleReset() {
2246
+ // Reset to original values
2247
+ document.querySelectorAll('.settings-input, .settings-textarea, .settings-select').forEach(input => {
2248
+ // Skip inputs that are part of expanded JSON controls
2249
+ if (input.classList.contains('json-property-control')) return;
2250
+
2251
+ const originalValue = originalSettings[input.name];
2252
+
2253
+ if (typeof originalValue === 'object' && originalValue !== null) {
2254
+ input.value = JSON.stringify(originalValue, null, 2);
2255
+ } else {
2256
+ input.value = originalValue !== undefined ? originalValue : '';
2257
+ }
2258
+ });
2259
+
2260
+ document.querySelectorAll('.settings-checkbox').forEach(checkbox => {
2261
+ // Skip checkboxes that are part of expanded JSON controls
2262
+ if (checkbox.classList.contains('json-property-control')) return;
2263
+
2264
+ const originalValue = originalSettings[checkbox.name];
2265
+ checkbox.checked = originalValue === true || originalValue === 'true';
2266
+ });
2267
+
2268
+ document.querySelectorAll('.settings-range').forEach(range => {
2269
+ const originalValue = originalSettings[range.name];
2270
+ range.value = originalValue !== undefined ? originalValue : range.min;
2271
+
2272
+ // Update value display
2273
+ const valueDisplay = range.nextElementSibling;
2274
+ if (valueDisplay && valueDisplay.classList.contains('settings-range-value')) {
2275
+ valueDisplay.textContent = range.value;
2276
+ }
2277
+ });
2278
+
2279
+ // Reset expanded JSON controls
2280
+ document.querySelectorAll('input[id$="_original"]').forEach(input => {
2281
+ if (input.name.endsWith('_original')) {
2282
+ const actualName = input.name.replace('_original', '');
2283
+ const originalValue = originalSettings[actualName];
2284
+
2285
+ if (originalValue) {
2286
+ // Check for corrupted JSON (single character values like "{")
2287
+ if (typeof originalValue === 'string' && originalValue.length < 3) {
2288
+ console.log(`Skipping corrupted JSON value for ${actualName}`);
2289
+ return;
2290
+ }
2291
+
2292
+ let jsonData = originalValue;
2293
+ if (typeof jsonData === 'string') {
2294
+ try {
2295
+ jsonData = JSON.parse(jsonData);
2296
+ } catch (e) {
2297
+ console.log('Error parsing JSON during reset:', e);
2298
+ return;
2299
+ }
2300
+ }
2301
+
2302
+ // Update the hidden input
2303
+ input.value = JSON.stringify(jsonData);
2304
+
2305
+ // Update individual controls
2306
+ for (const prop in jsonData) {
2307
+ const control = document.querySelector(`.json-property-control[data-parent-key="${actualName}"][data-property="${prop}"]`);
2308
+ if (control) {
2309
+ if (control.type === 'checkbox') {
2310
+ control.checked = !!jsonData[prop];
2311
+ } else if (control.tagName === 'SELECT') {
2312
+ control.value = jsonData[prop];
2313
+ } else {
2314
+ control.value = jsonData[prop];
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ }
2320
+ });
2321
+
2322
+ // Format JSON values
2323
+ initJsonFormatting();
2324
+
2325
+ showAlert('Settings reset to last saved values', 'info');
2326
+ }
2327
+
2328
+ /**
2329
+ * Handle the reset to defaults button click
2330
+ */
2331
+ function handleResetToDefaults() {
2332
+ // Show confirmation dialog
2333
+ if (confirm('Are you sure you want to reset ALL settings to their default values? This cannot be undone.')) {
2334
+ // Call the reset to defaults API
2335
+ fetch('/research/settings/reset_to_defaults', {
2336
+ method: 'POST',
2337
+ headers: {
2338
+ 'Content-Type': 'application/json',
2339
+ 'X-CSRFToken': getCsrfToken()
2340
+ }
2341
+ })
2342
+ .then(response => response.json())
2343
+ .then(data => {
2344
+ if (data.status === 'success') {
2345
+ showAlert('Settings have been reset to defaults. Reloading page...', 'success');
2346
+
2347
+ // Reload the page after a brief delay to show the success message
2348
+ setTimeout(() => {
2349
+ window.location.reload();
2350
+ }, 1500);
2351
+ } else {
2352
+ showAlert('Error resetting settings: ' + data.message, 'error');
2353
+ }
2354
+ })
2355
+ .catch(error => {
2356
+ showAlert('Error resetting settings: ' + error, 'error');
2357
+ });
2358
+ }
2359
+ }
2360
+
2361
+ /**
2362
+ * Toggle the display of raw configuration
2363
+ */
2364
+ function toggleRawConfig() {
2365
+ if (rawConfigSection && rawConfigEditor) {
2366
+ const isVisible = rawConfigSection.style.display !== 'none';
2367
+
2368
+ // If hiding the editor, try to apply changes
2369
+ if (isVisible) {
2370
+ try {
2371
+ // Parse the JSON to validate it
2372
+ const rawConfig = JSON.parse(rawConfigEditor.value);
2373
+
2374
+ // Process and flatten the JSON
2375
+ const flattenedConfig = {};
2376
+
2377
+ Object.keys(rawConfig).forEach(namespace => {
2378
+ const section = rawConfig[namespace];
2379
+
2380
+ Object.keys(section).forEach(key => {
2381
+ const fullKey = `${namespace}.${key}`;
2382
+ flattenedConfig[fullKey] = section[key];
2383
+ });
2384
+ });
2385
+
2386
+ // Save the changes to apply them to UI
2387
+ submitSettingsData(flattenedConfig, null);
2388
+ } catch (e) {
2389
+ // Show error but don't prevent hiding the editor
2390
+ showAlert('Invalid JSON in editor: ' + e.message, 'error');
2391
+ }
2392
+ }
2393
+
2394
+ // Toggle visibility
2395
+ rawConfigSection.style.display = isVisible ? 'none' : 'block';
2396
+
2397
+ // Update toggle text
2398
+ const toggleText = document.getElementById('toggle-text');
2399
+ if (toggleText) {
2400
+ toggleText.textContent = isVisible ? 'Show JSON Configuration' : 'Hide JSON Configuration';
2401
+ }
2402
+
2403
+ // If showing the config, prepare it
2404
+ if (!isVisible) {
2405
+ prepareRawJsonEditor();
2406
+ }
2407
+ }
2408
+ }
2409
+
2410
+ /**
2411
+ * Prepare the raw JSON editor with all settings
2412
+ */
2413
+ function prepareRawJsonEditor() {
2414
+ if (rawConfigEditor && allSettings.length > 0) {
2415
+ // Try to parse existing JSON from editor if it exists
2416
+ let existingConfig = {};
2417
+ try {
2418
+ if (rawConfigEditor.value) {
2419
+ existingConfig = JSON.parse(rawConfigEditor.value);
2420
+ }
2421
+ } catch (e) {
2422
+ console.warn('Could not parse existing JSON config, starting fresh');
2423
+ existingConfig = {};
2424
+ }
2425
+
2426
+ // Prepare settings as a JSON object
2427
+ const settingsObj = {};
2428
+
2429
+ // Group by prefix (app, llm, search, report)
2430
+ allSettings.forEach(setting => {
2431
+ const key = setting.key;
2432
+ const parts = key.split('.');
2433
+ const prefix = parts[0];
2434
+
2435
+ // Initialize namespace if needed
2436
+ if (!settingsObj[prefix]) {
2437
+ settingsObj[prefix] = {};
2438
+ }
2439
+
2440
+ // Parse JSON values
2441
+ let value = setting.value;
2442
+ if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
2443
+ try {
2444
+ value = JSON.parse(value);
2445
+ } catch (e) {
2446
+ // Leave as string if not valid JSON
2447
+ }
2448
+ }
2449
+
2450
+ // Add to settings object
2451
+ settingsObj[prefix][key.substring(prefix.length + 1)] = value;
2452
+ });
2453
+
2454
+ // Merge with existing config to preserve unknown parameters
2455
+ Object.keys(existingConfig).forEach(prefix => {
2456
+ if (!settingsObj[prefix]) {
2457
+ settingsObj[prefix] = {};
2458
+ }
2459
+
2460
+ Object.keys(existingConfig[prefix]).forEach(key => {
2461
+ // Only keep parameters that don't exist in our known settings
2462
+ const fullKey = `${prefix}.${key}`;
2463
+ const exists = allSettings.some(s => s.key === fullKey);
2464
+
2465
+ if (!exists) {
2466
+ settingsObj[prefix][key] = existingConfig[prefix][key];
2467
+ }
2468
+ });
2469
+ });
2470
+
2471
+ // Format as pretty JSON
2472
+ rawConfigEditor.value = JSON.stringify(settingsObj, null, 2);
2473
+ }
2474
+ }
2475
+
2476
+ /**
2477
+ * Function to open file location (for collections config)
2478
+ * @param {string} filePath - The file path to open
2479
+ */
2480
+ function openFileLocation(filePath) {
2481
+ // Create a hidden form and submit it to a route that will open the file location
2482
+ const form = document.createElement('form');
2483
+ form.method = 'POST';
2484
+ form.action = "/research/open_file_location";
2485
+
2486
+ const input = document.createElement('input');
2487
+ input.type = 'hidden';
2488
+ input.name = 'file_path';
2489
+ input.value = filePath;
2490
+
2491
+ form.appendChild(input);
2492
+ document.body.appendChild(form);
2493
+ form.submit();
2494
+ }
2495
+
2496
+ /**
2497
+ * Initialize click handlers for checkbox wrappers
2498
+ */
2499
+ function initCheckboxWrappers() {
2500
+ // No longer needed - using direct onclick attribute instead
2501
+ }
2502
+
2503
+ /**
2504
+ * Toggle checkbox directly from onclick event
2505
+ * Simple, direct function to toggle checkboxes
2506
+ * @param {string} checkboxId - The ID of the checkbox to toggle
2507
+ */
2508
+ function directToggleCheckbox(checkboxId) {
2509
+ const checkbox = document.getElementById(checkboxId);
2510
+ if (checkbox && !checkbox.disabled) {
2511
+ // Toggle the checkbox state
2512
+ checkbox.checked = !checkbox.checked;
2513
+
2514
+ // Trigger change event for listeners
2515
+ const changeEvent = new Event('change', { bubbles: true });
2516
+ checkbox.dispatchEvent(changeEvent);
2517
+
2518
+ // Stop event propagation
2519
+ event.stopPropagation();
2520
+ }
2521
+ }
2522
+
2523
+ /**
2524
+ * Get CSRF token from meta tag
2525
+ */
2526
+ function getCsrfToken() {
2527
+ return document.querySelector('meta[name="csrf-token"]').getAttribute('content');
2528
+ }
2529
+
2530
+ /**
2531
+ * Handle the fix corrupted settings button click
2532
+ */
2533
+ function handleFixCorruptedSettings() {
2534
+ // Call the fix corrupted settings API
2535
+ fetch('/research/settings/fix_corrupted_settings', {
2536
+ method: 'POST',
2537
+ headers: {
2538
+ 'Content-Type': 'application/json',
2539
+ 'X-CSRFToken': getCsrfToken()
2540
+ }
2541
+ })
2542
+ .then(response => response.json())
2543
+ .then(data => {
2544
+ if (data.status === 'success') {
2545
+ if (data.fixed_settings && data.fixed_settings.length > 0) {
2546
+ showAlert(`Fixed ${data.fixed_settings.length} corrupted settings. Reloading page...`, 'success');
2547
+
2548
+ // Reload the page after a brief delay to show the success message
2549
+ setTimeout(() => {
2550
+ window.location.reload();
2551
+ }, 1500);
2552
+ } else {
2553
+ showAlert('No corrupted settings were found.', 'info');
2554
+ }
2555
+ } else {
2556
+ showAlert('Error fixing corrupted settings: ' + data.message, 'error');
2557
+ }
2558
+ })
2559
+ .catch(error => {
2560
+ showAlert('Error fixing corrupted settings: ' + error, 'error');
2561
+ });
2562
+ }
2563
+
2564
+ /**
2565
+ * Check if Ollama service is running
2566
+ * @returns {Promise<boolean>} True if Ollama is running
2567
+ */
2568
+ async function isOllamaRunning() {
2569
+ try {
2570
+ const controller = new AbortController();
2571
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
2572
+
2573
+ const response = await fetch('/research/settings/api/ollama-status', {
2574
+ signal: controller.signal
2575
+ });
2576
+
2577
+ clearTimeout(timeoutId);
2578
+
2579
+ if (response.ok) {
2580
+ const data = await response.json();
2581
+ return data.running === true;
2582
+ }
2583
+ return false;
2584
+ } catch (error) {
2585
+ console.error('Ollama check failed:', error.name === 'AbortError' ? 'Request timed out' : error);
2586
+ return false;
2587
+ }
2588
+ }
2589
+
2590
+ /**
2591
+ * Fetch model providers from API
2592
+ * @param {boolean} forceRefresh - Whether to force refresh the data
2593
+ * @returns {Promise} - A promise that resolves with the model providers
2594
+ */
2595
+ function fetchModelProviders(forceRefresh = false) {
2596
+ // Use a debounce mechanism to prevent multiple calls in quick succession
2597
+ if (window.modelProvidersRequestInProgress && !forceRefresh) {
2598
+ console.log('Model providers request already in progress, using existing promise');
2599
+ return window.modelProvidersRequestInProgress;
2600
+ }
2601
+
2602
+ const cachedData = getCachedData('deepResearch.modelProviders');
2603
+ const cacheTimestamp = getCachedData('deepResearch.cacheTimestamp');
2604
+
2605
+ // If not forcing refresh and we have valid cached data, use it
2606
+ if (!forceRefresh && cachedData && cacheTimestamp && (Date.now() - cacheTimestamp < 3600000)) { // 1 hour cache
2607
+ console.log('Using cached model providers');
2608
+ return Promise.resolve(cachedData);
2609
+ }
2610
+
2611
+ console.log('Fetching model providers from API');
2612
+
2613
+ // Create a promise and store it
2614
+ window.modelProvidersRequestInProgress = fetch('/research/settings/api/available-models')
2615
+ .then(response => {
2616
+ if (!response.ok) {
2617
+ throw new Error(`API returned status: ${response.status}`);
2618
+ }
2619
+ return response.json();
2620
+ })
2621
+ .then(data => {
2622
+ console.log('Got model data from API:', data);
2623
+ // Cache the data for future use
2624
+ cacheData('deepResearch.modelProviders', data);
2625
+ cacheData('deepResearch.cacheTimestamp', Date.now());
2626
+
2627
+ // Process the data
2628
+ const processedData = processModelData(data);
2629
+ // Clear the request flag
2630
+ window.modelProvidersRequestInProgress = null;
2631
+ return processedData;
2632
+ })
2633
+ .catch(error => {
2634
+ console.error('Error fetching model providers:', error);
2635
+ // Clear the request flag on error
2636
+ window.modelProvidersRequestInProgress = null;
2637
+ throw error;
2638
+ });
2639
+
2640
+ return window.modelProvidersRequestInProgress;
2641
+ }
2642
+
2643
+ /**
2644
+ * Fetch search engines from API
2645
+ * @param {boolean} forceRefresh - Whether to force refresh the data
2646
+ * @returns {Promise} - A promise that resolves with the search engines
2647
+ */
2648
+ function fetchSearchEngines(forceRefresh = false) {
2649
+ // Use a debounce mechanism to prevent multiple calls in quick succession
2650
+ if (window.searchEnginesRequestInProgress && !forceRefresh) {
2651
+ console.log('Search engines request already in progress, using existing promise');
2652
+ return window.searchEnginesRequestInProgress;
2653
+ }
2654
+
2655
+ const cachedData = getCachedData('deepResearch.searchEngines');
2656
+ const cacheTimestamp = getCachedData('deepResearch.cacheTimestamp');
2657
+
2658
+ // Use cached data if available and not forcing refresh
2659
+ if (!forceRefresh && cachedData && cacheTimestamp && (Date.now() - cacheTimestamp < 3600000)) { // 1 hour cache
2660
+ console.log('Using cached search engines data');
2661
+ return Promise.resolve(cachedData);
2662
+ }
2663
+
2664
+ console.log('Fetching search engines from API');
2665
+
2666
+ // Create a promise and store it
2667
+ window.searchEnginesRequestInProgress = fetch('/research/settings/api/available-search-engines')
2668
+ .then(response => {
2669
+ if (!response.ok) {
2670
+ throw new Error(`API returned status: ${response.status}`);
2671
+ }
2672
+ return response.json();
2673
+ })
2674
+ .then(data => {
2675
+ console.log('Received search engine data:', data);
2676
+ // Cache the data
2677
+ cacheData('deepResearch.searchEngines', data);
2678
+ cacheData('deepResearch.cacheTimestamp', Date.now());
2679
+
2680
+ // Process the data
2681
+ const processedData = processSearchEngineData(data);
2682
+ // Clear the request flag
2683
+ window.searchEnginesRequestInProgress = null;
2684
+ return processedData;
2685
+ })
2686
+ .catch(error => {
2687
+ console.error('Error fetching search engines:', error);
2688
+ // Clear the request flag on error
2689
+ window.searchEnginesRequestInProgress = null;
2690
+ throw error;
2691
+ });
2692
+
2693
+ return window.searchEnginesRequestInProgress;
2694
+ }
2695
+
2696
+ /**
2697
+ * Process model data from API or cache
2698
+ * @param {Object} data - The model data
2699
+ */
2700
+ function processModelData(data) {
2701
+ console.log('Processing model data:', data);
2702
+
2703
+ // Create a new array to store all formatted models
2704
+ const formattedModels = [];
2705
+
2706
+ // Process provider options first
2707
+ if (data.provider_options) {
2708
+ console.log('Found provider options:', data.provider_options.length);
2709
+ }
2710
+
2711
+ // Check for Ollama models
2712
+ if (data.providers && data.providers.ollama_models && data.providers.ollama_models.length > 0) {
2713
+ const ollama_models = data.providers.ollama_models;
2714
+ console.log('Found Ollama models:', ollama_models.length);
2715
+
2716
+ // Add provider information to each model
2717
+ ollama_models.forEach(model => {
2718
+ formattedModels.push({
2719
+ value: model.value,
2720
+ label: model.label,
2721
+ provider: 'OLLAMA' // Ensure provider field is added
2722
+ });
2723
+ });
2724
+ }
2725
+
2726
+ // Add OpenAI models if available
2727
+ if (data.providers && data.providers.openai_models && data.providers.openai_models.length > 0) {
2728
+ const openai_models = data.providers.openai_models;
2729
+ console.log('Found OpenAI models:', openai_models.length);
2730
+
2731
+ // Add provider information to each model
2732
+ openai_models.forEach(model => {
2733
+ formattedModels.push({
2734
+ value: model.value,
2735
+ label: model.label,
2736
+ provider: 'OPENAI' // Ensure provider field is added
2737
+ });
2738
+ });
2739
+ }
2740
+
2741
+ // Add Anthropic models if available
2742
+ if (data.providers && data.providers.anthropic_models && data.providers.anthropic_models.length > 0) {
2743
+ const anthropic_models = data.providers.anthropic_models;
2744
+ console.log('Found Anthropic models:', anthropic_models.length);
2745
+
2746
+ // Add provider information to each model
2747
+ anthropic_models.forEach(model => {
2748
+ formattedModels.push({
2749
+ value: model.value,
2750
+ label: model.label,
2751
+ provider: 'ANTHROPIC' // Ensure provider field is added
2752
+ });
2753
+ });
2754
+ }
2755
+
2756
+ // Update the global modelOptions array
2757
+ modelOptions = formattedModels;
2758
+ console.log('Final modelOptions:', modelOptions.length, 'models');
2759
+
2760
+ // Cache the processed models
2761
+ cacheData('deepResearch.availableModels', formattedModels);
2762
+
2763
+ // Return the processed models
2764
+ return formattedModels;
2765
+ }
2766
+
2767
+ /**
2768
+ * Process search engine data from API or cache
2769
+ * @param {Object} data - The search engine data
2770
+ */
2771
+ function processSearchEngineData(data) {
2772
+ console.log('Processing search engine data:', data);
2773
+ if (data.engine_options && data.engine_options.length > 0) {
2774
+ searchEngineOptions = data.engine_options;
2775
+ console.log('Updated search engine options:', searchEngineOptions);
2776
+
2777
+ // Always initialize search engine dropdowns when receiving new data
2778
+ initializeSearchEngineDropdowns();
2779
+ } else {
2780
+ console.warn('No engine options found in search engine data');
2781
+ }
2782
+ }
2783
+
2784
+ /**
2785
+ * Initialize custom model dropdowns in the LLM section
2786
+ */
2787
+ function initializeModelDropdowns() {
2788
+ console.log('Initializing model dropdowns');
2789
+
2790
+ // Use getElementById for direct access
2791
+ const settingsProviderInput = document.getElementById('llm.provider');
2792
+ const settingsModelInput = document.getElementById('llm.model');
2793
+ const providerHiddenInput = document.getElementById('llm.provider_hidden');
2794
+ const modelHiddenInput = document.getElementById('llm.model_hidden');
2795
+ const providerDropdownList = document.getElementById('setting-llm-provider-dropdown-list');
2796
+ const modelDropdownList = document.getElementById('setting-llm-model-dropdown-list');
2797
+
2798
+ // Skip if already initialized (avoid redundant calls)
2799
+ if (window.modelDropdownsInitialized) {
2800
+ console.log('Model dropdowns already initialized, skipping');
2801
+ return;
2802
+ }
2803
+
2804
+ console.log('Found model elements:', {
2805
+ settingsProviderInput: !!settingsProviderInput,
2806
+ settingsModelInput: !!settingsModelInput,
2807
+ providerHiddenInput: !!providerHiddenInput,
2808
+ modelHiddenInput: !!modelHiddenInput,
2809
+ providerDropdownList: !!providerDropdownList,
2810
+ modelDropdownList: !!modelDropdownList
2811
+ });
2812
+
2813
+ // Check if elements exist before proceeding
2814
+ if (!settingsProviderInput || !providerDropdownList || !providerHiddenInput) {
2815
+ console.warn('LLM Provider input, dropdown list, or hidden input element not found. Skipping provider initialization.');
2816
+ return; // Don't proceed if required elements are missing
2817
+ }
2818
+
2819
+ if (!settingsModelInput || !modelDropdownList || !modelHiddenInput) {
2820
+ console.warn('LLM Model input, dropdown list, or hidden input element not found. Skipping model initialization.');
2821
+ return; // Don't proceed if required elements are missing
2822
+ }
2823
+
2824
+ // Mark as initialized to prevent redundant setup
2825
+ window.modelDropdownsInitialized = true;
2826
+
2827
+ // Load model options first
2828
+ loadModelOptions().then(() => {
2829
+ console.log(`Models loaded, available options: ${modelOptions.length}`);
2830
+
2831
+ // Get current settings from hidden inputs
2832
+ const currentProvider = providerHiddenInput.value || 'ollama';
2833
+ const currentModel = modelHiddenInput.value || 'gemma3:12b';
2834
+
2835
+ console.log('Current settings:', { provider: currentProvider, model: currentModel });
2836
+
2837
+ // Setup provider dropdown
2838
+ if (settingsProviderInput && providerDropdownList && window.setupCustomDropdown) {
2839
+ // Set hidden input value first for provider (prevents race conditions)
2840
+ if (providerHiddenInput) {
2841
+ console.log('Set provider hidden input value:', currentProvider);
2842
+ providerHiddenInput.value = currentProvider;
2843
+ }
2844
+
2845
+ // Set hidden input value for model too
2846
+ if (modelHiddenInput) {
2847
+ console.log('Set model hidden input value:', currentModel);
2848
+ modelHiddenInput.value = currentModel;
2849
+ }
2850
+
2851
+ // If there are available options, create or update the dropdowns
2852
+ if (MODEL_PROVIDERS && MODEL_PROVIDERS.length > 0) {
2853
+ // Cache references to DOM elements to prevent lookups
2854
+ const providerList = providerDropdownList;
2855
+
2856
+ // Create provider dropdown
2857
+ const providerDropdown = window.setupCustomDropdown(
2858
+ settingsProviderInput,
2859
+ providerList,
2860
+ () => MODEL_PROVIDERS,
2861
+ (value, item) => {
2862
+ console.log('Provider selected:', value);
2863
+
2864
+ // Update hidden input
2865
+ if (providerHiddenInput) {
2866
+ providerHiddenInput.value = value;
2867
+
2868
+ // Trigger filtering of model options
2869
+ filterModelOptionsForProvider(value);
2870
+
2871
+ // Save to localStorage
2872
+ localStorage.setItem('lastUsedProvider', value);
2873
+
2874
+ // Trigger save
2875
+ const changeEvent = new Event('change', { bubbles: true });
2876
+ providerHiddenInput.dispatchEvent(changeEvent);
2877
+ }
2878
+ },
2879
+ false // Don't allow custom values
2880
+ );
2881
+
2882
+ // Set initial value
2883
+ if (currentProvider && providerDropdown.setValue) {
2884
+ console.log('Setting initial provider:', currentProvider);
2885
+ providerDropdown.setValue(currentProvider, false); // Don't fire event
2886
+ // Explicitly set hidden input value on init
2887
+ providerHiddenInput.value = currentProvider.toLowerCase();
2888
+ }
2889
+
2890
+ // --- ADD CHANGE LISTENER TO HIDDEN INPUT ---
2891
+ providerHiddenInput.removeEventListener('change', handleInputChange); // Remove old listener first
2892
+ providerHiddenInput.addEventListener('change', handleInputChange);
2893
+ console.log('Added change listener to hidden provider input:', providerHiddenInput.id);
2894
+ // --- END OF ADDED LISTENER ---
2895
+ }
2896
+ }
2897
+
2898
+ // Create model dropdown with full list of models first
2899
+ if (settingsModelInput && modelDropdownList && modelHiddenInput && window.setupCustomDropdown) {
2900
+ // Initialize the dropdown with ALL models first, don't filter yet
2901
+ const modelDropdownControl = window.setupCustomDropdown(
2902
+ settingsModelInput,
2903
+ modelDropdownList,
2904
+ () => modelOptions.length > 0 ? modelOptions : [
2905
+ { value: 'gpt-4o', label: 'GPT-4o (OpenAI)' },
2906
+ { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (OpenAI)' },
2907
+ { value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (Anthropic)' },
2908
+ { value: 'llama3', label: 'Llama 3 (Ollama)' }
2909
+ ],
2910
+ (value, item) => {
2911
+ console.log('Model selected:', value);
2912
+
2913
+ // Update hidden input
2914
+ if (modelHiddenInput) {
2915
+ modelHiddenInput.value = value;
2916
+
2917
+ // Save to localStorage
2918
+ localStorage.setItem('lastUsedModel', value);
2919
+ }
2920
+ },
2921
+ true // Allow custom values
2922
+ );
2923
+
2924
+ // Set initial model value
2925
+ if (modelDropdownControl) {
2926
+ // Set the current model without filtering first
2927
+ if (currentModel) {
2928
+ console.log('Setting initial model:', currentModel);
2929
+ modelDropdownControl.setValue(currentModel, false); // Don't fire event
2930
+ // Explicitly set hidden input value on init
2931
+ modelHiddenInput.value = currentModel;
2932
+ }
2933
+
2934
+ // Now filter models for the current provider - AFTER setting the initial value
2935
+ setTimeout(() => {
2936
+ filterModelOptionsForProvider(currentProvider);
2937
+ }, 100); // Small delay to ensure value is set first
2938
+
2939
+ // --- ADD CHANGE LISTENER TO HIDDEN INPUT ---
2940
+ modelHiddenInput.removeEventListener('change', handleInputChange); // Remove old listener first
2941
+ modelHiddenInput.addEventListener('change', handleInputChange);
2942
+ console.log('Added change listener to hidden model input:', modelHiddenInput.id);
2943
+ // --- END OF ADDED LISTENER ---
2944
+ }
2945
+
2946
+ // Set up refresh button
2947
+ const refreshBtn = document.querySelector('#llm-model-refresh');
2948
+ if (refreshBtn) {
2949
+ refreshBtn.addEventListener('click', function() {
2950
+ const icon = refreshBtn.querySelector('i');
2951
+ if (icon) icon.className = 'fas fa-spinner fa-spin';
2952
+
2953
+ // Force refresh models
2954
+ loadModelOptions(true).then(() => {
2955
+ if (icon) icon.className = 'fas fa-sync-alt';
2956
+
2957
+ // Re-filter for current provider
2958
+ const currentProvider = providerHiddenInput ?
2959
+ providerHiddenInput.value :
2960
+ settingsProviderInput ? settingsProviderInput.value : 'ollama';
2961
+
2962
+ filterModelOptionsForProvider(currentProvider);
2963
+
2964
+ showAlert('Model list refreshed', 'success');
2965
+ }).catch(error => {
2966
+ console.error('Error refreshing models:', error);
2967
+ if (icon) icon.className = 'fas fa-sync-alt';
2968
+ showAlert('Failed to refresh models: ' + error.message, 'error');
2969
+ });
2970
+ });
2971
+ }
2972
+ }
2973
+
2974
+ // Set up provider change listener after everything is initialized
2975
+ setupProviderChangeListener();
2976
+ }).catch(err => {
2977
+ console.error('Error initializing model dropdowns:', err);
2978
+ // Show a warning to the user
2979
+ showAlert('Failed to load model options. Using fallback values.', 'warning');
2980
+ });
2981
+ }
2982
+
2983
+ /**
2984
+ * Add fallback model based on provider
2985
+ */
2986
+ function addFallbackModel(provider, hiddenInput, visibleInput) {
2987
+ let fallbackModel = '';
2988
+ let displayName = '';
2989
+
2990
+ if (provider === 'OLLAMA') {
2991
+ fallbackModel = 'llama3';
2992
+ displayName = 'Llama 3 (Ollama)';
2993
+ } else if (provider === 'OPENAI') {
2994
+ fallbackModel = 'gpt-3.5-turbo';
2995
+ displayName = 'GPT-3.5 Turbo (OpenAI)';
2996
+ } else if (provider === 'ANTHROPIC') {
2997
+ fallbackModel = 'claude-3-5-sonnet-latest';
2998
+ displayName = 'Claude 3.5 Sonnet (Anthropic)';
2999
+ } else {
3000
+ fallbackModel = 'gpt-3.5-turbo';
3001
+ displayName = 'GPT-3.5 Turbo';
3002
+ }
3003
+
3004
+ if (hiddenInput) {
3005
+ hiddenInput.value = fallbackModel;
3006
+ }
3007
+
3008
+ if (visibleInput) {
3009
+ visibleInput.value = displayName;
3010
+ }
3011
+ }
3012
+
3013
+ /**
3014
+ * Initialize custom search engine dropdowns
3015
+ */
3016
+ function initializeSearchEngineDropdowns() {
3017
+ console.log('Initializing search engine dropdown');
3018
+ // Check for the search engine input field
3019
+ const searchEngineInput = document.getElementById('search.tool');
3020
+ const searchEngineHiddenInput = document.getElementById('search.tool_hidden');
3021
+ const dropdownList = document.getElementById('setting-search-tool-dropdown-list');
3022
+
3023
+ // Skip if already initialized (avoid redundant calls)
3024
+ if (window.searchEngineDropdownInitialized) {
3025
+ console.log('Search engine dropdown already initialized, skipping');
3026
+ return;
3027
+ }
3028
+
3029
+ console.log('Found search engine elements:', {
3030
+ searchEngineInput: !!searchEngineInput,
3031
+ searchEngineHiddenInput: !!searchEngineHiddenInput,
3032
+ dropdownList: !!dropdownList
3033
+ });
3034
+
3035
+ if (!searchEngineInput || !dropdownList || !searchEngineHiddenInput) {
3036
+ console.warn('Search engine input, hidden input, or dropdown list not found. Skipping initialization.');
3037
+ return; // Exit early if required elements are missing
3038
+ }
3039
+
3040
+ // Mark as initialized to prevent redundant calls
3041
+ window.searchEngineDropdownInitialized = true;
3042
+
3043
+ // Set up the dropdown
3044
+ if (window.setupCustomDropdown) {
3045
+ const dropdown = window.setupCustomDropdown(
3046
+ searchEngineInput,
3047
+ dropdownList,
3048
+ () => searchEngineOptions.length > 0 ? searchEngineOptions : [{ value: 'auto', label: 'Auto (Default)' }],
3049
+ (value, item) => {
3050
+ console.log('Search engine selected:', value);
3051
+ // Update the hidden input value
3052
+ searchEngineHiddenInput.value = value;
3053
+ // Trigger a change event on the hidden input to save
3054
+ const changeEvent = new Event('change', { bubbles: true });
3055
+ searchEngineHiddenInput.dispatchEvent(changeEvent);
3056
+ // Save to localStorage
3057
+ localStorage.setItem('lastUsedSearchEngine', value);
3058
+ },
3059
+ false, // Don't allow custom values
3060
+ 'No search engines available.'
3061
+ );
3062
+
3063
+ // Get current value
3064
+ let currentValue = '';
3065
+ if (typeof allSettings !== 'undefined' && Array.isArray(allSettings)) {
3066
+ const currentSetting = allSettings.find(s => s.key === 'search.tool');
3067
+ if (currentSetting) {
3068
+ currentValue = currentSetting.value || '';
3069
+ }
3070
+ }
3071
+ if (!currentValue) {
3072
+ currentValue = localStorage.getItem('lastUsedSearchEngine') || 'auto';
3073
+ }
3074
+
3075
+ // Set initial value
3076
+ if (currentValue && dropdown.setValue) {
3077
+ console.log('Setting initial search engine value:', currentValue);
3078
+ dropdown.setValue(currentValue, false);
3079
+ searchEngineHiddenInput.value = currentValue;
3080
+ }
3081
+
3082
+ // --- ADD CHANGE LISTENER TO HIDDEN INPUT ---
3083
+ searchEngineHiddenInput.removeEventListener('change', handleInputChange); // Remove old listener first
3084
+ searchEngineHiddenInput.addEventListener('change', handleInputChange);
3085
+ console.log('Added change listener to hidden search engine input:', searchEngineHiddenInput.id);
3086
+ // --- END OF ADDED LISTENER ---
3087
+ }
3088
+ }
3089
+
3090
+ /**
3091
+ * Process settings to handle object values
3092
+ */
3093
+ function processSettings(settings) {
3094
+ return settings.map(setting => {
3095
+ const processedSetting = {...setting};
3096
+
3097
+ // Convert object values to JSON strings for display
3098
+ if (typeof processedSetting.value === 'object' && processedSetting.value !== null) {
3099
+ processedSetting.value = JSON.stringify(processedSetting.value, null, 2);
3100
+ }
3101
+
3102
+ // Handle corrupted JSON values (e.g., just "{" or "[" or "[object Object]")
3103
+ if (typeof processedSetting.value === 'string' &&
3104
+ (processedSetting.value === '{' ||
3105
+ processedSetting.value === '[' ||
3106
+ processedSetting.value === '{}' ||
3107
+ processedSetting.value === '[]' ||
3108
+ processedSetting.value === '[object Object]')) {
3109
+
3110
+ console.log(`Detected corrupted JSON value for ${processedSetting.key}: ${processedSetting.value}`);
3111
+
3112
+ // Initialize with empty object for corrupted JSON values
3113
+ if (processedSetting.key.startsWith('report.')) {
3114
+ processedSetting.value = '{}';
3115
+ }
3116
+ }
3117
+
3118
+ return processedSetting;
3119
+ });
3120
+ }
3121
+
3122
+ /**
3123
+ * Add CSS styles for loading indicators and saved state
3124
+ */
3125
+ function addDynamicStyles() {
3126
+ // Create a style element if it doesn't exist
3127
+ let styleEl = document.getElementById('settings-dynamic-styles');
3128
+ if (!styleEl) {
3129
+ styleEl = document.createElement('style');
3130
+ styleEl.id = 'settings-dynamic-styles';
3131
+ document.head.appendChild(styleEl);
3132
+ }
3133
+
3134
+ // Add CSS for saving and success states
3135
+ styleEl.textContent = `
3136
+ .saving {
3137
+ opacity: 0.7;
3138
+ pointer-events: none;
3139
+ position: relative;
3140
+ }
3141
+
3142
+ .saving::after {
3143
+ content: '';
3144
+ position: absolute;
3145
+ top: 50%;
3146
+ right: 10px;
3147
+ width: 16px;
3148
+ height: 16px;
3149
+ margin-top: -8px;
3150
+ border: 2px solid rgba(0, 123, 255, 0.1);
3151
+ border-top-color: #007bff;
3152
+ border-radius: 50%;
3153
+ animation: spinner 0.8s linear infinite;
3154
+ z-index: 10;
3155
+ }
3156
+
3157
+ .save-success {
3158
+ border-color: #28a745 !important;
3159
+ transition: border-color 0.3s;
3160
+ }
3161
+
3162
+ @keyframes spinner {
3163
+ to { transform: rotate(360deg); }
3164
+ }
3165
+
3166
+ .spinner {
3167
+ width: 40px;
3168
+ height: 40px;
3169
+ border: 3px solid rgba(255, 255, 255, 0.1);
3170
+ border-radius: 50%;
3171
+ border-top-color: var(--accent-primary);
3172
+ animation: spin 1s ease-in-out infinite;
3173
+ margin: 0 auto 1rem auto;
3174
+ display: block;
3175
+ }
3176
+
3177
+ .settings-item .checkbox-label {
3178
+ margin-top: 8px;
3179
+ padding-left: 0;
3180
+ }
3181
+
3182
+ // Add styles for the loading spinner
3183
+ const spinnerStyles =
3184
+ '.saving {' +
3185
+ ' position: relative;' +
3186
+ '}' +
3187
+ '' +
3188
+ '.saving:before {' +
3189
+ ' content: \'\';' +
3190
+ ' position: absolute;' +
3191
+ ' left: -25px;' +
3192
+ ' top: 50%;' +
3193
+ ' transform: translateY(-50%);' +
3194
+ ' width: 16px;' +
3195
+ ' height: 16px;' +
3196
+ ' border: 2px solid rgba(255, 255, 255, 0.3);' +
3197
+ ' border-radius: 50%;' +
3198
+ ' border-top-color: #fff;' +
3199
+ ' animation: spinner .6s linear infinite;' +
3200
+ ' z-index: 10;' +
3201
+ '}' +
3202
+ '' +
3203
+ '.checkbox-label.saving:before {' +
3204
+ ' left: -25px;' +
3205
+ ' top: 50%;' +
3206
+ '}' +
3207
+ '' +
3208
+ '@keyframes spinner {' +
3209
+ ' to {transform: translateY(-50%) rotate(360deg);}' +
3210
+ '}';
3211
+
3212
+ // Add the styles to the head
3213
+ const style = document.createElement('style');
3214
+ style.textContent = spinnerStyles;
3215
+ document.head.appendChild(style);
3216
+ `;
3217
+ }
3218
+
3219
+ // Initialize dynamic styles
3220
+ addDynamicStyles();
3221
+
3222
+ /**
3223
+ * Initialize the settings component
3224
+ */
3225
+ function initializeSettings() {
3226
+ // Get DOM elements
3227
+ settingsForm = document.querySelector('form');
3228
+ settingsContent = document.getElementById('settings-content');
3229
+ settingsSearch = document.getElementById('settings-search');
3230
+ settingsTabs = document.querySelectorAll('.settings-tab');
3231
+ settingsAlert = document.getElementById('settings-alert');
3232
+ rawConfigToggle = document.getElementById('toggle-raw-config');
3233
+ rawConfigSection = document.getElementById('raw-config');
3234
+ rawConfigEditor = document.getElementById('raw_config_editor');
3235
+
3236
+ // Add dynamic styles immediately
3237
+ addDynamicStyles();
3238
+
3239
+ // Initialize range inputs to display their values
3240
+ initRangeInputs();
3241
+
3242
+ // Initialize accordion behavior
3243
+ initAccordions();
3244
+
3245
+ // Initialize JSON handling
3246
+ initJsonFormatting();
3247
+
3248
+ // Load settings from API if on settings dashboard
3249
+ if (settingsContent) {
3250
+ // First fetch the model and search engine data to have it ready
3251
+ // when needed, but don't wait for it to complete
3252
+ Promise.all([fetchModelProviders(), fetchSearchEngines()]).then(() => {
3253
+ // Then load settings
3254
+ loadSettings();
3255
+ }).catch(err => {
3256
+ console.error("Error fetching providers/engines initially", err);
3257
+ // Still try to load settings even if fetching options fails
3258
+ loadSettings();
3259
+ });
3260
+ }
3261
+
3262
+ // Handle tab switching
3263
+ if (settingsTabs) {
3264
+ settingsTabs.forEach(tab => {
3265
+ tab.addEventListener('click', () => {
3266
+ // Remove active class from all tabs
3267
+ settingsTabs.forEach(t => t.classList.remove('active'));
3268
+
3269
+ // Add active class to clicked tab
3270
+ tab.classList.add('active');
3271
+
3272
+ // Update active tab and re-render
3273
+ activeTab = tab.dataset.tab;
3274
+ renderSettingsByTab(activeTab);
3275
+
3276
+ // Set a small timeout to ensure DOM is ready before initializing
3277
+ setTimeout(() => {
3278
+ // Initialize dropdowns after rendering content
3279
+ // Moved dropdown init inside loadSettings success callback
3280
+ // if (activeTab === 'llm' || activeTab === 'all') {
3281
+ // initializeModelDropdowns();
3282
+ // }
3283
+ // if (activeTab === 'search' || activeTab === 'all') {
3284
+ // initializeSearchEngineDropdowns();
3285
+ // }
3286
+
3287
+ // Re-initialize auto-save handlers after tab switch and render
3288
+ initAutoSaveHandlers();
3289
+ // Setup refresh buttons after dropdowns might have been created
3290
+ setupRefreshButtons();
3291
+ }, 100); // Reduced timeout slightly
3292
+ });
3293
+ });
3294
+ }
3295
+
3296
+ // Handle search filtering
3297
+ if (settingsSearch) {
3298
+ settingsSearch.addEventListener('input', handleSearchInput);
3299
+ }
3300
+
3301
+ // Handle reset to defaults button
3302
+ const resetToDefaultsButton = document.getElementById('reset-to-defaults-button');
3303
+ if (resetToDefaultsButton) {
3304
+ resetToDefaultsButton.addEventListener('click', handleResetToDefaults);
3305
+ }
3306
+
3307
+ // Add a fix corrupted settings button
3308
+ const fixCorruptedButton = document.createElement('button');
3309
+ fixCorruptedButton.setAttribute('type', 'button');
3310
+ fixCorruptedButton.setAttribute('id', 'fix-corrupted-button');
3311
+ fixCorruptedButton.className = 'btn btn-info';
3312
+ fixCorruptedButton.innerHTML = '<i class="fas fa-wrench"></i> Fix Corrupted Settings';
3313
+ fixCorruptedButton.addEventListener('click', handleFixCorruptedSettings);
3314
+
3315
+ // Insert it after the reset to defaults button
3316
+ if (resetToDefaultsButton) {
3317
+ resetToDefaultsButton.insertAdjacentElement('afterend', fixCorruptedButton);
3318
+ }
3319
+
3320
+ // Handle raw config toggle
3321
+ if (rawConfigToggle) {
3322
+ rawConfigToggle.addEventListener('click', toggleRawConfig);
3323
+ }
3324
+
3325
+ // Initialize specific settings page form handlers
3326
+ initSpecificSettingsForm();
3327
+
3328
+ // Handle form submission
3329
+ if (settingsForm) {
3330
+ settingsForm.addEventListener('submit', handleSettingsSubmit);
3331
+ }
3332
+
3333
+ // Add click handler for the logo to navigate home
3334
+ const logoLink = document.getElementById('logo-link');
3335
+ if (logoLink) {
3336
+ logoLink.addEventListener('click', () => {
3337
+ window.location.href = '/research/';
3338
+ });
3339
+ }
3340
+
3341
+ // --- MODIFICATION START: Call initAutoSaveHandlers at the end of initializeSettings ---
3342
+ // Initialize auto-save handlers after all other setup
3343
+ initAutoSaveHandlers();
3344
+ // --- MODIFICATION END ---
3345
+ }
3346
+
3347
+ // Initialize on DOM content loaded
3348
+ // --- MODIFICATION START: Ensure initialization order ---
3349
+ // Ensure initialization happens after DOM content is loaded
3350
+ if (document.readyState === 'loading') {
3351
+ document.addEventListener('DOMContentLoaded', initializeSettings);
3352
+ } else {
3353
+ // DOM is already loaded, run initialize
3354
+ initializeSettings();
3355
+ }
3356
+ // --- MODIFICATION END ---
3357
+
3358
+ // Expose the setupCustomDropdowns function for other modules to use
3359
+ window.setupSettingsDropdowns = initializeModelDropdowns;
3360
+
3361
+ /**
3362
+ * Show an alert message at the top of the settings form
3363
+ * @param {string} message - The message to display
3364
+ * @param {string} type - The alert type: success, error, warning, info
3365
+ * @param {boolean} skipIfToastShown - Whether to skip showing this alert if a toast was already shown
3366
+ */
3367
+ function showAlert(message, type, skipIfToastShown = true) {
3368
+ // If window.ui.showAlert exists, use it
3369
+ if (window.ui && window.ui.showAlert) {
3370
+ window.ui.showAlert(message, type, skipIfToastShown);
3371
+ return;
3372
+ }
3373
+
3374
+ // Otherwise fallback to old implementation (this shouldn't happen once ui.js is loaded)
3375
+ // If we're showing a toast and we want to skip the regular alert, just return
3376
+ if (skipIfToastShown && window.ui && window.ui.showMessage) {
3377
+ return;
3378
+ }
3379
+
3380
+ // Find the alert container - look for filtered settings alert first
3381
+ let alertContainer = document.getElementById('filtered-settings-alert');
3382
+
3383
+ // If not found, fall back to the regular alert
3384
+ if (!alertContainer) {
3385
+ alertContainer = document.getElementById('settings-alert');
3386
+ }
3387
+
3388
+ if (!alertContainer) return;
3389
+
3390
+ // Clear any existing alerts
3391
+ alertContainer.innerHTML = '';
3392
+
3393
+ // Create alert element
3394
+ const alert = document.createElement('div');
3395
+ alert.className = `alert alert-${type}`;
3396
+ alert.innerHTML = `<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> ${message}`;
3397
+
3398
+ // Add a close button
3399
+ const closeBtn = document.createElement('span');
3400
+ closeBtn.className = 'alert-close';
3401
+ closeBtn.innerHTML = '&times;';
3402
+ closeBtn.addEventListener('click', () => {
3403
+ alert.remove();
3404
+ alertContainer.style.display = 'none';
3405
+ });
3406
+
3407
+ alert.appendChild(closeBtn);
3408
+
3409
+ // Add to container
3410
+ alertContainer.appendChild(alert);
3411
+ alertContainer.style.display = 'block';
3412
+
3413
+ // Auto-hide after 5 seconds
3414
+ setTimeout(() => {
3415
+ alert.remove();
3416
+ if (alertContainer.children.length === 0) {
3417
+ alertContainer.style.display = 'none';
3418
+ }
3419
+ }, 5000);
3420
+ }
3421
+
3422
+ /**
3423
+ * Set up custom dropdowns for settings
3424
+ */
3425
+ function setupCustomDropdowns() {
3426
+ // Find all custom dropdowns in the settings form
3427
+ const customDropdowns = document.querySelectorAll('.custom-dropdown');
3428
+
3429
+ // Process each dropdown
3430
+ customDropdowns.forEach(dropdown => {
3431
+ const dropdownInput = dropdown.querySelector('.custom-dropdown-input');
3432
+ const dropdownList = dropdown.querySelector('.custom-dropdown-list');
3433
+
3434
+ if (!dropdownInput || !dropdownList) return;
3435
+
3436
+ // Get the setting key from the data attribute or input ID
3437
+ const settingKey = dropdownInput.getAttribute('data-setting-key') || dropdownInput.id;
3438
+ if (!settingKey) return;
3439
+
3440
+ console.log('Setting up custom dropdown for:', settingKey);
3441
+
3442
+ // Get current setting value from settings or localStorage
3443
+ let currentValue = '';
3444
+
3445
+ // Try to get from allSettings first if available
3446
+ if (typeof allSettings !== 'undefined' && Array.isArray(allSettings)) {
3447
+ const currentSetting = allSettings.find(s => s.key === settingKey);
3448
+ if (currentSetting) {
3449
+ currentValue = currentSetting.value || '';
3450
+ }
3451
+ }
3452
+
3453
+ // Fallback to localStorage values if we don't have a value yet
3454
+ if (!currentValue) {
3455
+ if (settingKey === 'llm.model') {
3456
+ currentValue = localStorage.getItem('lastUsedModel') || '';
3457
+ } else if (settingKey === 'llm.provider') {
3458
+ currentValue = localStorage.getItem('lastUsedProvider') || '';
3459
+ } else if (settingKey === 'search.tool') {
3460
+ currentValue = localStorage.getItem('lastUsedSearchEngine') || '';
3461
+ }
3462
+ }
3463
+
3464
+ // Get the hidden input
3465
+ const hiddenInput = document.getElementById(`${dropdownInput.id}_hidden`);
3466
+ if (!hiddenInput) {
3467
+ console.warn(`Hidden input not found for dropdown: ${dropdownInput.id}`);
3468
+ return; // Skip if hidden input doesn't exist
3469
+ }
3470
+
3471
+ // Set up options source based on setting key
3472
+ let optionsSource = [];
3473
+ let allowCustom = false;
3474
+
3475
+ if (settingKey === 'llm.model') {
3476
+ // For model dropdown, use the model options from cache or fallback
3477
+ optionsSource = typeof modelOptions !== 'undefined' && modelOptions.length > 0 ?
3478
+ modelOptions : [
3479
+ { value: 'gpt-4o', label: 'GPT-4o (OpenAI)' },
3480
+ { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (OpenAI)' },
3481
+ { value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (Anthropic)' },
3482
+ { value: 'llama3', label: 'Llama 3 (Ollama)' }
3483
+ ];
3484
+ allowCustom = true;
3485
+
3486
+ // Set up refresh button if it exists
3487
+ const refreshBtn = dropdown.querySelector('.dropdown-refresh-button');
3488
+ if (refreshBtn) {
3489
+ refreshBtn.addEventListener('click', function() {
3490
+ const icon = refreshBtn.querySelector('i');
3491
+ if (icon) icon.className = 'fas fa-spinner fa-spin';
3492
+
3493
+ // Force refresh of model options
3494
+ if (typeof loadModelOptions === 'function') {
3495
+ loadModelOptions(true).then(() => {
3496
+ if (icon) icon.className = 'fas fa-sync-alt';
3497
+
3498
+ // Force dropdown update
3499
+ const event = new Event('click', { bubbles: true });
3500
+ dropdownInput.dispatchEvent(event);
3501
+ }).catch(error => {
3502
+ console.error('Error refreshing models:', error);
3503
+ if (icon) icon.className = 'fas fa-sync-alt';
3504
+ if (typeof showAlert === 'function') {
3505
+ showAlert('Failed to refresh models: ' + error.message, 'error');
3506
+ }
3507
+ });
3508
+ } else {
3509
+ if (icon) icon.className = 'fas fa-sync-alt';
3510
+ }
3511
+ });
3512
+ }
3513
+ } else if (settingKey === 'llm.provider') {
3514
+ // Special handling for provider dropdown
3515
+ const MODEL_PROVIDERS = [
3516
+ { value: 'ollama', label: 'Ollama (Local)' },
3517
+ { value: 'openai', label: 'OpenAI (Cloud)' },
3518
+ { value: 'anthropic', label: 'Anthropic (Cloud)' },
3519
+ { value: 'openai_endpoint', label: 'Custom OpenAI Endpoint' }
3520
+ ];
3521
+
3522
+ optionsSource = MODEL_PROVIDERS;
3523
+
3524
+ // Try to get options from settings if available
3525
+ if (typeof allSettings !== 'undefined' && Array.isArray(allSettings)) {
3526
+ const availableProviders = allSettings.find(s => s.key === 'llm.provider');
3527
+ if (availableProviders && availableProviders.options && availableProviders.options.length > 0) {
3528
+ optionsSource = availableProviders.options.map(opt => ({
3529
+ value: opt.value,
3530
+ label: opt.label
3531
+ }));
3532
+ }
3533
+ }
3534
+ } else if (settingKey === 'search.tool') {
3535
+ optionsSource = typeof searchEngineOptions !== 'undefined' && searchEngineOptions.length > 0 ?
3536
+ searchEngineOptions : [
3537
+ { value: 'google_pse', label: 'Google Programmable Search' },
3538
+ { value: 'duckduckgo', label: 'DuckDuckGo' },
3539
+ { value: 'auto', label: 'Auto (Default)' }
3540
+ ];
3541
+ }
3542
+
3543
+ console.log(`Setting up dropdown for ${settingKey} with ${optionsSource.length} options`);
3544
+
3545
+ // Initialize the dropdown
3546
+ if (window.setupCustomDropdown) {
3547
+ const dropdown = window.setupCustomDropdown(
3548
+ dropdownInput,
3549
+ dropdownList,
3550
+ () => optionsSource,
3551
+ (value, item) => {
3552
+ console.log(`Dropdown ${settingKey} selected:`, value);
3553
+ // --- MODIFICATION START: Removed hiddenInput retrieval, already have it ---
3554
+ const hiddenInput = document.getElementById(`${dropdownInput.id}_hidden`);
3555
+ // --- MODIFICATION END ---
3556
+
3557
+ // --- MODIFICATION START: Update hidden input and trigger change ---
3558
+ if (hiddenInput) {
3559
+ hiddenInput.value = value;
3560
+ const changeEvent = new Event('change', { bubbles: true });
3561
+ hiddenInput.dispatchEvent(changeEvent);
3562
+ }
3563
+ // --- MODIFICATION END ---
3564
+
3565
+ // For provider changes, update model options
3566
+ if (settingKey === 'llm.provider' && typeof filterModelOptionsForProvider === 'function') {
3567
+ filterModelOptionsForProvider(value);
3568
+ }
3569
+
3570
+ // Save to localStorage for persistence
3571
+ if (settingKey === 'llm.model') {
3572
+ localStorage.setItem('lastUsedModel', value);
3573
+ } else if (settingKey === 'llm.provider') {
3574
+ localStorage.setItem('lastUsedProvider', value);
3575
+ } else if (settingKey === 'search.tool') {
3576
+ localStorage.setItem('lastUsedSearchEngine', value);
3577
+ }
3578
+ },
3579
+ allowCustom
3580
+ );
3581
+
3582
+ // Set initial value
3583
+ if (currentValue && dropdown.setValue) {
3584
+ console.log(`Setting initial value for ${settingKey}:`, currentValue);
3585
+ dropdown.setValue(currentValue, false); // Don't fire event on init
3586
+ // --- MODIFICATION START: Set hidden input initial value ---
3587
+ if (hiddenInput) {
3588
+ hiddenInput.value = currentValue;
3589
+ console.log('Set initial hidden input value for', settingKey, 'to', currentValue);
3590
+ }
3591
+ // --- MODIFICATION END ---
3592
+ }
3593
+
3594
+ // --- MODIFICATION START: Add listener to hidden input in initAutoSaveHandlers ---
3595
+ // The listener is added globally in initAutoSaveHandlers now.
3596
+ // Ensure initAutoSaveHandlers is called *after* setupCustomDropdowns.
3597
+ // --- MODIFICATION END ---
3598
+ }
3599
+ });
3600
+
3601
+ // --- MODIFICATION START: Call initAutoSaveHandlers after setup ---
3602
+ // Ensure initAutoSaveHandlers is called after dropdowns are set up
3603
+ // It might be better to call initAutoSaveHandlers once after all rendering and setup is done.
3604
+ // Let's move the call within initializeSettings() to ensure order.
3605
+ initAutoSaveHandlers();
3606
+ // --- MODIFICATION END ---
3607
+ }
3608
+
3609
+ /**
3610
+ * Filter model options based on the selected provider
3611
+ * @param {string} provider - The provider to filter models by
3612
+ */
3613
+ function filterModelOptionsForProvider(provider) {
3614
+ const providerUpper = provider ? provider.toUpperCase() : ''; // Handle potential null/undefined
3615
+ console.log('Filtering models for provider:', providerUpper);
3616
+
3617
+ // Get model dropdown elements using ID
3618
+ const modelInput = document.getElementById('llm.model');
3619
+ const modelDropdownList = document.getElementById('setting-llm-model-dropdown-list'); // Correct ID based on template generation
3620
+ const modelHiddenInput = document.getElementById('llm.model_hidden');
3621
+
3622
+ if (!modelInput || !modelDropdownList) { // Use correct variable name
3623
+ console.warn('Model input or list not found when filtering.');
3624
+ return;
3625
+ }
3626
+
3627
+ // Check if dropdown is currently open
3628
+ const isDropdownOpen = window.getComputedStyle(modelDropdownList).display !== 'none';
3629
+ console.log('Dropdown is currently:', isDropdownOpen ? 'open' : 'closed');
3630
+
3631
+ // Filter the models based on provider
3632
+ const filteredModels = modelOptions.filter(model => {
3633
+ if (!model || typeof model !== 'object') return false;
3634
+
3635
+ // For Ollama, use more flexible matching due to model name variations
3636
+ if (providerUpper === 'OLLAMA') {
3637
+ // Check model provider property first
3638
+ if (model.provider && model.provider.toUpperCase() === 'OLLAMA') {
3639
+ return true;
3640
+ }
3641
+
3642
+ // Check label for Ollama mentions
3643
+ if (model.label && model.label.toUpperCase().includes('OLLAMA')) {
3644
+ return true;
3645
+ }
3646
+
3647
+ // Check value for common Ollama model name patterns
3648
+ if (model.value) {
3649
+ const value = model.value.toLowerCase();
3650
+ // Common Ollama model name patterns
3651
+ if (value.includes('llama') || value.includes('mistral') ||
3652
+ value.includes('gemma') || value.includes('falcon') ||
3653
+ value.includes('codellama') || value.includes('phi')) {
3654
+ return true;
3655
+ }
3656
+ }
3657
+
3658
+ return false;
3659
+ }
3660
+
3661
+ // For other providers, use standard matching
3662
+ if (model.provider) {
3663
+ return model.provider.toUpperCase() === providerUpper;
3664
+ }
3665
+
3666
+ // If provider is missing, check label for provider hints
3667
+ if (model.label) {
3668
+ const label = model.label.toUpperCase();
3669
+ if (providerUpper === 'OPENAI' && label.includes('OPENAI'))
3670
+ return true;
3671
+ if (providerUpper === 'ANTHROPIC' && (label.includes('ANTHROPIC') || label.includes('CLAUDE')))
3672
+ return true;
3673
+ }
3674
+
3675
+ return false;
3676
+ });
3677
+
3678
+ console.log(`Filtered models for ${providerUpper}:`, filteredModels.length, 'models');
3679
+
3680
+ // Try to update the dropdown options without reinitializing if possible
3681
+ if (window.updateDropdownOptions && typeof window.updateDropdownOptions === 'function') {
3682
+ console.log('Using updateDropdownOptions to preserve dropdown state');
3683
+ window.updateDropdownOptions(modelInput, filteredModels);
3684
+
3685
+ // Try to maintain the current selection if applicable
3686
+ const currentModel = modelHiddenInput ? modelHiddenInput.value : null;
3687
+ if (currentModel) {
3688
+ // Check if current model is valid for this provider
3689
+ const isValid = filteredModels.some(m => m.value === currentModel);
3690
+ if (!isValid && filteredModels.length > 0) {
3691
+ // Select first available model if current is not valid
3692
+ const firstModel = filteredModels[0].value;
3693
+ console.log(`Current model ${currentModel} invalid for provider ${providerUpper}. Setting to first available: ${firstModel}`);
3694
+ modelHiddenInput.value = firstModel;
3695
+ modelInput.value = filteredModels[0].label || firstModel;
3696
+ }
3697
+ }
3698
+
3699
+ // If dropdown was open, ensure it stays open
3700
+ if (isDropdownOpen) {
3701
+ setTimeout(() => {
3702
+ if (modelDropdownList.style.display === 'none') {
3703
+ console.log('Reopening dropdown that was closed during update');
3704
+ modelDropdownList.style.display = 'block';
3705
+ }
3706
+ }, 50);
3707
+ }
3708
+
3709
+ return;
3710
+ }
3711
+
3712
+ // Backup method - reinitialize the dropdown but try to preserve open state
3713
+ if (window.setupCustomDropdown) {
3714
+ console.log('Reinitializing model dropdown with filtered models');
3715
+
3716
+ // Store the returned control object
3717
+ const modelDropdownControl = window.setupCustomDropdown(
3718
+ modelInput,
3719
+ modelDropdownList, // Use correct variable name
3720
+ () => filteredModels.length > 0 ? filteredModels : [
3721
+ { value: 'no-models', label: 'No models available for this provider' }
3722
+ ],
3723
+ (value, item) => {
3724
+ console.log('Selected model:', value);
3725
+ // Save the selection
3726
+ if (modelHiddenInput) { // Use the variable we already have
3727
+ modelHiddenInput.value = value;
3728
+
3729
+ // Trigger change event to save
3730
+ const changeEvent = new Event('change', { bubbles: true });
3731
+ modelHiddenInput.dispatchEvent(changeEvent);
3732
+ }
3733
+ },
3734
+ true // Allow custom values
3735
+ );
3736
+
3737
+ // Try to maintain the current selection if applicable
3738
+ const currentModel = modelHiddenInput ? modelHiddenInput.value : null;
3739
+
3740
+ if (currentModel && modelDropdownControl && modelDropdownControl.setValue) {
3741
+ // Check if current model is valid for this provider
3742
+ const isValid = filteredModels.some(m => m.value === currentModel);
3743
+ if (isValid) {
3744
+ console.log(`Setting model value to currently selected: ${currentModel}`);
3745
+ modelDropdownControl.setValue(currentModel, false);
3746
+ } else {
3747
+ // Select first available model
3748
+ // *** FIX: Check if filteredModels has elements ***
3749
+ if (filteredModels.length > 0) {
3750
+ const firstModel = filteredModels[0].value;
3751
+ console.log(`Current model ${currentModel} invalid for provider ${providerUpper}. Setting to first available: ${firstModel}`);
3752
+ modelDropdownControl.setValue(firstModel, false); // DON'T fire event, avoid loop
3753
+ } else {
3754
+ // No models available, clear the input
3755
+ console.log(`No models found for provider ${providerUpper}. Clearing model selection.`);
3756
+ modelDropdownControl.setValue("", false);
3757
+ }
3758
+ }
3759
+ }
3760
+
3761
+ // If dropdown was open, force it to reopen
3762
+ if (isDropdownOpen) {
3763
+ setTimeout(() => {
3764
+ console.log('Reopening dropdown that was closed during reinitialization');
3765
+ modelDropdownList.style.display = 'block';
3766
+ }, 100);
3767
+ }
3768
+ }
3769
+
3770
+ // Also update any provider-dependent UI
3771
+ updateProviderDependentUI(providerUpper);
3772
+ }
3773
+
3774
+ /**
3775
+ * Update any UI elements that depend on the provider selection
3776
+ */
3777
+ function updateProviderDependentUI(provider) {
3778
+ // Show/hide custom endpoint input if needed
3779
+ const endpointContainer = document.querySelector('#endpoint-container');
3780
+ if (endpointContainer) {
3781
+ if (provider === 'OPENAI_ENDPOINT') {
3782
+ endpointContainer.style.display = 'block';
3783
+ } else {
3784
+ endpointContainer.style.display = 'none';
3785
+ }
3786
+ }
3787
+ }
3788
+
3789
+ /**
3790
+ * Set up event listener for provider changes to update model options
3791
+ */
3792
+ function setupProviderChangeListener() {
3793
+ console.log('Setting up provider change listener');
3794
+ const providerInput = document.getElementById('llm.provider'); // Use ID selector
3795
+ const providerHiddenInput = document.getElementById('llm.provider_hidden');
3796
+
3797
+ // Function to handle the change
3798
+ const handleProviderChange = (selectedValue) => {
3799
+ console.log('Provider changed to:', selectedValue);
3800
+ if (typeof filterModelOptionsForProvider === 'function') {
3801
+ filterModelOptionsForProvider(selectedValue);
3802
+ }
3803
+ // Update other UI elements if needed
3804
+ updateProviderDependentUI(selectedValue ? selectedValue.toUpperCase() : '');
3805
+ // No need to explicitly save here, the main auto-save handler for hidden input does it
3806
+ };
3807
+
3808
+ if (providerHiddenInput) {
3809
+ // Monitor the hidden input for changes (triggered by custom dropdown selection)
3810
+ providerHiddenInput.removeEventListener('change', (e) => handleProviderChange(e.target.value)); // Remove previous if any
3811
+ providerHiddenInput.addEventListener('change', (e) => handleProviderChange(e.target.value));
3812
+ console.log('Re-added provider change listener to hidden input:', providerHiddenInput.id);
3813
+ } else if (providerInput && providerInput.tagName === 'SELECT') {
3814
+ // Fallback for standard select (shouldn't happen with custom dropdown)
3815
+ providerInput.removeEventListener('change', (e) => handleProviderChange(e.target.value));
3816
+ providerInput.addEventListener('change', (e) => handleProviderChange(e.target.value));
3817
+ console.log('Added change listener to standard provider select');
3818
+ } else {
3819
+ console.warn('Could not find provider input (hidden or standard select) to attach change listener.');
3820
+ }
3821
+ }
3822
+
3823
+ /**
3824
+ * Constants - model providers
3825
+ */
3826
+ const MODEL_PROVIDERS = [
3827
+ { value: 'OLLAMA', label: 'Ollama (Local)' },
3828
+ { value: 'OPENAI', label: 'OpenAI (Cloud)' },
3829
+ { value: 'ANTHROPIC', label: 'Anthropic (Cloud)' },
3830
+ { value: 'OPENAI_ENDPOINT', label: 'Custom OpenAI Endpoint' },
3831
+ { value: 'VLLM', label: 'vLLM (Local)' },
3832
+ { value: 'LMSTUDIO', label: 'LM Studio (Local)' },
3833
+ { value: 'LLAMACPP', label: 'Llama.cpp (Local)' }
3834
+ ];
3835
+
3836
+ /**
3837
+ * Load model options for the dropdown
3838
+ * @param {boolean} forceRefresh - Force refresh of model options
3839
+ * @returns {Promise} Promise that resolves with model options
3840
+ */
3841
+ function loadModelOptions(forceRefresh = false) {
3842
+ // Check if we already have cached models and haven't forced a refresh
3843
+ const cachedModels = getCachedData('deepResearch.availableModels');
3844
+ const cacheTimestamp = getCachedData('deepResearch.cacheTimestamp');
3845
+
3846
+ if (!forceRefresh && cachedModels && Array.isArray(cachedModels) && cachedModels.length > 0 &&
3847
+ cacheTimestamp && (Date.now() - cacheTimestamp < 3600000)) { // 1 hour cache
3848
+ console.log('Using cached model options, count:', cachedModels.length);
3849
+ modelOptions = cachedModels;
3850
+ return Promise.resolve(cachedModels);
3851
+ }
3852
+
3853
+ console.log('Loading model options from API' + (forceRefresh ? ' (forced refresh)' : ''));
3854
+
3855
+ return fetchModelProviders(forceRefresh)
3856
+ .then(data => {
3857
+ // Don't overwrite our model options if the result is empty
3858
+ if (data && Array.isArray(data) && data.length > 0) {
3859
+ modelOptions = data;
3860
+ cacheData('deepResearch.availableModels', data);
3861
+ console.log('Stored model options, count:', data.length);
3862
+ } else {
3863
+ console.warn('API returned empty model data, keeping existing options');
3864
+ }
3865
+ return modelOptions;
3866
+ })
3867
+ .catch(error => {
3868
+ console.error('Error loading model options:', error);
3869
+ // Log but don't throw, so we can continue with default models if needed
3870
+ if (!modelOptions || modelOptions.length === 0) {
3871
+ console.log('Using fallback model options due to error');
3872
+ modelOptions = [
3873
+ { value: 'gpt-4o', label: 'GPT-4o (OpenAI)', provider: 'OPENAI' },
3874
+ { value: 'gpt-3.5-turbo', label: 'GPT-3.5 Turbo (OpenAI)', provider: 'OPENAI' },
3875
+ { value: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (Anthropic)', provider: 'ANTHROPIC' },
3876
+ { value: 'llama3', label: 'Llama 3 (Ollama)', provider: 'OLLAMA' },
3877
+ { value: 'mistral', label: 'Mistral (Ollama)', provider: 'OLLAMA' },
3878
+ { value: 'gemma:latest', label: 'Gemma (Ollama)', provider: 'OLLAMA' }
3879
+ ];
3880
+ }
3881
+ return modelOptions;
3882
+ });
3883
+ }
3884
+
3885
+ /**
3886
+ * Create a refresh button for a dropdown input
3887
+ * @param {string} inputId - The ID of the input to create a refresh button for
3888
+ * @param {Function} fetchFunc - The function to call when the button is clicked
3889
+ */
3890
+ function createRefreshButton(inputId, fetchFunc) {
3891
+ console.log('Creating refresh button for', inputId);
3892
+ // Check if the input exists
3893
+ const input = document.getElementById(inputId);
3894
+ if (!input) {
3895
+ console.warn(`Cannot create refresh button for non-existent input: ${inputId}`);
3896
+ return null;
3897
+ }
3898
+
3899
+ // Find the parent container
3900
+ const container = input.closest('.form-group');
3901
+ if (!container) {
3902
+ console.warn(`Cannot find container for input: ${inputId}`);
3903
+ return null;
3904
+ }
3905
+
3906
+ // Create a new button
3907
+ const refreshBtn = document.createElement('button');
3908
+ refreshBtn.type = 'button';
3909
+ refreshBtn.id = inputId + '-refresh';
3910
+ refreshBtn.className = 'custom-dropdown-refresh-btn';
3911
+ refreshBtn.setAttribute('aria-label', 'Refresh options');
3912
+ refreshBtn.style.display = 'flex';
3913
+ refreshBtn.style.alignItems = 'center';
3914
+ refreshBtn.style.justifyContent = 'center';
3915
+ refreshBtn.style.width = '38px';
3916
+ refreshBtn.style.height = '38px';
3917
+ refreshBtn.style.backgroundColor = 'var(--bg-tertiary, #2a2a3a)';
3918
+ refreshBtn.style.border = '1px solid var(--border-color, #343452)';
3919
+ refreshBtn.style.borderRadius = '6px';
3920
+ refreshBtn.style.cursor = 'pointer';
3921
+ refreshBtn.style.marginLeft = '8px';
3922
+
3923
+ // Add icon to the button
3924
+ const icon = document.createElement('i');
3925
+ icon.className = 'fas fa-sync-alt';
3926
+ refreshBtn.appendChild(icon);
3927
+
3928
+ // Add event listener to the button
3929
+ refreshBtn.addEventListener('click', function(e) {
3930
+ e.preventDefault();
3931
+ e.stopPropagation();
3932
+
3933
+ console.log('Refresh button clicked for', inputId);
3934
+ icon.className = 'fas fa-spinner fa-spin';
3935
+
3936
+ // Reset initialization flags
3937
+ if (inputId.includes('llm') || inputId.includes('model')) {
3938
+ window.modelDropdownsInitialized = false;
3939
+ } else if (inputId.includes('search') || inputId.includes('tool')) {
3940
+ window.searchEngineDropdownInitialized = false;
3941
+ }
3942
+
3943
+ // Call the function directly as a parameter
3944
+ fetchFunc(true).then(() => {
3945
+ icon.className = 'fas fa-sync-alt';
3946
+
3947
+ // Re-initialize appropriate dropdowns
3948
+ if (inputId.includes('llm') || inputId.includes('model')) {
3949
+ initializeModelDropdowns();
3950
+ } else if (inputId.includes('search') || inputId.includes('tool')) {
3951
+ initializeSearchEngineDropdowns();
3952
+ }
3953
+
3954
+ showAlert(`Options refreshed`, 'success');
3955
+ }).catch(error => {
3956
+ console.error('Error refreshing options:', error);
3957
+ icon.className = 'fas fa-sync-alt';
3958
+ showAlert('Failed to refresh options', 'error');
3959
+ });
3960
+ });
3961
+
3962
+ // Find the input wrapper or create one
3963
+ let inputWrapper = input.parentElement;
3964
+ if (inputWrapper.classList.contains('custom-dropdown-input')) {
3965
+ inputWrapper = inputWrapper.parentElement;
3966
+ }
3967
+
3968
+ if (inputWrapper) {
3969
+ // Add the button after the input
3970
+ inputWrapper.style.display = 'flex';
3971
+ inputWrapper.style.alignItems = 'center';
3972
+ inputWrapper.style.gap = '8px';
3973
+ inputWrapper.appendChild(refreshBtn);
3974
+ console.log('Created new refresh button for:', inputId);
3975
+ return refreshBtn;
3976
+ }
3977
+
3978
+ console.warn(`Could not find a suitable place to add refresh button for ${inputId}`);
3979
+ return null;
3980
+ }
3981
+ })();