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.
217 lines
9.2 KiB
217 lines
9.2 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');
|
|
|
|
let currentEventSource = null;
|
|
|
|
// 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();
|
|
|
|
// 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');
|
|
});
|
|
|
|
// Utility: Append log to terminal
|
|
function appendLog(text, type = '') {
|
|
const line = document.createElement('div');
|
|
line.className = `log-line ${type}`;
|
|
|
|
// Simple 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('Traceback') || text.includes('Exception')) line.classList.add('error');
|
|
|
|
line.textContent = text;
|
|
terminalOutput.appendChild(line);
|
|
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);
|
|
} else {
|
|
// Only append if it's not a carriage return override (like tqdm progress bars)
|
|
// We do a simple replacement to simulate carriage returns in HTML
|
|
if (data.includes('\r')) {
|
|
const parts = data.split('\r');
|
|
const lastPart = parts[parts.length - 1];
|
|
// Replace the content of the very last line if it exists
|
|
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);
|
|
}
|
|
}
|
|
|
|
function setLoadingState(isLoading) {
|
|
launchBtn.disabled = isLoading;
|
|
launchText.style.display = isLoading ? 'none' : 'block';
|
|
launchSpinner.style.display = isLoading ? 'block' : 'none';
|
|
|
|
if (!isLoading) {
|
|
launchText.textContent = "Launch Another 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>';
|
|
});
|
|
}
|
|
});
|