aboutsummaryrefslogtreecommitdiff
path: root/modules/core
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-05-31 06:00:55 +0200
committerAgando <roduccomm@gmail.com>2026-05-31 06:00:55 +0200
commit7cc82d2d32e44ab1c5b7d042d5522d036d9aa0be (patch)
tree0a64e32f8b77043677e62485d7a0ecdf77e5c2d3 /modules/core
parentd8bf7fa398d6d928325be9b5c64ca24875bfeef8 (diff)
auto: 2026-05-31_06:00 changes
Diffstat (limited to 'modules/core')
-rw-r--r--modules/core/module-aiassist.js47
-rw-r--r--modules/core/ollama-manager.js253
-rw-r--r--modules/core/terminal.js97
3 files changed, 270 insertions, 127 deletions
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index f6fadb5..8f8a60c 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -415,7 +415,15 @@ RULES:
}
this._cancelled = false;
this.terminal.setAiBusy(true);
- while (this._chatLock) await new Promise(r => setTimeout(r, 50));
+ const lockStart = Date.now();
+ while (this._chatLock) {
+ if (Date.now() - lockStart > 10000) {
+ this._chatLock = false;
+ this.terminal.print('⚠️ Chat lock timed out — force released', 'warning');
+ break;
+ }
+ await new Promise(r => setTimeout(r, 50));
+ }
this._chatLock = true;
this.terminal.markDirty();
// Re-read context from localStorage (auto-restore may have written it after init)
@@ -545,18 +553,35 @@ RULES:
}
_trackToolFailure(toolName, error) {
try {
+ if (!this._toolFailuresBuffer) this._toolFailuresBuffer = {};
+ if (!this._toolFailuresBuffer[toolName]) this._toolFailuresBuffer[toolName] = { count: 0, last: '', errors: [] };
+ const entry = this._toolFailuresBuffer[toolName];
+ entry.count++;
+ entry.last = new Date().toISOString();
+ entry.errors.push(error);
+ if (entry.errors.length > 5) entry.errors.shift();
+ if (entry.count >= 3) {
+ this.terminal.print(`⚠️ Tool '${toolName}' has failed ${entry.count} times. Consider creating a workaround skill.`, 'warning');
+ }
+ if (this._toolFailureFlushTimer) clearTimeout(this._toolFailureFlushTimer);
+ this._toolFailureFlushTimer = setTimeout(() => this._flushToolFailures(), 2000);
+ } catch (e) { /* ignore */ }
+ }
+ _flushToolFailures() {
+ try {
+ if (!this._toolFailuresBuffer || Object.keys(this._toolFailuresBuffer).length === 0) return;
const key = 'webx11_tool_failures';
const raw = localStorage.getItem(key) || '{}';
const map = JSON.parse(raw);
- if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] };
- map[toolName].count++;
- map[toolName].last = new Date().toISOString();
- map[toolName].errors.push(error);
- if (map[toolName].errors.length > 5) map[toolName].errors.shift();
- localStorage.setItem(key, JSON.stringify(map));
- if (map[toolName].count >= 3) {
- this.terminal.print(`⚠️ Tool '${toolName}' has failed ${map[toolName].count} times. Consider creating a workaround skill.`, 'warning');
+ for (const [toolName, entry] of Object.entries(this._toolFailuresBuffer)) {
+ if (!map[toolName]) map[toolName] = { count: 0, last: '', errors: [] };
+ map[toolName].count += entry.count;
+ map[toolName].last = entry.last;
+ map[toolName].errors.push(...entry.errors);
+ if (map[toolName].errors.length > 10) map[toolName].errors = map[toolName].errors.slice(-10);
}
+ localStorage.setItem(key, JSON.stringify(map));
+ this._toolFailuresBuffer = {};
} catch (e) { /* ignore */ }
}
async agenticLoop(messages) {
@@ -781,7 +806,7 @@ RULES:
}
}, 500);
this._confirmTimerInterval = timerInterval;
- setTimeout(() => {
+ this._confirmTimeout = setTimeout(() => {
clearInterval(timerInterval);
if (this._confirmResolve) {
this._confirmResolve(false);
@@ -804,6 +829,8 @@ RULES:
}
_cleanupConfirmBar() {
clearInterval(this._confirmTimerInterval);
+ clearTimeout(this._confirmTimeout);
+ this._confirmTimeout = null;
this._confirmTimerInterval = null;
this._confirmResolve = null;
this._pendingConfirmation = null;
diff --git a/modules/core/ollama-manager.js b/modules/core/ollama-manager.js
index 420cbc3..ab3ec2c 100644
--- a/modules/core/ollama-manager.js
+++ b/modules/core/ollama-manager.js
@@ -13,6 +13,8 @@ class OllamaManager {
this._abortController = null;
// --- AI Agent Loop Optimization Toggle ---
this.optimizationEnabled = localStorage.getItem('webx11_optimization_enabled') !== 'false';
+ // --- Streaming Toggle ---
+ this.streamEnabled = localStorage.getItem('webx11_stream_enabled') !== 'false';
// --- Validator Agent Config (future) ---
this.validatorEnabled = localStorage.getItem('webx11_validator_enabled') === 'true';
this.validatorModel = localStorage.getItem('webx11_validator_model') || '';
@@ -359,48 +361,53 @@ class OllamaManager {
async _chatWithProvider(provider, prompt, model) {
const useModel = model || provider.model || this.currentModel;
let fullText = '';
+ this._abortController = new AbortController();
+ const signal = this._abortController.signal;
try {
if (provider.type === 'ollama') {
- this._abortController = new AbortController();
- const signal = this._abortController.signal;
const response = await fetch(`${provider.endpoint}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ model: useModel, prompt: prompt, stream: true }),
+ body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }),
signal
});
- this._abortController = null;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let isFirstToken = true;
+ if (this.streamEnabled) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let isFirstToken = true;
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
- const chunk = decoder.decode(value, { stream: true });
- const lines = chunk.split('\n').filter(l => l.trim());
+ const chunk = decoder.decode(value, { stream: true });
+ const lines = chunk.split('\n').filter(l => l.trim());
- for (const line of lines) {
- try {
- const parsed = JSON.parse(line);
- if (parsed.response) {
- fullText += parsed.response;
- this.terminal.printStreamingResponse(parsed.response, !isFirstToken);
- isFirstToken = false;
+ for (const line of lines) {
+ try {
+ const parsed = JSON.parse(line);
+ if (parsed.response) {
+ fullText += parsed.response;
+ this.terminal.printStreamingResponse(parsed.response, !isFirstToken);
+ isFirstToken = false;
+ }
+ } catch (e) {
+ // Skip malformed partial lines
}
- } catch (e) {
- // Skip malformed partial lines
}
}
+ } else {
+ const data = await response.json();
+ fullText = data.response || '';
+ if (fullText) {
+ this.terminal.printStreamingResponse(fullText, false);
+ }
}
return fullText || null;
} else {
// OpenAI-compatible provider
- this._abortController = new AbortController();
- const signal = this._abortController.signal;
const headers = { 'Content-Type': 'application/json' };
if (provider.apiKey) {
headers['Authorization'] = 'Bearer ' + provider.apiKey;
@@ -411,43 +418,50 @@ class OllamaManager {
body: JSON.stringify({
model: useModel,
messages: [{ role: 'user', content: prompt }],
- stream: true
+ stream: this.streamEnabled
}),
signal
});
- this._abortController = null;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let isFirstToken = true;
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
-
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
-
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed || trimmed === 'data: [DONE]') continue;
- if (trimmed.startsWith('data: ')) {
- try {
- const parsed = JSON.parse(trimmed.substring(6));
- const content = parsed.choices?.[0]?.delta?.content || '';
- if (content) {
- fullText += content;
- this.terminal.printStreamingResponse(content, !isFirstToken);
- isFirstToken = false;
+ if (this.streamEnabled) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let isFirstToken = true;
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed === 'data: [DONE]') continue;
+ if (trimmed.startsWith('data: ')) {
+ try {
+ const parsed = JSON.parse(trimmed.substring(6));
+ const content = parsed.choices?.[0]?.delta?.content || '';
+ if (content) {
+ fullText += content;
+ this.terminal.printStreamingResponse(content, !isFirstToken);
+ isFirstToken = false;
+ }
+ } catch (e) {
+ // Skip malformed SSE lines
}
- } catch (e) {
- // Skip malformed SSE lines
}
}
}
+ } else {
+ const data = await response.json();
+ fullText = data.choices?.[0]?.message?.content || '';
+ if (fullText) {
+ this.terminal.printStreamingResponse(fullText, false);
+ }
}
return fullText || null;
}
@@ -457,6 +471,8 @@ class OllamaManager {
return fullText;
}
return null; // signal failure so caller can try next provider
+ } finally {
+ this._abortController = null;
}
}
@@ -496,52 +512,58 @@ class OllamaManager {
async _chatStreamWithProvider(provider, prompt, onToken, onComplete, model) {
const useModel = model || provider.model || this.currentModel;
+ this._abortController = new AbortController();
+ const signal = this._abortController.signal;
try {
if (provider.type === 'ollama') {
- this._abortController = new AbortController();
- const signal = this._abortController.signal;
const response = await fetch(`${provider.endpoint}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ model: useModel, prompt: prompt, stream: true }),
+ body: JSON.stringify({ model: useModel, prompt: prompt, stream: this.streamEnabled }),
signal
});
- this._abortController = null;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
+
let fullResponse = '';
- let buffer = '';
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
- for (const line of lines) {
- if (!line.trim()) continue;
+ if (this.streamEnabled) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+ for (const line of lines) {
+ if (!line.trim()) continue;
+ try {
+ const obj = JSON.parse(line);
+ if (obj.response) {
+ fullResponse += obj.response;
+ onToken && onToken(obj.response, fullResponse);
+ }
+ } catch (e) { continue; }
+ }
+ }
+ if (buffer.trim()) {
try {
- const obj = JSON.parse(line);
- if (obj.response) {
- fullResponse += obj.response;
- onToken && onToken(obj.response, fullResponse);
- }
- } catch (e) { continue; }
+ const obj = JSON.parse(buffer);
+ if (obj.response) { fullResponse += obj.response; onToken && onToken(obj.response, fullResponse); }
+ } catch (e) {}
+ }
+ } else {
+ const data = await response.json();
+ fullResponse = data.response || '';
+ if (fullResponse) {
+ onToken && onToken(fullResponse, fullResponse);
}
- }
- if (buffer.trim()) {
- try {
- const obj = JSON.parse(buffer);
- if (obj.response) { fullResponse += obj.response; onToken && onToken(obj.response, fullResponse); }
- } catch (e) {}
}
this.fireResponseHooks(fullResponse);
onComplete && onComplete(fullResponse);
return fullResponse;
} else {
// OpenAI-compatible provider
- this._abortController = new AbortController();
- const signal = this._abortController.signal;
const headers = { 'Content-Type': 'application/json' };
if (provider.apiKey) {
headers['Authorization'] = 'Bearer ' + provider.apiKey;
@@ -552,38 +574,45 @@ class OllamaManager {
body: JSON.stringify({
model: useModel,
messages: [{ role: 'user', content: prompt }],
- stream: true
+ stream: this.streamEnabled
}),
signal
});
- this._abortController = null;
if (!response.ok) throw new Error(`HTTP ${response.status}`);
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
let fullResponse = '';
- let buffer = '';
-
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- buffer += decoder.decode(value, { stream: true });
- const lines = buffer.split('\n');
- buffer = lines.pop() || '';
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed || trimmed === 'data: [DONE]') continue;
- if (trimmed.startsWith('data: ')) {
- try {
- const parsed = JSON.parse(trimmed.substring(6));
- const content = parsed.choices?.[0]?.delta?.content || '';
- if (content) {
- fullResponse += content;
- onToken && onToken(content, fullResponse);
- }
- } catch (e) { continue; }
+ if (this.streamEnabled) {
+ const reader = response.body.getReader();
+ const decoder = new TextDecoder();
+ let buffer = '';
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) break;
+ buffer += decoder.decode(value, { stream: true });
+ const lines = buffer.split('\n');
+ buffer = lines.pop() || '';
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed === 'data: [DONE]') continue;
+ if (trimmed.startsWith('data: ')) {
+ try {
+ const parsed = JSON.parse(trimmed.substring(6));
+ const content = parsed.choices?.[0]?.delta?.content || '';
+ if (content) {
+ fullResponse += content;
+ onToken && onToken(content, fullResponse);
+ }
+ } catch (e) { continue; }
+ }
}
}
+ } else {
+ const data = await response.json();
+ fullResponse = data.choices?.[0]?.message?.content || '';
+ if (fullResponse) {
+ onToken && onToken(fullResponse, fullResponse);
+ }
}
this.fireResponseHooks(fullResponse);
onComplete && onComplete(fullResponse);
@@ -593,6 +622,8 @@ class OllamaManager {
this.terminal.print(`Stream error with ${provider.name}: ${error.message}`, 'error');
onComplete && onComplete(null);
return null;
+ } finally {
+ this._abortController = null;
}
}
@@ -654,7 +685,6 @@ class OllamaManager {
body: JSON.stringify(body),
signal
});
- this._abortController = null;
const networkTime = Math.round((Date.now() - t0) / 1000);
if (!response.ok) {
if (response.status === 400) {
@@ -696,7 +726,6 @@ class OllamaManager {
body: JSON.stringify(body),
signal
});
- this._abortController = null;
const networkTime = Math.round((Date.now() - t0) / 1000);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
@@ -729,11 +758,13 @@ class OllamaManager {
this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
body.stream = true;
delete body.stop;
+ this._abortController = new AbortController();
+ const retrySignal = this._abortController.signal;
const retryResponse = await fetch(`${provider.endpoint}/v1/chat/completions`, {
method: 'POST',
headers,
body: JSON.stringify(body),
- signal: this._abortController ? undefined : undefined
+ signal: retrySignal
});
if (retryResponse.ok) {
const reader = retryResponse.body.getReader();
@@ -907,6 +938,7 @@ class OllamaManager {
localStorage.setItem('webx11_ollama_endpoint', this.endpoint);
localStorage.setItem('webx11_ollama_model', this.currentModel);
localStorage.setItem('webx11_optimization_enabled', this.optimizationEnabled ? 'true' : 'false');
+ localStorage.setItem('webx11_stream_enabled', this.streamEnabled ? 'true' : 'false');
localStorage.setItem('webx11_validator_enabled', this.validatorEnabled ? 'true' : 'false');
localStorage.setItem('webx11_validator_model', this.validatorModel);
localStorage.setItem('webx11_validator_endpoint', this.validatorEndpoint || '');
@@ -918,6 +950,13 @@ class OllamaManager {
this.saveSettings();
}
+ setStreamEnabled(val) {
+ this.streamEnabled = !!val;
+ this.terminal?.print(`Response streaming ${this.streamEnabled ? 'enabled' : 'disabled'}.`, 'info');
+ this.terminal?.updateStreamIndicator();
+ this.saveSettings();
+ }
+
setValidatorConfig(enabled, model) {
this.validatorEnabled = !!enabled;
if (model !== undefined) {
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index de2f4a7..d53ab70 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -419,6 +419,7 @@
return fragments.length > 0 ? fragments : null;
}
printAIResponse(text) {
+ this._flushStreamChunks();
const fragments = this._renderHTMLBlock(text);
if (fragments) {
for (const frag of fragments) {
@@ -443,6 +444,7 @@
this._trimOutput();
}
printStreamingResponse(text, append) {
+ if (!this._streamChunks) this._streamChunks = [];
if (!append) {
const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
const wrapper = document.createElement('div');
@@ -452,16 +454,30 @@
<div class="ai-content">${safeText}</div>
`;
this.outputContainer.appendChild(wrapper);
+ this._streamChunks = [];
} else {
const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
if (contentDiv) {
- const safeText = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
- contentDiv.textContent += safeText;
+ this._streamChunks.push(text);
+ if (this._streamChunks.length >= 5) {
+ contentDiv.textContent += this._streamChunks.join('');
+ this._streamChunks = [];
+ if (contentDiv.textContent.length > 50000) {
+ contentDiv.textContent = contentDiv.textContent.slice(-30000);
+ }
+ }
}
}
this.scrollToBottom();
this._trimOutput();
}
+ _flushStreamChunks() {
+ const contentDiv = this.outputContainer.querySelector('.output-line.ollama:last-child .ai-content');
+ if (contentDiv && this._streamChunks && this._streamChunks.length > 0) {
+ contentDiv.textContent += this._streamChunks.join('');
+ this._streamChunks = [];
+ }
+ }
copyToClipboard(btn) {
const contentDiv = btn.nextElementSibling;
const text = contentDiv.innerText;
@@ -576,8 +592,9 @@
}
if (toRender.length > batchSize) {
const remaining = toRender.slice(batchSize);
- const doBatch = (index) => {
- if (index >= remaining.length) {
+ const MAX_BATCHES = 50;
+ const doBatch = (index, batchCount) => {
+ if (index >= remaining.length || batchCount >= MAX_BATCHES) {
this.scrollToBottom();
return;
}
@@ -589,10 +606,10 @@
line.textContent = remaining[i].l || '';
this.outputContainer.appendChild(line);
}
- doBatch(end);
+ doBatch(end, (batchCount || 0) + 1);
});
};
- doBatch(0);
+ doBatch(0, 0);
} else {
this.scrollToBottom();
}
@@ -652,12 +669,29 @@
this._autoSaveSession();
this._dirty = false;
}, 180000);
+ window.addEventListener('beforeunload', () => {
+ if (this._autoSaveTimer) {
+ clearInterval(this._autoSaveTimer);
+ this._autoSaveTimer = null;
+ }
+ });
}
_autoSaveSession() {
try {
this.fs.mkdirRecursive('_sessions');
- // Clone fsRoot and strip _sessions to prevent recursive nesting
- var rootClone = JSON.parse(JSON.stringify(this.fs.root));
+ var rootClone;
+ try {
+ const json = JSON.stringify(this.fs.root);
+ if (json.length > 5242880) { // 5MB cap
+ rootClone = { type: 'directory', name: '/', content: {}, _truncated: true };
+ this.print('⚠ Auto-save skipped (VFS too large)', 'warning');
+ return;
+ }
+ rootClone = JSON.parse(json);
+ } catch (e) {
+ rootClone = { type: 'directory', name: '/', content: {}, _error: true };
+ return;
+ }
if (rootClone && rootClone.content && rootClone.content['_sessions']) {
delete rootClone.content['_sessions'];
}
@@ -669,8 +703,12 @@
version: 1,
terminalLines: this._historyBuffer.slice(-50)
};
- this.fs.writeFile('_sessions/_auto.json', JSON.stringify(snapshot, null, 2));
- // Also persist AI context concurrently
+ const snapshotJson = JSON.stringify(snapshot);
+ if (snapshotJson.length > 5242880) {
+ this.print('⚠ Auto-save snapshot too large, skipping', 'warning');
+ return;
+ }
+ this.fs.writeFile('_sessions/_auto.json', snapshotJson);
try {
const ctx = localStorage.getItem('webx11_ai_context');
if (ctx) this.fs.writeFile('_sessions/_auto-context.json', ctx);
@@ -1451,6 +1489,28 @@
}
this.ollamaManager.saveSettings();
}
+ toggleStream() {
+ const cb = document.getElementById('ollama-stream-toggle');
+ const label = document.getElementById('ollama-stream-label');
+ const slider = document.getElementById('ollama-stream-slider');
+ const enabled = cb.checked;
+ this.ollamaManager.setStreamEnabled(enabled);
+ if (label) {
+ label.textContent = enabled ? 'Enabled' : 'Disabled';
+ label.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (slider) {
+ slider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ slider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ }
+ updateStreamIndicator() {
+ const el = document.getElementById('stream-status');
+ if (!el) return;
+ const enabled = this.ollamaManager?.streamEnabled !== false;
+ el.textContent = enabled ? '▶ Stream' : '🔇 No-Stream';
+ el.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
saveValidatorConfig() {
const select = document.getElementById('validator-model-select');
const endpoint = document.getElementById('validator-endpoint-input')?.value?.trim() || 'http://localhost:11434';
@@ -1529,6 +1589,23 @@
valSlider.style.borderColor = enabled ? 'var(--info-color)' : 'var(--border-color)';
}
}
+ // Initialize stream toggle from stored state
+ const strCb = document.getElementById('ollama-stream-toggle');
+ const strLabel = document.getElementById('ollama-stream-label');
+ const strSlider = document.getElementById('ollama-stream-slider');
+ if (strCb && this.ollamaManager) {
+ const enabled = this.ollamaManager.streamEnabled !== false;
+ strCb.checked = enabled;
+ if (strLabel) {
+ strLabel.textContent = enabled ? 'Enabled' : 'Disabled';
+ strLabel.style.color = enabled ? 'var(--success-color)' : 'var(--text-muted)';
+ }
+ if (strSlider) {
+ strSlider.style.background = enabled ? 'var(--accent-color)' : 'var(--bg-tertiary)';
+ strSlider.style.borderColor = enabled ? 'var(--success-color)' : 'var(--border-color)';
+ }
+ }
+ this.updateStreamIndicator();
// Populate validator model dropdown from stored config
const valSelect = document.getElementById('validator-model-select');
const valEndpoint = document.getElementById('validator-endpoint-input');