You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
551 lines
26 KiB
551 lines
26 KiB
document.addEventListener('DOMContentLoaded', () => {
|
|
const scenarioList = document.getElementById('scenario-list');
|
|
const weatherSelect = document.getElementById('weather');
|
|
const framesInput = document.getElementById('frames');
|
|
const launchForm = document.getElementById('launch-form');
|
|
const launchBtn = document.getElementById('launch-btn');
|
|
const launchText = document.getElementById('launch-text');
|
|
const launchSpinner = document.getElementById('launch-spinner');
|
|
const terminalOutput = document.getElementById('terminal-output');
|
|
const clearTerminalBtn = document.getElementById('clear-terminal');
|
|
|
|
// New Elements
|
|
const stopBtn = document.getElementById('stop-btn');
|
|
const simStatusDot = document.getElementById('sim-status-dot');
|
|
const simStatusText = document.getElementById('sim-status-text');
|
|
const launchSimBtn = document.getElementById('launch-sim-btn');
|
|
const killSimBtn = document.getElementById('kill-sim-btn');
|
|
const exitAllBtn = document.getElementById('exit-all-btn');
|
|
|
|
// Idle Toggle Elements
|
|
const idleToggleContainer = document.getElementById('idle-toggle-container');
|
|
const gpuIdleToggle = document.getElementById('gpu-idle-toggle');
|
|
|
|
let currentEventSource = null;
|
|
let idleTimer = null;
|
|
let isSimulatorRunning = false;
|
|
let isSimulationActive = false; // user currently running a scenario
|
|
|
|
// --- Collapsible Config Card Logic ---
|
|
const configSection = document.getElementById('config-section');
|
|
const configToggleHeader = document.getElementById('config-toggle-header');
|
|
|
|
// Initial State Persistence
|
|
if (localStorage.getItem('config_collapsed') === 'true') {
|
|
configSection.classList.add('collapsed');
|
|
}
|
|
|
|
configToggleHeader.addEventListener('click', () => {
|
|
configSection.classList.toggle('collapsed');
|
|
localStorage.setItem('config_collapsed', configSection.classList.contains('collapsed'));
|
|
});
|
|
|
|
// Auto Idle Tracker
|
|
function resetIdleTimer() {
|
|
if (idleTimer) clearTimeout(idleTimer);
|
|
// If simulation is not active and simulator is ready but NOT paused
|
|
if (!isSimulationActive && isSimulatorRunning && gpuIdleToggle && !gpuIdleToggle.checked) {
|
|
idleTimer = setTimeout(() => {
|
|
console.log("Auto-idling simulator due to 30s inactivity...");
|
|
gpuIdleToggle.checked = true;
|
|
gpuIdleToggle.dispatchEvent(new Event('change'));
|
|
}, 30000);
|
|
}
|
|
}
|
|
|
|
if (gpuIdleToggle) {
|
|
gpuIdleToggle.addEventListener('change', (e) => {
|
|
const isIdle = e.target.checked;
|
|
const endpoint = isIdle ? '/api/simulator/idle' : '/api/simulator/wake';
|
|
fetch(endpoint, { method: 'POST' });
|
|
if (!isIdle) {
|
|
resetIdleTimer();
|
|
} else {
|
|
if (idleTimer) clearTimeout(idleTimer);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Simulator Status Polling
|
|
function updateSimulatorStatus() {
|
|
fetch('/api/simulator/status')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const status = data.status; // offline, loading, ready
|
|
simStatusDot.className = 'dot';
|
|
|
|
if (status === 'ready') {
|
|
if (!isSimulatorRunning) {
|
|
isSimulatorRunning = true;
|
|
resetIdleTimer(); // start tracker as soon as it becomes ready
|
|
}
|
|
if (idleToggleContainer) idleToggleContainer.style.display = 'flex';
|
|
|
|
if (gpuIdleToggle && gpuIdleToggle.checked !== data.is_paused) {
|
|
gpuIdleToggle.checked = data.is_paused;
|
|
}
|
|
|
|
if (data.is_paused) {
|
|
simStatusDot.classList.add('yellow');
|
|
simStatusText.textContent = 'Ready (Idle)';
|
|
} else {
|
|
simStatusDot.classList.add('green');
|
|
simStatusText.textContent = 'Ready';
|
|
}
|
|
launchSimBtn.style.display = 'none';
|
|
if (killSimBtn) killSimBtn.style.display = 'block';
|
|
} else if (status === 'loading') {
|
|
isSimulatorRunning = false;
|
|
if (idleToggleContainer) idleToggleContainer.style.display = 'none';
|
|
simStatusDot.classList.add('yellow');
|
|
simStatusText.textContent = 'Loading...';
|
|
launchSimBtn.style.display = 'none';
|
|
if (killSimBtn) killSimBtn.style.display = 'block';
|
|
} else {
|
|
isSimulatorRunning = false;
|
|
if (idleToggleContainer) idleToggleContainer.style.display = 'none';
|
|
if (idleTimer) clearTimeout(idleTimer);
|
|
|
|
simStatusDot.classList.add('red');
|
|
simStatusText.textContent = 'Offline';
|
|
launchSimBtn.style.display = 'block';
|
|
if (killSimBtn) killSimBtn.style.display = 'none';
|
|
}
|
|
})
|
|
.catch(() => {
|
|
simStatusDot.className = 'dot red';
|
|
simStatusText.textContent = 'Error';
|
|
});
|
|
}
|
|
|
|
updateSimulatorStatus();
|
|
setInterval(updateSimulatorStatus, 3000);
|
|
|
|
// Launch CarlaUE4 action
|
|
launchSimBtn.addEventListener('click', () => {
|
|
simStatusText.textContent = 'Starting...';
|
|
simStatusDot.className = 'dot yellow';
|
|
fetch('/api/simulator/launch', { method: 'POST' });
|
|
});
|
|
|
|
// Kill CarlaUE4 action
|
|
if (killSimBtn) {
|
|
killSimBtn.addEventListener('click', () => {
|
|
if (confirm("Are you sure you want to FORCE KILL CarlaUE4?\n\nThis will terminate the engine immediately. Any unsaved simulation data or unsynced frames will be lost.")) {
|
|
fetch('/api/simulator/kill', { method: 'POST' })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
appendLog('> CarlaUE4 process terminated by user.', 'warn');
|
|
updateSimulatorStatus();
|
|
}
|
|
})
|
|
.catch(err => appendLog(`> Failed to kill simulator: ${err.message}`, 'error'));
|
|
}
|
|
});
|
|
}
|
|
|
|
// EXIT ALL action
|
|
if (exitAllBtn) {
|
|
exitAllBtn.addEventListener('click', () => {
|
|
if (confirm("FULL SYSTEM EXIT\n\nThis will:\n1. Terminate CarlaUE4\n2. Shutdown the Dashboard Server\n3. Close this browser window\n\nProceed ? (use with caution)")) {
|
|
appendLog('\n> [CRITICAL] Initiating full system shutdown...', 'error');
|
|
|
|
// 1. Kill CARLA
|
|
fetch('/api/simulator/kill', { method: 'POST' });
|
|
|
|
// 2. Shutdown Server
|
|
fetch('/api/shutdown', { method: 'POST' });
|
|
|
|
// 3. UI Feedback and self-destruct window
|
|
setTimeout(() => {
|
|
document.body.innerHTML = `
|
|
<div style="height:100vh; width: 100vw; position: fixed; top: 0; left: 0; z-index: 9999; display:flex; flex-direction:column; align-items:center; justify-content:center; background:radial-gradient(circle at center, #1a202c, #0d1117); color:white; font-family:'Inter', sans-serif; text-align:center;">
|
|
<div style="width: 80px; height: 80px; background: #f85149; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-bottom: 2rem; box-shadow: 0 0 40px rgba(248, 81, 73, 0.6); animation: pulse 2s infinite;">
|
|
<svg viewBox="0 0 24 24" width="40" height="40" stroke="white" stroke-width="2" fill="none"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
|
|
</div>
|
|
<h1 style="font-size: 2.5rem; margin-bottom: 0.5rem; letter-spacing: -1.5px; font-weight: 800; background: linear-gradient(to bottom, #fff, #8b949e); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">SYSTEM OFFLINE</h1>
|
|
<p style="color: #8b949e; font-size: 1.2rem; max-width: 400px;">All simulation processes and the dashboard server have been terminated.</p>
|
|
<p style="margin-top: 3rem; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; color: #484f58; border: 1px solid #30363d; padding: 0.5rem 1rem; border-radius: 6px;">You can now safely close this tab.</p>
|
|
<style>
|
|
@keyframes pulse {
|
|
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.7); }
|
|
70% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(248, 81, 73, 0); }
|
|
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(248, 81, 73, 0); }
|
|
}
|
|
</style>
|
|
</div>
|
|
`;
|
|
setTimeout(() => window.close(), 2000);
|
|
}, 500);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fetch Initial Config
|
|
fetch('/api/config')
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
// Populate Scenarios
|
|
config.scenarios.forEach((scenario, index) => {
|
|
const isChecked = scenario === config.default_scenario ? 'checked' : '';
|
|
const cardHtml = `
|
|
<label class="scenario-card">
|
|
<input type="radio" name="scenario" value="${scenario}" ${isChecked}>
|
|
<div class="card-content">
|
|
<span class="card-title">${scenario}</span>
|
|
</div>
|
|
</label>
|
|
`;
|
|
scenarioList.insertAdjacentHTML('beforeend', cardHtml);
|
|
});
|
|
|
|
// Listen for scenario changes to load dynamic parameters
|
|
const scenarioRadios = document.querySelectorAll('input[name="scenario"]');
|
|
scenarioRadios.forEach(radio => {
|
|
radio.addEventListener('change', (e) => loadScenarioParams(e.target.value));
|
|
});
|
|
|
|
// Load params for the initially selected scenario
|
|
if (config.default_scenario) {
|
|
loadScenarioParams(config.default_scenario);
|
|
}
|
|
|
|
// Populate Weather
|
|
config.weather_options.forEach(weather => {
|
|
const isSelected = weather === config.default_weather ? 'selected' : '';
|
|
const optionHtml = `<option value="${weather}" ${isSelected}>${weather}</option>`;
|
|
weatherSelect.insertAdjacentHTML('beforeend', optionHtml);
|
|
});
|
|
|
|
// Default Frames
|
|
framesInput.value = config.default_frames;
|
|
|
|
appendLog(`> Config loaded successfully. Found ${config.scenarios.length} scenarios.`, 'system');
|
|
})
|
|
.catch(error => {
|
|
appendLog(`> Error loading config: ${error.message}`, 'error');
|
|
});
|
|
|
|
// Handle Launch
|
|
launchForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
|
|
isSimulationActive = true;
|
|
if (idleTimer) clearTimeout(idleTimer);
|
|
|
|
// Ensure simulator is awake before scenario runs just in case
|
|
if (gpuIdleToggle && gpuIdleToggle.checked) {
|
|
gpuIdleToggle.checked = false;
|
|
fetch('/api/simulator/wake', { method: 'POST' });
|
|
}
|
|
|
|
// UI Loading State
|
|
setLoadingState(true);
|
|
appendLog(`\n> --- NEW SIMULATION RUN ---`, 'info');
|
|
|
|
// Close existing stream if any
|
|
if (currentEventSource) {
|
|
currentEventSource.close();
|
|
}
|
|
|
|
const formData = new FormData(launchForm);
|
|
|
|
// Scenario radions are in the sidebar, outside the form
|
|
const scenarioRadio = document.querySelector('input[name="scenario"]:checked');
|
|
const scenarioValue = scenarioRadio ? scenarioRadio.value : 'braking';
|
|
|
|
// Serialize dynamic parameters
|
|
const paramInputs = document.querySelectorAll('#dynamic-params-container input');
|
|
let paramsString = [];
|
|
paramInputs.forEach(input => {
|
|
if (input.value !== '') {
|
|
paramsString.push(`${input.name}=${input.value}`);
|
|
}
|
|
});
|
|
|
|
const payload = {
|
|
scenario: scenarioValue,
|
|
frames: formData.get('frames'),
|
|
weather: formData.get('weather'),
|
|
no_record: formData.get('no_record') === 'on',
|
|
params: paramsString.join(',')
|
|
};
|
|
|
|
// We use fetch API to trigger the run (which could take long), but Server Sent Events (SSE)
|
|
// doesn't support POST natively without fetch streams. So we do a POST fetch that returns an SSE stream.
|
|
startStream('/api/run', payload);
|
|
});
|
|
|
|
// Clear Terminal
|
|
clearTerminalBtn.addEventListener('click', () => {
|
|
terminalOutput.innerHTML = '';
|
|
appendLog('> Terminal cleared.', 'system');
|
|
});
|
|
|
|
// Stop Simulation
|
|
if (stopBtn) {
|
|
stopBtn.addEventListener('click', () => {
|
|
stopBtn.disabled = true;
|
|
stopBtn.querySelector('.btn-text').textContent = 'Stopping...';
|
|
appendLog('\n> Sending stop signal to simulation. Waiting for graceful exit...', 'warn');
|
|
|
|
fetch('/api/stop', { method: 'POST' })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
console.log('Stop requested:', data.message);
|
|
})
|
|
.catch(err => {
|
|
appendLog('> Failed to send stop signal: ' + err.message, 'error');
|
|
stopBtn.disabled = false;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Utility: Append log to terminal
|
|
function appendLog(text, type = '') {
|
|
const line = document.createElement('div');
|
|
line.className = `log-line ${type}`;
|
|
|
|
// Advanced heuristic for colored logs
|
|
if (text.includes('[INFO]')) line.classList.add('info');
|
|
if (text.includes('[WARN]')) line.classList.add('warn');
|
|
if (text.includes('[ERROR]')) line.classList.add('error');
|
|
if (text.includes('[SUCCESS]')) line.classList.add('success');
|
|
if (text.includes('[FRAME ')) line.classList.add('frame-log');
|
|
if (text.includes('Traceback') || text.includes('Exception')) line.classList.add('error');
|
|
|
|
line.textContent = text;
|
|
|
|
// Smart Scroll Lock
|
|
// Check if the user is currently at the bottom of the scroll view
|
|
const isAtBottom = terminalOutput.scrollHeight - terminalOutput.scrollTop - terminalOutput.clientHeight < 50;
|
|
|
|
terminalOutput.appendChild(line);
|
|
|
|
if (isAtBottom) {
|
|
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Utility: Post JSON and stream response
|
|
async function startStream(url, payload) {
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder('utf-8');
|
|
let buffer = '';
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
buffer += decoder.decode(value, { stream: true });
|
|
let boundary = buffer.indexOf('\n\n');
|
|
|
|
while (boundary !== -1) {
|
|
const chunk = buffer.substring(0, boundary);
|
|
buffer = buffer.substring(boundary + 2);
|
|
|
|
if (chunk.startsWith('data: ')) {
|
|
const data = chunk.substring(6);
|
|
|
|
// Check for completion marker
|
|
if (data.startsWith('[PROCESS_COMPLETED]')) {
|
|
appendLog(`> Simulation Finished. ${data}`, 'info');
|
|
setLoadingState(false);
|
|
document.getElementById('sim-progress-container').style.display = 'none';
|
|
document.getElementById('shenron-progress-container').style.display = 'none';
|
|
document.getElementById('shenron-telemetry-hud').style.display = 'none';
|
|
|
|
isSimulationActive = false;
|
|
resetIdleTimer();
|
|
|
|
// ---- SHENRON INIT TELEMETRY ----
|
|
} else if (data.includes('[SHENRON_INIT]')) {
|
|
try {
|
|
const jsonStr = data.substring(data.indexOf('[SHENRON_INIT]') + 14);
|
|
const tel = JSON.parse(jsonStr);
|
|
renderShenronTelemetry(tel);
|
|
appendLog(`> [SHENRON] Radar synthesis started — ${tel.total_frames} frames`, 'success');
|
|
} catch(e) {
|
|
appendLog(data);
|
|
}
|
|
|
|
// ---- SHENRON STEP PROGRESS ----
|
|
} else if (data.includes('[SHENRON_STEP]')) {
|
|
try {
|
|
const jsonStr = data.substring(data.indexOf('[SHENRON_STEP]') + 14);
|
|
const step = JSON.parse(jsonStr);
|
|
updateShenronProgress(step);
|
|
} catch(e) {
|
|
appendLog(data);
|
|
}
|
|
|
|
} else {
|
|
// Intercept CARLA simulation progress bar
|
|
if (data.includes('SIMULATING:')) {
|
|
const match = data.match(/(\d+)%\|.*\|\s*(\d+)\/(\d+)/);
|
|
if (match) {
|
|
document.getElementById('sim-progress-container').style.display = 'block';
|
|
document.getElementById('sim-progress-fill').style.width = match[1] + '%';
|
|
document.getElementById('sim-progress-text').textContent = `${match[1]}% (${match[2]} / ${match[3]} frames)`;
|
|
continue; // Skip appending this line to the terminal box
|
|
}
|
|
}
|
|
|
|
// Filter out tqdm-style progress bars (containing |###)
|
|
if (data.includes('|#') || data.includes('|█') || data.match(/\d+%\|/)) {
|
|
continue; // Skip tqdm bars entirely
|
|
}
|
|
|
|
// Filter out suppressed internal logs
|
|
if (data.includes('Simulating Radar:') ||
|
|
data.includes('------- Using') ||
|
|
data.includes('Number of points =') ||
|
|
data.includes('[ITER 17]') ||
|
|
data.includes('Synced global config:')) {
|
|
continue; // These are now handled by telemetry
|
|
}
|
|
|
|
// Prevent carriage return visual artifacts
|
|
if (data.includes('\r')) {
|
|
const parts = data.split('\r');
|
|
const lastPart = parts[parts.length - 1];
|
|
const lastLine = terminalOutput.lastElementChild;
|
|
if (lastLine && !lastLine.textContent.includes('\n')) {
|
|
lastLine.textContent = lastPart;
|
|
continue;
|
|
}
|
|
}
|
|
appendLog(data);
|
|
}
|
|
}
|
|
boundary = buffer.indexOf('\n\n');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
appendLog(`> Connection error: ${error.message}`, 'error');
|
|
setLoadingState(false);
|
|
}
|
|
}
|
|
|
|
// ---- SHENRON TELEMETRY RENDERING (Compact Chips) ----
|
|
|
|
function renderShenronTelemetry(tel) {
|
|
const hud = document.getElementById('shenron-telemetry-hud');
|
|
const hwRow = document.getElementById('shenron-telemetry-hw');
|
|
const radarRow = document.getElementById('shenron-telemetry-radars');
|
|
const shenronContainer = document.getElementById('shenron-progress-container');
|
|
|
|
hud.style.display = 'flex';
|
|
shenronContainer.style.display = 'block';
|
|
|
|
// Row 1: Hardware + Session
|
|
let hw = '';
|
|
hw += chip('GPU', tel.gpu.name, 'c-emerald');
|
|
hw += chip('VRAM', `${tel.gpu.vram_gb} GB`, 'c-blue');
|
|
hw += chip('Backend', tel.gpu.backend, 'c-blue');
|
|
hw += '<span class="tel-separator"></span>';
|
|
hw += chip('Session', tel.session, 'c-rose');
|
|
hw += chip('Frames', tel.total_frames, 'c-emerald');
|
|
hwRow.innerHTML = hw;
|
|
|
|
// Row 2: Radar specs (compact, separated by type)
|
|
let rd = '';
|
|
const radarEntries = Object.entries(tel.radars);
|
|
radarEntries.forEach(([rType, specs], idx) => {
|
|
if (idx > 0) rd += '<span class="tel-separator"></span>';
|
|
rd += chip('Radar', rType.toUpperCase(), 'c-amber');
|
|
rd += chip('Freq', `${specs.freq_ghz}GHz`, 'c-blue');
|
|
rd += chip('BW', `${specs.bw_mhz}MHz`, 'c-cyan');
|
|
rd += chip('Chirps', specs.chirps, 'c-emerald');
|
|
rd += chip('Ant', `${specs.antennas}vRx`, 'c-emerald');
|
|
rd += chip('RRes', `${specs.range_res_m}m`, 'c-blue');
|
|
rd += chip('Gain', `${specs.gain_db}dB`, 'c-amber');
|
|
});
|
|
radarRow.innerHTML = rd;
|
|
}
|
|
|
|
function updateShenronProgress(step) {
|
|
const fill = document.getElementById('shenron-progress-fill');
|
|
const text = document.getElementById('shenron-progress-text');
|
|
const eta = document.getElementById('shenron-eta');
|
|
const container = document.getElementById('shenron-progress-container');
|
|
const liveRow = document.getElementById('shenron-telemetry-live');
|
|
|
|
container.style.display = 'block';
|
|
fill.style.width = step.pct + '%';
|
|
text.textContent = `${step.pct}% (${step.frame} / ${step.total} frames)`;
|
|
eta.textContent = `${step.fps} fps · ETA: ${step.eta}`;
|
|
|
|
// Update live signal metrics as inline chips
|
|
if (step.metrics && liveRow) {
|
|
let live = '';
|
|
const entries = Object.entries(step.metrics);
|
|
entries.forEach(([rType, m], idx) => {
|
|
if (idx > 0) live += '<span class="tel-separator"></span>';
|
|
live += chip(rType, '', 'c-amber');
|
|
live += chip('SNR', `${m.snr}dB`, 'c-amber');
|
|
live += chip('Pts', m.pts, 'c-emerald');
|
|
live += chip('Bins', m.bins, 'c-blue');
|
|
});
|
|
liveRow.innerHTML = live;
|
|
}
|
|
}
|
|
|
|
function chip(label, value, colorClass) {
|
|
return `<span class="tel-chip"><span class="chip-label">${label}</span><span class="chip-value ${colorClass}">${value}</span></span>`;
|
|
}
|
|
|
|
function setLoadingState(isLoading) {
|
|
launchBtn.disabled = isLoading;
|
|
launchBtn.style.display = isLoading ? 'none' : 'block';
|
|
if (stopBtn) {
|
|
stopBtn.style.display = isLoading ? 'block' : 'none';
|
|
}
|
|
|
|
if (!isLoading) {
|
|
launchText.textContent = "Launch Another Simulation";
|
|
if (stopBtn) {
|
|
stopBtn.disabled = false;
|
|
stopBtn.querySelector('.btn-text').textContent = 'Stop Simulation';
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility: Fetch and render dynamic scenario parameters
|
|
function loadScenarioParams(scenarioName) {
|
|
const container = document.getElementById('dynamic-params-container');
|
|
container.innerHTML = '<div class="input-group full-width"><span class="helper">Loading parameters...</span></div>';
|
|
|
|
fetch(`/api/scenario_params/${scenarioName}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
container.innerHTML = '';
|
|
if (data.success && Object.keys(data.params).length > 0) {
|
|
Object.entries(data.params).forEach(([key, val]) => {
|
|
const inputHtml = `
|
|
<div class="input-group">
|
|
<label for="param_${key}">${key}</label>
|
|
<div class="input-wrapper">
|
|
<input type="text" id="param_${key}" name="${key}" value="${val}">
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', inputHtml);
|
|
});
|
|
} else {
|
|
container.innerHTML = '<div class="input-group full-width"><span class="helper">No tunable constants found for this scenario.</span></div>';
|
|
}
|
|
})
|
|
.catch(err => {
|
|
container.innerHTML = '<div class="input-group full-width"><span class="helper" style="color:var(--danger)">Failed to load parameters.</span></div>';
|
|
});
|
|
}
|
|
});
|