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 = ` `; 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 = ``; 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 = '