local-deep-research 0.1.26__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- local_deep_research/__init__.py +23 -22
- local_deep_research/__main__.py +16 -0
- local_deep_research/advanced_search_system/__init__.py +7 -0
- local_deep_research/advanced_search_system/filters/__init__.py +8 -0
- local_deep_research/advanced_search_system/filters/base_filter.py +38 -0
- local_deep_research/advanced_search_system/filters/cross_engine_filter.py +200 -0
- local_deep_research/advanced_search_system/findings/base_findings.py +81 -0
- local_deep_research/advanced_search_system/findings/repository.py +452 -0
- local_deep_research/advanced_search_system/knowledge/__init__.py +1 -0
- local_deep_research/advanced_search_system/knowledge/base_knowledge.py +151 -0
- local_deep_research/advanced_search_system/knowledge/standard_knowledge.py +159 -0
- local_deep_research/advanced_search_system/questions/__init__.py +1 -0
- local_deep_research/advanced_search_system/questions/base_question.py +64 -0
- local_deep_research/advanced_search_system/questions/decomposition_question.py +445 -0
- local_deep_research/advanced_search_system/questions/standard_question.py +119 -0
- local_deep_research/advanced_search_system/repositories/__init__.py +7 -0
- local_deep_research/advanced_search_system/strategies/__init__.py +1 -0
- local_deep_research/advanced_search_system/strategies/base_strategy.py +118 -0
- local_deep_research/advanced_search_system/strategies/iterdrag_strategy.py +450 -0
- local_deep_research/advanced_search_system/strategies/parallel_search_strategy.py +312 -0
- local_deep_research/advanced_search_system/strategies/rapid_search_strategy.py +270 -0
- local_deep_research/advanced_search_system/strategies/standard_strategy.py +300 -0
- local_deep_research/advanced_search_system/tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/base_tool.py +100 -0
- local_deep_research/advanced_search_system/tools/knowledge_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/question_tools/__init__.py +1 -0
- local_deep_research/advanced_search_system/tools/search_tools/__init__.py +1 -0
- local_deep_research/api/__init__.py +5 -5
- local_deep_research/api/research_functions.py +96 -84
- local_deep_research/app.py +8 -0
- local_deep_research/citation_handler.py +25 -16
- local_deep_research/{config.py → config/config_files.py} +102 -110
- local_deep_research/config/llm_config.py +472 -0
- local_deep_research/config/search_config.py +77 -0
- local_deep_research/defaults/__init__.py +10 -5
- local_deep_research/defaults/main.toml +2 -2
- local_deep_research/defaults/search_engines.toml +60 -34
- local_deep_research/main.py +121 -19
- local_deep_research/migrate_db.py +147 -0
- local_deep_research/report_generator.py +72 -44
- local_deep_research/search_system.py +147 -283
- local_deep_research/setup_data_dir.py +35 -0
- local_deep_research/test_migration.py +178 -0
- local_deep_research/utilities/__init__.py +0 -0
- local_deep_research/utilities/db_utils.py +49 -0
- local_deep_research/{utilties → utilities}/enums.py +2 -2
- local_deep_research/{utilties → utilities}/llm_utils.py +63 -29
- local_deep_research/utilities/search_utilities.py +242 -0
- local_deep_research/{utilties → utilities}/setup_utils.py +4 -2
- local_deep_research/web/__init__.py +0 -1
- local_deep_research/web/app.py +86 -1709
- local_deep_research/web/app_factory.py +289 -0
- local_deep_research/web/database/README.md +70 -0
- local_deep_research/web/database/migrate_to_ldr_db.py +289 -0
- local_deep_research/web/database/migrations.py +447 -0
- local_deep_research/web/database/models.py +117 -0
- local_deep_research/web/database/schema_upgrade.py +107 -0
- local_deep_research/web/models/database.py +294 -0
- local_deep_research/web/models/settings.py +94 -0
- local_deep_research/web/routes/api_routes.py +559 -0
- local_deep_research/web/routes/history_routes.py +354 -0
- local_deep_research/web/routes/research_routes.py +715 -0
- local_deep_research/web/routes/settings_routes.py +1592 -0
- local_deep_research/web/services/research_service.py +947 -0
- local_deep_research/web/services/resource_service.py +149 -0
- local_deep_research/web/services/settings_manager.py +669 -0
- local_deep_research/web/services/settings_service.py +187 -0
- local_deep_research/web/services/socket_service.py +210 -0
- local_deep_research/web/static/css/custom_dropdown.css +277 -0
- local_deep_research/web/static/css/settings.css +1223 -0
- local_deep_research/web/static/css/styles.css +525 -48
- local_deep_research/web/static/js/components/custom_dropdown.js +428 -0
- local_deep_research/web/static/js/components/detail.js +348 -0
- local_deep_research/web/static/js/components/fallback/formatting.js +122 -0
- local_deep_research/web/static/js/components/fallback/ui.js +215 -0
- local_deep_research/web/static/js/components/history.js +487 -0
- local_deep_research/web/static/js/components/logpanel.js +949 -0
- local_deep_research/web/static/js/components/progress.js +1107 -0
- local_deep_research/web/static/js/components/research.js +1865 -0
- local_deep_research/web/static/js/components/results.js +766 -0
- local_deep_research/web/static/js/components/settings.js +3981 -0
- local_deep_research/web/static/js/components/settings_sync.js +106 -0
- local_deep_research/web/static/js/main.js +226 -0
- local_deep_research/web/static/js/services/api.js +253 -0
- local_deep_research/web/static/js/services/audio.js +31 -0
- local_deep_research/web/static/js/services/formatting.js +119 -0
- local_deep_research/web/static/js/services/pdf.js +622 -0
- local_deep_research/web/static/js/services/socket.js +882 -0
- local_deep_research/web/static/js/services/ui.js +546 -0
- local_deep_research/web/templates/base.html +72 -0
- local_deep_research/web/templates/components/custom_dropdown.html +47 -0
- local_deep_research/web/templates/components/log_panel.html +32 -0
- local_deep_research/web/templates/components/mobile_nav.html +22 -0
- local_deep_research/web/templates/components/settings_form.html +299 -0
- local_deep_research/web/templates/components/sidebar.html +21 -0
- local_deep_research/web/templates/pages/details.html +73 -0
- local_deep_research/web/templates/pages/history.html +51 -0
- local_deep_research/web/templates/pages/progress.html +57 -0
- local_deep_research/web/templates/pages/research.html +139 -0
- local_deep_research/web/templates/pages/results.html +59 -0
- local_deep_research/web/templates/settings_dashboard.html +78 -192
- local_deep_research/web/utils/__init__.py +0 -0
- local_deep_research/web/utils/formatters.py +76 -0
- local_deep_research/web_search_engines/engines/full_search.py +18 -16
- local_deep_research/web_search_engines/engines/meta_search_engine.py +182 -131
- local_deep_research/web_search_engines/engines/search_engine_arxiv.py +224 -139
- local_deep_research/web_search_engines/engines/search_engine_brave.py +88 -71
- local_deep_research/web_search_engines/engines/search_engine_ddg.py +48 -39
- local_deep_research/web_search_engines/engines/search_engine_github.py +415 -204
- local_deep_research/web_search_engines/engines/search_engine_google_pse.py +123 -90
- local_deep_research/web_search_engines/engines/search_engine_guardian.py +210 -157
- local_deep_research/web_search_engines/engines/search_engine_local.py +532 -369
- local_deep_research/web_search_engines/engines/search_engine_local_all.py +42 -36
- local_deep_research/web_search_engines/engines/search_engine_pubmed.py +358 -266
- local_deep_research/web_search_engines/engines/search_engine_searxng.py +211 -159
- local_deep_research/web_search_engines/engines/search_engine_semantic_scholar.py +213 -170
- local_deep_research/web_search_engines/engines/search_engine_serpapi.py +84 -68
- local_deep_research/web_search_engines/engines/search_engine_wayback.py +186 -154
- local_deep_research/web_search_engines/engines/search_engine_wikipedia.py +115 -77
- local_deep_research/web_search_engines/search_engine_base.py +174 -99
- local_deep_research/web_search_engines/search_engine_factory.py +192 -102
- local_deep_research/web_search_engines/search_engines_config.py +22 -15
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/METADATA +177 -97
- local_deep_research-0.2.0.dist-info/RECORD +135 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/WHEEL +1 -2
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/entry_points.txt +3 -0
- local_deep_research/defaults/llm_config.py +0 -338
- local_deep_research/utilties/search_utilities.py +0 -114
- local_deep_research/web/static/js/app.js +0 -3763
- local_deep_research/web/templates/api_keys_config.html +0 -82
- local_deep_research/web/templates/collections_config.html +0 -90
- local_deep_research/web/templates/index.html +0 -348
- local_deep_research/web/templates/llm_config.html +0 -120
- local_deep_research/web/templates/main_config.html +0 -89
- local_deep_research/web/templates/search_engines_config.html +0 -154
- local_deep_research/web/templates/settings.html +0 -519
- local_deep_research-0.1.26.dist-info/RECORD +0 -61
- local_deep_research-0.1.26.dist-info/top_level.txt +0 -1
- /local_deep_research/{utilties → config}/__init__.py +0 -0
- {local_deep_research-0.1.26.dist-info → local_deep_research-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -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 = '×';
|
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
|
+
})();
|