vectra-js 0.9.0
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.
- package/LICENSE +21 -0
- package/README.md +625 -0
- package/bin/vectra.js +76 -0
- package/documentation.md +288 -0
- package/index.js +11 -0
- package/package.json +53 -0
- package/src/backends/anthropic.js +37 -0
- package/src/backends/chroma_store.js +110 -0
- package/src/backends/gemini.js +68 -0
- package/src/backends/huggingface.js +52 -0
- package/src/backends/milvus_store.js +61 -0
- package/src/backends/ollama.js +63 -0
- package/src/backends/openai.js +46 -0
- package/src/backends/openrouter.js +51 -0
- package/src/backends/prisma_store.js +160 -0
- package/src/backends/qdrant_store.js +68 -0
- package/src/callbacks.js +31 -0
- package/src/config.js +123 -0
- package/src/core.js +591 -0
- package/src/evaluation/index.js +15 -0
- package/src/interfaces.js +21 -0
- package/src/memory.js +96 -0
- package/src/processor.js +155 -0
- package/src/reranker.js +26 -0
- package/src/ui/index.html +665 -0
- package/src/ui/script.js +785 -0
- package/src/ui/style.css +281 -0
- package/src/webconfig_server.js +175 -0
package/src/ui/script.js
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
2
|
+
// --- Navigation Logic (Smooth Scrolling & Scroll Spy) ---
|
|
3
|
+
const links = document.querySelectorAll('.nav-item');
|
|
4
|
+
const sections = document.querySelectorAll('section');
|
|
5
|
+
const mainScroll = document.getElementById('main-scroll');
|
|
6
|
+
let isManualScroll = false;
|
|
7
|
+
|
|
8
|
+
// Click to scroll
|
|
9
|
+
links.forEach(link => {
|
|
10
|
+
link.addEventListener('click', (e) => {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
const targetId = link.getAttribute('data-target');
|
|
13
|
+
const targetSection = document.getElementById(targetId);
|
|
14
|
+
|
|
15
|
+
if (targetSection) {
|
|
16
|
+
isManualScroll = true;
|
|
17
|
+
// Highlight immediately
|
|
18
|
+
updateSidebarState(targetId);
|
|
19
|
+
|
|
20
|
+
targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
21
|
+
|
|
22
|
+
// Reset manual scroll flag after animation
|
|
23
|
+
setTimeout(() => { isManualScroll = false; }, 1000);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Scroll Spy
|
|
29
|
+
mainScroll.addEventListener('scroll', () => {
|
|
30
|
+
// Sync Scroll to Preview
|
|
31
|
+
const previewScroll = document.getElementById('preview-scroll');
|
|
32
|
+
if (previewScroll) {
|
|
33
|
+
const maxMain = mainScroll.scrollHeight - mainScroll.clientHeight;
|
|
34
|
+
const maxPreview = previewScroll.scrollHeight - previewScroll.clientHeight;
|
|
35
|
+
if (maxMain > 0) {
|
|
36
|
+
const percentage = mainScroll.scrollTop / maxMain;
|
|
37
|
+
previewScroll.scrollTop = percentage * maxPreview;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isManualScroll) return;
|
|
42
|
+
|
|
43
|
+
let current = '';
|
|
44
|
+
sections.forEach(section => {
|
|
45
|
+
const sectionTop = section.offsetTop;
|
|
46
|
+
// Adjust offset for better trigger point (e.g. 1/3 down the viewport)
|
|
47
|
+
if (mainScroll.scrollTop >= (sectionTop - 20)) {
|
|
48
|
+
current = section.getAttribute('id');
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// If we are at the top, force the first one
|
|
53
|
+
if (mainScroll.scrollTop < 50) {
|
|
54
|
+
current = sections[0].getAttribute('id');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (current) updateSidebarState(current);
|
|
58
|
+
updatePreviewHighlight(current);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function updateSidebarState(currentId) {
|
|
62
|
+
links.forEach(link => {
|
|
63
|
+
link.classList.remove('bg-indigo-50', 'text-indigo-600', 'active', 'border-l-4', 'border-indigo-600');
|
|
64
|
+
link.classList.add('text-gray-600', 'hover:bg-gray-50');
|
|
65
|
+
|
|
66
|
+
// We use a slight visual indicator for active state
|
|
67
|
+
if (link.getAttribute('data-target') === currentId) {
|
|
68
|
+
link.classList.remove('text-gray-600', 'hover:bg-gray-50');
|
|
69
|
+
link.classList.add('bg-indigo-50', 'text-indigo-600', 'active');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Accordion Logic ---
|
|
75
|
+
document.querySelectorAll('.accordion-header').forEach(header => {
|
|
76
|
+
header.addEventListener('click', () => {
|
|
77
|
+
const targetId = header.getAttribute('data-target');
|
|
78
|
+
const targetContent = document.getElementById(targetId);
|
|
79
|
+
const icon = header.querySelector('.accordion-icon');
|
|
80
|
+
|
|
81
|
+
const isHidden = targetContent.classList.contains('hidden');
|
|
82
|
+
|
|
83
|
+
// Auto-collapse others (if enabled)
|
|
84
|
+
const autoFocus = document.getElementById('auto-focus-toggle')?.checked;
|
|
85
|
+
if (autoFocus) {
|
|
86
|
+
document.querySelectorAll('.accordion-content').forEach(content => {
|
|
87
|
+
// If it's not the one we clicked, hide it
|
|
88
|
+
if (content.id !== targetId) {
|
|
89
|
+
content.classList.add('hidden');
|
|
90
|
+
// Reset icon rotation for others
|
|
91
|
+
const otherHeader = document.querySelector(`[data-target="${content.id}"]`);
|
|
92
|
+
if (otherHeader) {
|
|
93
|
+
otherHeader.querySelector('.accordion-icon').classList.remove('rotate-180');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Toggle clicked
|
|
100
|
+
if (isHidden) {
|
|
101
|
+
targetContent.classList.remove('hidden');
|
|
102
|
+
icon.classList.add('rotate-180');
|
|
103
|
+
} else {
|
|
104
|
+
targetContent.classList.add('hidden');
|
|
105
|
+
icon.classList.remove('rotate-180');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// --- Temperature Slider Sync ---
|
|
111
|
+
const tempSlider = document.getElementById('temp-slider');
|
|
112
|
+
const tempInput = document.getElementById('temp-input');
|
|
113
|
+
|
|
114
|
+
if (tempSlider && tempInput) {
|
|
115
|
+
tempSlider.addEventListener('input', (e) => {
|
|
116
|
+
tempInput.value = e.target.value;
|
|
117
|
+
triggerChange(); // To update preview
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
tempInput.addEventListener('input', (e) => {
|
|
121
|
+
const val = parseFloat(e.target.value);
|
|
122
|
+
if (val >= 0 && val <= 1) {
|
|
123
|
+
tempSlider.value = val;
|
|
124
|
+
}
|
|
125
|
+
triggerChange();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --- Key-Value Headers Builder ---
|
|
130
|
+
const addHeaderBtn = document.getElementById('add-header-btn');
|
|
131
|
+
const headersBuilder = document.getElementById('headers-builder');
|
|
132
|
+
|
|
133
|
+
if (addHeaderBtn) {
|
|
134
|
+
addHeaderBtn.addEventListener('click', () => addHeaderRow());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function addHeaderRow(key = '', value = '') {
|
|
138
|
+
const row = document.createElement('div');
|
|
139
|
+
row.className = 'flex items-center space-x-2 header-row';
|
|
140
|
+
row.innerHTML = `
|
|
141
|
+
<input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
|
|
142
|
+
<input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
|
|
143
|
+
<button type="button" class="remove-header p-2 text-gray-400 hover:text-red-500">
|
|
144
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
|
145
|
+
</button>
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
row.querySelector('.remove-header').addEventListener('click', () => {
|
|
149
|
+
row.remove();
|
|
150
|
+
updateHiddenHeaders();
|
|
151
|
+
triggerChange();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
row.querySelectorAll('input').forEach(input => {
|
|
155
|
+
input.addEventListener('input', () => {
|
|
156
|
+
updateHiddenHeaders();
|
|
157
|
+
triggerChange();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
headersBuilder.appendChild(row);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function updateHiddenHeaders() {
|
|
165
|
+
const rows = headersBuilder.querySelectorAll('.header-row');
|
|
166
|
+
const headers = {};
|
|
167
|
+
let hasHeaders = false;
|
|
168
|
+
|
|
169
|
+
rows.forEach(row => {
|
|
170
|
+
const key = row.querySelector('.header-key').value.trim();
|
|
171
|
+
const value = row.querySelector('.header-value').value.trim();
|
|
172
|
+
if (key) {
|
|
173
|
+
headers[key] = value;
|
|
174
|
+
hasHeaders = true;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const input = document.querySelector('[name="llm.defaultHeaders"]');
|
|
179
|
+
if (input) {
|
|
180
|
+
input.value = hasHeaders ? JSON.stringify(headers) : '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function triggerChange() {
|
|
185
|
+
document.getElementById('config-form').dispatchEvent(new Event('change'));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// --- Load Config ---
|
|
189
|
+
fetchConfig();
|
|
190
|
+
|
|
191
|
+
// --- Save Config ---
|
|
192
|
+
document.getElementById('save-btn').addEventListener('click', saveConfig);
|
|
193
|
+
|
|
194
|
+
// --- Backend Toggle Logic ---
|
|
195
|
+
const btnNode = document.getElementById('backend-node');
|
|
196
|
+
const btnPython = document.getElementById('backend-python');
|
|
197
|
+
|
|
198
|
+
if (btnNode && btnPython) {
|
|
199
|
+
btnNode.addEventListener('click', () => setBackend('node'));
|
|
200
|
+
btnPython.addEventListener('click', () => setBackend('python'));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- OpenRouter Logic ---
|
|
204
|
+
const providerSelect = document.querySelector('[name="llm.provider"]');
|
|
205
|
+
const openRouterExtras = document.getElementById('openrouter-extras');
|
|
206
|
+
const openRouterHint = document.getElementById('openrouter-base-hint');
|
|
207
|
+
|
|
208
|
+
if (providerSelect) {
|
|
209
|
+
providerSelect.addEventListener('change', () => {
|
|
210
|
+
toggleOpenRouter(providerSelect.value);
|
|
211
|
+
updatePreview();
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const orInputs = ['or-referer', 'or-title'];
|
|
216
|
+
orInputs.forEach(id => {
|
|
217
|
+
const el = document.getElementById(id);
|
|
218
|
+
if (el) {
|
|
219
|
+
el.addEventListener('input', () => {
|
|
220
|
+
updateOpenRouterHeaders();
|
|
221
|
+
updatePreview();
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
function toggleOpenRouter(provider) {
|
|
227
|
+
if (!openRouterExtras) return;
|
|
228
|
+
if (provider === 'openrouter') {
|
|
229
|
+
openRouterExtras.classList.remove('hidden');
|
|
230
|
+
if (openRouterHint) openRouterHint.classList.remove('hidden');
|
|
231
|
+
} else {
|
|
232
|
+
openRouterExtras.classList.add('hidden');
|
|
233
|
+
if (openRouterHint) openRouterHint.classList.add('hidden');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function updateOpenRouterHeaders() {
|
|
238
|
+
const referer = document.getElementById('or-referer').value.trim();
|
|
239
|
+
const title = document.getElementById('or-title').value.trim();
|
|
240
|
+
|
|
241
|
+
// We sync these to the existing headers mechanism
|
|
242
|
+
// First, get existing custom headers
|
|
243
|
+
const headersBuilder = document.getElementById('headers-builder');
|
|
244
|
+
const rows = headersBuilder.querySelectorAll('.header-row');
|
|
245
|
+
const headers = {};
|
|
246
|
+
|
|
247
|
+
rows.forEach(row => {
|
|
248
|
+
const key = row.querySelector('.header-key').value.trim();
|
|
249
|
+
const value = row.querySelector('.header-value').value.trim();
|
|
250
|
+
if (key) headers[key] = value;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// Add/Update OpenRouter specific headers
|
|
254
|
+
if (referer) headers['HTTP-Referer'] = referer;
|
|
255
|
+
if (title) headers['X-Title'] = title;
|
|
256
|
+
|
|
257
|
+
// Update the hidden input
|
|
258
|
+
const input = document.querySelector('[name="llm.defaultHeaders"]');
|
|
259
|
+
if (input) {
|
|
260
|
+
input.value = Object.keys(headers).length > 0 ? JSON.stringify(headers) : '';
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// --- Live Preview Listeners ---
|
|
265
|
+
document.getElementById('config-form').addEventListener('input', updatePreview);
|
|
266
|
+
document.getElementById('config-form').addEventListener('change', updatePreview);
|
|
267
|
+
|
|
268
|
+
// --- Conditional Section Visibility ---
|
|
269
|
+
const chunkingStrategyEl = document.querySelector('[name="chunking.strategy"]');
|
|
270
|
+
const agenticLlmSection = document.getElementById('agentic-llm-content');
|
|
271
|
+
if (chunkingStrategyEl && agenticLlmSection) {
|
|
272
|
+
const toggleAgentic = () => {
|
|
273
|
+
const isAgentic = chunkingStrategyEl.value === 'agentic';
|
|
274
|
+
agenticLlmSection.classList.toggle('hidden', !isAgentic);
|
|
275
|
+
};
|
|
276
|
+
chunkingStrategyEl.addEventListener('change', () => { toggleAgentic(); updatePreview(); });
|
|
277
|
+
toggleAgentic();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const retrievalStrategyEl = document.querySelector('[name="retrieval.strategy"]');
|
|
281
|
+
const retrievalLlmSection = document.getElementById('retrieval-llm-content');
|
|
282
|
+
if (retrievalStrategyEl && retrievalLlmSection) {
|
|
283
|
+
const toggleRetrievalLLM = () => {
|
|
284
|
+
const needsLLM = ['hyde','multi_query'].includes(retrievalStrategyEl.value);
|
|
285
|
+
retrievalLlmSection.classList.toggle('hidden', !needsLLM);
|
|
286
|
+
};
|
|
287
|
+
retrievalStrategyEl.addEventListener('change', () => { toggleRetrievalLLM(); updatePreview(); });
|
|
288
|
+
toggleRetrievalLLM();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const rerankingEnabledEl = document.querySelector('[name="reranking.enabled"]');
|
|
292
|
+
const rerankingLlmSection = document.getElementById('reranking-llm-content');
|
|
293
|
+
if (rerankingEnabledEl && rerankingLlmSection) {
|
|
294
|
+
const toggleRerankLLM = () => {
|
|
295
|
+
rerankingLlmSection.classList.toggle('hidden', !rerankingEnabledEl.checked);
|
|
296
|
+
};
|
|
297
|
+
rerankingEnabledEl.addEventListener('change', () => { toggleRerankLLM(); updatePreview(); });
|
|
298
|
+
toggleRerankLLM();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Global state for current config type (Python vs JS)
|
|
303
|
+
let isPythonBackend = false;
|
|
304
|
+
|
|
305
|
+
function setBackend(type) {
|
|
306
|
+
isPythonBackend = (type === 'python');
|
|
307
|
+
|
|
308
|
+
const btnNode = document.getElementById('backend-node');
|
|
309
|
+
const btnPython = document.getElementById('backend-python');
|
|
310
|
+
|
|
311
|
+
if (isPythonBackend) {
|
|
312
|
+
btnPython.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-indigo-600 bg-indigo-50 shadow-sm';
|
|
313
|
+
btnNode.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-gray-500 hover:text-gray-900';
|
|
314
|
+
} else {
|
|
315
|
+
btnNode.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-indigo-600 bg-indigo-50 shadow-sm';
|
|
316
|
+
btnPython.className = 'px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200 text-gray-500 hover:text-gray-900';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
updatePreview();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function fetchConfig() {
|
|
323
|
+
try {
|
|
324
|
+
const response = await fetch('/config');
|
|
325
|
+
if (!response.ok) throw new Error('Failed to load config');
|
|
326
|
+
const config = await response.json();
|
|
327
|
+
|
|
328
|
+
// Detect backend type from loaded config
|
|
329
|
+
const detectedPython = detectBackendFromConfig(config);
|
|
330
|
+
setBackend(detectedPython ? 'python' : 'node');
|
|
331
|
+
|
|
332
|
+
populateForm(config);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
showStatus(error.message, 'error');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function detectBackendFromConfig(config) {
|
|
339
|
+
if (config.embedding && config.embedding.api_key !== undefined) return true;
|
|
340
|
+
if (config.llm && config.llm.api_key !== undefined) return true;
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function populateForm(config) {
|
|
345
|
+
// Embedding
|
|
346
|
+
setVal('embedding.provider', config.embedding?.provider);
|
|
347
|
+
setVal('embedding.apiKey', config.embedding?.apiKey || config.embedding?.api_key);
|
|
348
|
+
setVal('embedding.modelName', config.embedding?.modelName || config.embedding?.model_name);
|
|
349
|
+
setVal('embedding.dimensions', config.embedding?.dimensions);
|
|
350
|
+
|
|
351
|
+
// LLM
|
|
352
|
+
setVal('llm.provider', config.llm?.provider);
|
|
353
|
+
setVal('llm.apiKey', config.llm?.apiKey || config.llm?.api_key);
|
|
354
|
+
setVal('llm.modelName', config.llm?.modelName || config.llm?.model_name);
|
|
355
|
+
setVal('llm.temperature', config.llm?.temperature);
|
|
356
|
+
|
|
357
|
+
// Update Slider
|
|
358
|
+
const tempSlider = document.getElementById('temp-slider');
|
|
359
|
+
if (tempSlider && config.llm?.temperature !== undefined) {
|
|
360
|
+
tempSlider.value = config.llm.temperature;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
setVal('llm.maxTokens', config.llm?.maxTokens || config.llm?.max_tokens);
|
|
364
|
+
setVal('llm.baseUrl', config.llm?.baseUrl || config.llm?.base_url);
|
|
365
|
+
|
|
366
|
+
// Headers Builder
|
|
367
|
+
const headers = config.llm?.defaultHeaders || config.llm?.default_headers;
|
|
368
|
+
const headersBuilder = document.getElementById('headers-builder');
|
|
369
|
+
headersBuilder.innerHTML = ''; // Clear
|
|
370
|
+
|
|
371
|
+
let orReferer = '';
|
|
372
|
+
let orTitle = '';
|
|
373
|
+
|
|
374
|
+
if (headers && typeof headers === 'object') {
|
|
375
|
+
Object.entries(headers).forEach(([k, v]) => {
|
|
376
|
+
if (k === 'HTTP-Referer') orReferer = v;
|
|
377
|
+
else if (k === 'X-Title') orTitle = v;
|
|
378
|
+
else addHeaderRow(k, v);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// OpenRouter Specifics
|
|
383
|
+
const orRefererInput = document.getElementById('or-referer');
|
|
384
|
+
const orTitleInput = document.getElementById('or-title');
|
|
385
|
+
if (orRefererInput) orRefererInput.value = orReferer;
|
|
386
|
+
if (orTitleInput) orTitleInput.value = orTitle;
|
|
387
|
+
|
|
388
|
+
toggleOpenRouter(config.llm?.provider);
|
|
389
|
+
|
|
390
|
+
// Update hidden input
|
|
391
|
+
const headerInput = document.querySelector('[name="llm.defaultHeaders"]');
|
|
392
|
+
if (headerInput) headerInput.value = headers ? JSON.stringify(headers) : '';
|
|
393
|
+
|
|
394
|
+
// Database
|
|
395
|
+
setVal('database.type', config.database?.type);
|
|
396
|
+
setVal('database.tableName', config.database?.tableName || config.database?.table_name);
|
|
397
|
+
const colMap = config.database?.columnMap || config.database?.column_map;
|
|
398
|
+
setVal('database.columnMap', colMap ? JSON.stringify(colMap, null, 2) : '');
|
|
399
|
+
|
|
400
|
+
// Chunking
|
|
401
|
+
setVal('chunking.strategy', config.chunking?.strategy);
|
|
402
|
+
setVal('chunking.chunkSize', config.chunking?.chunkSize || config.chunking?.chunk_size);
|
|
403
|
+
setVal('chunking.chunkOverlap', config.chunking?.chunkOverlap || config.chunking?.chunk_overlap);
|
|
404
|
+
setVal('chunking.separators', config.chunking?.separators ? JSON.stringify(config.chunking.separators) : '');
|
|
405
|
+
|
|
406
|
+
// Retrieval
|
|
407
|
+
setVal('retrieval.strategy', config.retrieval?.strategy);
|
|
408
|
+
setVal('retrieval.hybridAlpha', config.retrieval?.hybridAlpha || config.retrieval?.hybrid_alpha);
|
|
409
|
+
|
|
410
|
+
// Reranking
|
|
411
|
+
const rerankEnabled = config.reranking?.enabled;
|
|
412
|
+
const cb = document.querySelector('[name="reranking.enabled"]');
|
|
413
|
+
if (cb) cb.checked = !!rerankEnabled;
|
|
414
|
+
|
|
415
|
+
setVal('reranking.topN', config.reranking?.topN || config.reranking?.top_n);
|
|
416
|
+
setVal('reranking.windowSize', config.reranking?.windowSize || config.reranking?.window_size);
|
|
417
|
+
|
|
418
|
+
// Metadata
|
|
419
|
+
const metadataFilters = config.metadata?.filters;
|
|
420
|
+
setVal('metadata.filters', metadataFilters ? JSON.stringify(metadataFilters, null, 2) : '');
|
|
421
|
+
|
|
422
|
+
// Query Planning
|
|
423
|
+
setVal('queryPlanning.strategy', config.queryPlanning?.strategy || config.query_planning?.strategy);
|
|
424
|
+
const initialPrompts = config.queryPlanning?.initialPrompts || config.query_planning?.initial_prompts;
|
|
425
|
+
setVal('queryPlanning.initialPrompts', initialPrompts ? JSON.stringify(initialPrompts, null, 2) : '');
|
|
426
|
+
|
|
427
|
+
// Grounding
|
|
428
|
+
const groundingEnabled = config.grounding?.enabled;
|
|
429
|
+
const cbGrounding = document.querySelector('[name="grounding.enabled"]');
|
|
430
|
+
if (cbGrounding) cbGrounding.checked = !!groundingEnabled;
|
|
431
|
+
setVal('grounding.threshold', config.grounding?.threshold);
|
|
432
|
+
|
|
433
|
+
// Generation
|
|
434
|
+
setVal('generation.style', config.generation?.style);
|
|
435
|
+
setVal('generation.maxLength', config.generation?.maxLength || config.generation?.max_length);
|
|
436
|
+
|
|
437
|
+
// Prompts
|
|
438
|
+
setVal('prompts.system', config.prompts?.system);
|
|
439
|
+
setVal('prompts.user', config.prompts?.user);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Fix: Move addHeaderRow to global scope or re-structure
|
|
443
|
+
function addHeaderRow(key = '', value = '') {
|
|
444
|
+
const headersBuilder = document.getElementById('headers-builder');
|
|
445
|
+
if (!headersBuilder) return;
|
|
446
|
+
|
|
447
|
+
const row = document.createElement('div');
|
|
448
|
+
row.className = 'flex items-center space-x-2 header-row mb-2';
|
|
449
|
+
row.innerHTML = `
|
|
450
|
+
<input type="text" placeholder="Key" value="${key}" class="header-key block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
|
|
451
|
+
<input type="text" placeholder="Value" value="${value}" class="header-value block w-1/2 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm py-2 px-3 border">
|
|
452
|
+
<button type="button" class="remove-header p-2 text-gray-400 hover:text-red-500 transition-colors">
|
|
453
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
|
454
|
+
</button>
|
|
455
|
+
`;
|
|
456
|
+
|
|
457
|
+
row.querySelector('.remove-header').addEventListener('click', () => {
|
|
458
|
+
row.remove();
|
|
459
|
+
updateHiddenHeaders();
|
|
460
|
+
document.getElementById('config-form').dispatchEvent(new Event('change'));
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
row.querySelectorAll('input').forEach(input => {
|
|
464
|
+
input.addEventListener('input', () => {
|
|
465
|
+
updateHiddenHeaders();
|
|
466
|
+
document.getElementById('config-form').dispatchEvent(new Event('change'));
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
headersBuilder.appendChild(row);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function updateHiddenHeaders() {
|
|
474
|
+
const headersBuilder = document.getElementById('headers-builder');
|
|
475
|
+
const rows = headersBuilder.querySelectorAll('.header-row');
|
|
476
|
+
const headers = {};
|
|
477
|
+
let hasHeaders = false;
|
|
478
|
+
|
|
479
|
+
rows.forEach(row => {
|
|
480
|
+
const key = row.querySelector('.header-key').value.trim();
|
|
481
|
+
const value = row.querySelector('.header-value').value.trim();
|
|
482
|
+
if (key) {
|
|
483
|
+
headers[key] = value;
|
|
484
|
+
hasHeaders = true;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const input = document.querySelector('[name="llm.defaultHeaders"]');
|
|
489
|
+
if (input) {
|
|
490
|
+
input.value = hasHeaders ? JSON.stringify(headers) : '';
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function setVal(name, value) {
|
|
495
|
+
const el = document.querySelector(`[name="${name}"]`);
|
|
496
|
+
if (el) {
|
|
497
|
+
el.value = (value === undefined || value === null) ? '' : value;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function buildPayload() {
|
|
502
|
+
const formData = new FormData(document.getElementById('config-form'));
|
|
503
|
+
const get = (n) => formData.get(n);
|
|
504
|
+
const getNum = (n) => { const v = get(n); return v ? Number(v) : undefined; };
|
|
505
|
+
const getJson = (n) => {
|
|
506
|
+
const v = get(n);
|
|
507
|
+
try { return v ? JSON.parse(v) : undefined; }
|
|
508
|
+
catch { return undefined; }
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const isPython = isPythonBackend;
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
embedding: {
|
|
515
|
+
provider: get('embedding.provider'),
|
|
516
|
+
[isPython ? 'api_key' : 'apiKey']: get('embedding.apiKey'),
|
|
517
|
+
[isPython ? 'model_name' : 'modelName']: get('embedding.modelName'),
|
|
518
|
+
dimensions: getNum('embedding.dimensions')
|
|
519
|
+
},
|
|
520
|
+
llm: {
|
|
521
|
+
provider: get('llm.provider'),
|
|
522
|
+
[isPython ? 'api_key' : 'apiKey']: get('llm.apiKey'),
|
|
523
|
+
[isPython ? 'model_name' : 'modelName']: get('llm.modelName'),
|
|
524
|
+
temperature: getNum('llm.temperature'),
|
|
525
|
+
[isPython ? 'max_tokens' : 'maxTokens']: getNum('llm.maxTokens'),
|
|
526
|
+
[isPython ? 'base_url' : 'baseUrl']: get('llm.baseUrl') || undefined,
|
|
527
|
+
[isPython ? 'default_headers' : 'defaultHeaders']: getJson('llm.defaultHeaders')
|
|
528
|
+
},
|
|
529
|
+
database: {
|
|
530
|
+
type: get('database.type'),
|
|
531
|
+
[isPython ? 'table_name' : 'tableName']: get('database.tableName'),
|
|
532
|
+
[isPython ? 'column_map' : 'columnMap']: getJson('database.columnMap')
|
|
533
|
+
},
|
|
534
|
+
chunking: {
|
|
535
|
+
strategy: get('chunking.strategy'),
|
|
536
|
+
[isPython ? 'chunk_size' : 'chunkSize']: getNum('chunking.chunkSize'),
|
|
537
|
+
[isPython ? 'chunk_overlap' : 'chunkOverlap']: getNum('chunking.chunkOverlap'),
|
|
538
|
+
separators: getJson('chunking.separators'),
|
|
539
|
+
[isPython ? 'agentic_llm' : 'agenticLlm']: (get('chunking.strategy') === 'agentic') ? {
|
|
540
|
+
provider: get('chunking.agentic.provider'),
|
|
541
|
+
[isPython ? 'api_key' : 'apiKey']: get('chunking.agentic.apiKey'),
|
|
542
|
+
[isPython ? 'model_name' : 'modelName']: get('chunking.agentic.modelName'),
|
|
543
|
+
temperature: getNum('chunking.agentic.temperature'),
|
|
544
|
+
[isPython ? 'max_tokens' : 'maxTokens']: getNum('chunking.agentic.maxTokens'),
|
|
545
|
+
[isPython ? 'base_url' : 'baseUrl']: get('chunking.agentic.baseUrl') || undefined
|
|
546
|
+
} : undefined
|
|
547
|
+
},
|
|
548
|
+
retrieval: {
|
|
549
|
+
strategy: get('retrieval.strategy'),
|
|
550
|
+
[isPython ? 'hybrid_alpha' : 'hybridAlpha']: getNum('retrieval.hybridAlpha'),
|
|
551
|
+
[isPython ? 'mmr_lambda' : 'mmrLambda']: getNum('retrieval.mmrLambda'),
|
|
552
|
+
[isPython ? 'mmr_fetch_k' : 'mmrFetchK']: getNum('retrieval.mmrFetchK'),
|
|
553
|
+
[isPython ? 'llm_config' : 'llmConfig']: (['hyde','multi_query'].includes(get('retrieval.strategy'))) ? {
|
|
554
|
+
provider: get('retrieval.llm.provider'),
|
|
555
|
+
[isPython ? 'api_key' : 'apiKey']: get('retrieval.llm.apiKey'),
|
|
556
|
+
[isPython ? 'model_name' : 'modelName']: get('retrieval.llm.modelName'),
|
|
557
|
+
temperature: getNum('retrieval.llm.temperature'),
|
|
558
|
+
[isPython ? 'max_tokens' : 'maxTokens']: getNum('retrieval.llm.maxTokens'),
|
|
559
|
+
[isPython ? 'base_url' : 'baseUrl']: get('retrieval.llm.baseUrl') || undefined
|
|
560
|
+
} : undefined
|
|
561
|
+
},
|
|
562
|
+
reranking: {
|
|
563
|
+
enabled: document.querySelector('[name="reranking.enabled"]').checked,
|
|
564
|
+
provider: 'llm',
|
|
565
|
+
[isPython ? 'top_n' : 'topN']: getNum('reranking.topN'),
|
|
566
|
+
[isPython ? 'window_size' : 'windowSize']: getNum('reranking.windowSize'),
|
|
567
|
+
[isPython ? 'llm_config' : 'llmConfig']: document.querySelector('[name="reranking.enabled"]').checked ? {
|
|
568
|
+
provider: get('reranking.llm.provider'),
|
|
569
|
+
[isPython ? 'api_key' : 'apiKey']: get('reranking.llm.apiKey'),
|
|
570
|
+
[isPython ? 'model_name' : 'modelName']: get('reranking.llm.modelName'),
|
|
571
|
+
temperature: getNum('reranking.llm.temperature'),
|
|
572
|
+
[isPython ? 'max_tokens' : 'maxTokens']: getNum('reranking.llm.maxTokens'),
|
|
573
|
+
[isPython ? 'base_url' : 'baseUrl']: get('reranking.llm.baseUrl') || undefined
|
|
574
|
+
} : undefined
|
|
575
|
+
},
|
|
576
|
+
metadata: {
|
|
577
|
+
filters: getJson('metadata.filters')
|
|
578
|
+
},
|
|
579
|
+
[isPython ? 'query_planning' : 'queryPlanning']: {
|
|
580
|
+
strategy: get('queryPlanning.strategy'),
|
|
581
|
+
[isPython ? 'initial_prompts' : 'initialPrompts']: getJson('queryPlanning.initialPrompts'),
|
|
582
|
+
[isPython ? 'token_budget' : 'tokenBudget']: getNum('queryPlanning.tokenBudget'),
|
|
583
|
+
[isPython ? 'prefer_summaries_below' : 'preferSummariesBelow']: getNum('queryPlanning.preferSummariesBelow'),
|
|
584
|
+
[isPython ? 'include_citations' : 'includeCitations']: !!document.querySelector('[name="queryPlanning.includeCitations"]').checked
|
|
585
|
+
},
|
|
586
|
+
grounding: {
|
|
587
|
+
enabled: document.querySelector('[name="grounding.enabled"]').checked,
|
|
588
|
+
threshold: getNum('grounding.threshold'),
|
|
589
|
+
strict: !!document.querySelector('[name="grounding.strict"]').checked,
|
|
590
|
+
[isPython ? 'max_snippets' : 'maxSnippets']: getNum('grounding.maxSnippets')
|
|
591
|
+
},
|
|
592
|
+
generation: {
|
|
593
|
+
style: get('generation.style'),
|
|
594
|
+
[isPython ? 'max_length' : 'maxLength']: getNum('generation.maxLength'),
|
|
595
|
+
[isPython ? 'structured_output' : 'structuredOutput']: get('generation.structuredOutput'),
|
|
596
|
+
[isPython ? 'output_format' : 'outputFormat']: get('generation.outputFormat')
|
|
597
|
+
},
|
|
598
|
+
prompts: {
|
|
599
|
+
system: get('prompts.system'),
|
|
600
|
+
user: get('prompts.user')
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function updatePreview() {
|
|
606
|
+
// We want to highlight the section that is currently active (scrolled to)
|
|
607
|
+
// We construct HTML manually for JS object structure with syntax highlighting.
|
|
608
|
+
|
|
609
|
+
const payload = buildPayload();
|
|
610
|
+
const previewEl = document.getElementById('json-preview');
|
|
611
|
+
if (!previewEl) return;
|
|
612
|
+
|
|
613
|
+
// Get active section from sidebar
|
|
614
|
+
const activeLink = document.querySelector('.nav-item.active');
|
|
615
|
+
const activeSection = activeLink ? activeLink.getAttribute('data-target') : null;
|
|
616
|
+
|
|
617
|
+
// Helper to format value with colors
|
|
618
|
+
const formatValue = (val, indentLevel) => {
|
|
619
|
+
if (val === null) return '<span class="text-[#ff7b72]">null</span>';
|
|
620
|
+
if (val === undefined) return '<span class="text-[#79c0ff]">undefined</span>';
|
|
621
|
+
if (typeof val === 'boolean') return `<span class="text-[#79c0ff]">${val}</span>`;
|
|
622
|
+
if (typeof val === 'number') return `<span class="text-[#79c0ff]">${val}</span>`;
|
|
623
|
+
if (typeof val === 'string') return `<span class="text-[#a5d6ff]">'${val}'</span>`;
|
|
624
|
+
if (Array.isArray(val)) {
|
|
625
|
+
if (val.length === 0) return '[]';
|
|
626
|
+
const indent = ' '.repeat(indentLevel);
|
|
627
|
+
const nextIndent = ' '.repeat(indentLevel + 1);
|
|
628
|
+
const items = val.map(v => `${nextIndent}${formatValue(v, indentLevel + 1)}`).join(',\n');
|
|
629
|
+
return `[\n${items}\n${indent}]`;
|
|
630
|
+
}
|
|
631
|
+
if (typeof val === 'object') {
|
|
632
|
+
if (Object.keys(val).length === 0) return '{}';
|
|
633
|
+
const indent = ' '.repeat(indentLevel);
|
|
634
|
+
const nextIndent = ' '.repeat(indentLevel + 1);
|
|
635
|
+
const props = Object.entries(val).map(([k, v]) => {
|
|
636
|
+
// Key color (light blue usually in VS Code for properties, or similar)
|
|
637
|
+
// Using a distinct color for keys: text-[#d2a8ff] (purple-ish) or text-[#7ee787] (green-ish)
|
|
638
|
+
// VS Code Default Dark+ Properties are light blue.
|
|
639
|
+
const keyStr = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : `'${k}'`;
|
|
640
|
+
return `\n${nextIndent}<span class="text-[#7ee787]">${keyStr}</span>: ${formatValue(v, indentLevel + 1)}`;
|
|
641
|
+
}).join(',');
|
|
642
|
+
return `{${props}\n${indent}}`;
|
|
643
|
+
}
|
|
644
|
+
return String(val);
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// Helper to wrap a section
|
|
648
|
+
const jsPart = (key, data, sectionId) => {
|
|
649
|
+
// Format the object body (level 1 indent)
|
|
650
|
+
const valStr = formatValue(data, 1);
|
|
651
|
+
const isActive = (sectionId === activeSection);
|
|
652
|
+
// Active: slight background, full opacity. Inactive: dimmed.
|
|
653
|
+
// We use 'group' to handle hover effects if needed, but here just static state.
|
|
654
|
+
const className = isActive
|
|
655
|
+
? 'json-section json-active bg-[#2d2d2d] rounded border-l-2 border-[#58a6ff]'
|
|
656
|
+
: 'json-section json-dimmed border-l-2 border-transparent';
|
|
657
|
+
|
|
658
|
+
return `<pre class="${className} p-2 pl-3 whitespace-pre leading-5"><span class="text-[#7ee787]">${key}</span>: ${valStr}</pre>`;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const isToggleOn = (name) => {
|
|
662
|
+
const el = document.querySelector(`[name="toggle.${name}"]`);
|
|
663
|
+
return el ? el.checked : false;
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
let html = '';
|
|
667
|
+
if (!isPythonBackend) {
|
|
668
|
+
html += '<span class="text-[#ff7b72]">const</span> <span class="text-[#d2a8ff]">config</span> = {\n';
|
|
669
|
+
} else {
|
|
670
|
+
html += '<span class="text-[#d2a8ff]">config</span> <span class="text-[#79c0ff]">=</span> {\n';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const parts = [];
|
|
674
|
+
parts.push(jsPart('embedding', payload.embedding, 'embedding'));
|
|
675
|
+
parts.push(jsPart('llm', payload.llm, 'llm'));
|
|
676
|
+
parts.push(jsPart('database', payload.database, 'database'));
|
|
677
|
+
parts.push(jsPart('chunking', payload.chunking, 'chunking'));
|
|
678
|
+
parts.push(jsPart('reranking', payload.reranking, 'reranking'));
|
|
679
|
+
if (isToggleOn('retrieval')) parts.push(jsPart('retrieval', payload.retrieval, 'retrieval'));
|
|
680
|
+
if (isToggleOn('metadata')) parts.push(jsPart('metadata', payload.metadata, 'metadata'));
|
|
681
|
+
const qpKey = isPythonBackend ? 'query_planning' : 'queryPlanning';
|
|
682
|
+
if (isToggleOn('queryPlanning')) parts.push(jsPart(qpKey, payload[qpKey], 'query-planning'));
|
|
683
|
+
if (isToggleOn('grounding')) parts.push(jsPart('grounding', payload.grounding, 'grounding'));
|
|
684
|
+
if (isToggleOn('generation')) parts.push(jsPart('generation', payload.generation, 'generation'));
|
|
685
|
+
if (isToggleOn('prompts')) parts.push(jsPart('prompts', payload.prompts, 'prompts'));
|
|
686
|
+
|
|
687
|
+
html += parts.join(',\n');
|
|
688
|
+
html += '\n}';
|
|
689
|
+
if (!isPythonBackend) {
|
|
690
|
+
html += '\n\n<span class="text-[#ff7b72]">module</span>.<span class="text-[#d2a8ff]">exports</span> = <span class="text-[#d2a8ff]">config</span>';
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
previewEl.innerHTML = html;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Wrapper for updatePreviewHighlight to be called from scroll spy
|
|
697
|
+
function updatePreviewHighlight(activeSectionId) {
|
|
698
|
+
// Re-render the preview to apply classes
|
|
699
|
+
// Note: This might be expensive on every scroll event if the payload is huge,
|
|
700
|
+
// but for this config size it's fine.
|
|
701
|
+
// Optimization: Just toggle classes on existing DOM elements if possible.
|
|
702
|
+
// But since we regenerate HTML in updatePreview, let's just call that.
|
|
703
|
+
updatePreview();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
async function saveConfig(e) {
|
|
707
|
+
e.preventDefault();
|
|
708
|
+
const btn = document.getElementById('save-btn');
|
|
709
|
+
const originalText = btn.innerText;
|
|
710
|
+
btn.innerText = 'Saving...';
|
|
711
|
+
btn.disabled = true;
|
|
712
|
+
btn.classList.add('opacity-75', 'cursor-not-allowed');
|
|
713
|
+
|
|
714
|
+
try {
|
|
715
|
+
const payload = buildPayload();
|
|
716
|
+
|
|
717
|
+
const isToggleOn = (name) => {
|
|
718
|
+
const el = document.querySelector(`[name="toggle.${name}"]`);
|
|
719
|
+
return el ? el.checked : false;
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const cfg = {
|
|
723
|
+
embedding: payload.embedding,
|
|
724
|
+
llm: payload.llm,
|
|
725
|
+
database: payload.database,
|
|
726
|
+
chunking: payload.chunking,
|
|
727
|
+
reranking: payload.reranking
|
|
728
|
+
};
|
|
729
|
+
if (isToggleOn('retrieval')) cfg.retrieval = payload.retrieval;
|
|
730
|
+
if (isToggleOn('metadata')) cfg.metadata = payload.metadata;
|
|
731
|
+
const qpKey = isPythonBackend ? 'query_planning' : 'queryPlanning';
|
|
732
|
+
if (isToggleOn('queryPlanning')) cfg[qpKey] = payload[qpKey];
|
|
733
|
+
if (isToggleOn('grounding')) cfg.grounding = payload.grounding;
|
|
734
|
+
if (isToggleOn('generation')) cfg.generation = payload.generation;
|
|
735
|
+
if (isToggleOn('prompts')) cfg.prompts = payload.prompts;
|
|
736
|
+
|
|
737
|
+
const cleanCfg = JSON.parse(JSON.stringify(cfg));
|
|
738
|
+
|
|
739
|
+
let code = '';
|
|
740
|
+
if (!isPythonBackend) {
|
|
741
|
+
code = `const config = ${JSON.stringify(cleanCfg, null, 2)};\nmodule.exports = config;`;
|
|
742
|
+
} else {
|
|
743
|
+
let py = JSON.stringify(cleanCfg, null, 2);
|
|
744
|
+
py = py.replace(/true/g, 'True').replace(/false/g, 'False').replace(/null/g, 'None');
|
|
745
|
+
py = py.replace(/"/g, "'");
|
|
746
|
+
code = `config = ${py}`;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const res = await fetch('/config', {
|
|
750
|
+
method: 'POST',
|
|
751
|
+
headers: { 'Content-Type': 'application/json' },
|
|
752
|
+
body: JSON.stringify({ backend: isPythonBackend ? 'python' : 'node', code, config: cleanCfg })
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
const data = await res.json();
|
|
756
|
+
|
|
757
|
+
if (res.ok) {
|
|
758
|
+
showStatus('Saved successfully!', 'success');
|
|
759
|
+
} else {
|
|
760
|
+
throw new Error(data.error || 'Unknown error');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
} catch (e) {
|
|
764
|
+
showStatus(e.message, 'error');
|
|
765
|
+
} finally {
|
|
766
|
+
btn.innerText = originalText;
|
|
767
|
+
btn.disabled = false;
|
|
768
|
+
btn.classList.remove('opacity-75', 'cursor-not-allowed');
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function showStatus(msg, type) {
|
|
773
|
+
const el = document.getElementById('status-msg');
|
|
774
|
+
el.textContent = msg;
|
|
775
|
+
|
|
776
|
+
if (type === 'success') {
|
|
777
|
+
el.className = 'text-sm font-medium transition-colors duration-300 text-green-600';
|
|
778
|
+
} else {
|
|
779
|
+
el.className = 'text-sm font-medium transition-colors duration-300 text-red-600';
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
setTimeout(() => {
|
|
783
|
+
el.textContent = '';
|
|
784
|
+
}, 3000);
|
|
785
|
+
}
|