| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>OpenEnv Traffic Signal Optimization</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono:wght@400;700&display=swap'); |
| |
| :root { |
| --bg-color: #0d1117; |
| --panel-bg: rgba(22, 27, 34, 0.6); |
| --panel-border: rgba(48, 54, 61, 0.8); |
| --text-main: #c9d1d9; |
| --text-muted: #8b949e; |
| --accent-glow: rgba(56, 139, 253, 0.4); |
| --accent-color: #58a6ff; |
| --green-light: #3fb950; |
| --red-light: #f85149; |
| --ev-color: #ff7b72; |
| } |
| |
| body { |
| margin: 0; |
| padding: 20px; |
| background-color: var(--bg-color); |
| color: var(--text-main); |
| font-family: 'Outfit', sans-serif; |
| display: grid; |
| grid-template-columns: 300px 1fr 300px; |
| gap: 20px; |
| height: 100vh; |
| overflow: hidden; |
| background: radial-gradient(circle at 50% -20%, #1a2332, #0d1117 70%); |
| } |
| |
| .panel { |
| background: var(--panel-bg); |
| border: 1px solid var(--panel-border); |
| border-radius: 16px; |
| padding: 20px; |
| backdrop-filter: blur(12px); |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .header { |
| grid-column: 1 / -1; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| padding: 10px 20px; |
| background: var(--panel-bg); |
| border: 1px solid var(--panel-border); |
| border-radius: 16px; |
| margin-bottom: -10px; |
| z-index: 10; |
| } |
| |
| .header h1 { |
| font-size: 1.4rem; |
| margin: 0; |
| font-weight: 800; |
| background: linear-gradient(90deg, #58a6ff, #a371f7); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| } |
| |
| .badge { |
| background: rgba(88, 166, 255, 0.1); |
| color: var(--accent-color); |
| padding: 4px 12px; |
| border-radius: 20px; |
| font-size: 0.8rem; |
| font-weight: 600; |
| border: 1px solid rgba(88, 166, 255, 0.2); |
| } |
| |
| |
| .metric-group { |
| margin-bottom: 20px; |
| } |
| |
| .metric-label { |
| font-size: 0.8rem; |
| color: var(--text-muted); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 5px; |
| } |
| |
| .metric-value { |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 1.8rem; |
| font-weight: 700; |
| color: white; |
| text-shadow: 0 0 10px rgba(255, 255, 255, 0.2); |
| } |
| |
| .metric-value.good { color: var(--green-light); text-shadow: 0 0 10px rgba(63, 185, 80, 0.4); } |
| .metric-value.warn { color: #d29922; } |
| .metric-value.bad { color: var(--red-light); } |
| |
| |
| .controls { |
| margin-top: auto; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| } |
| |
| button { |
| background: rgba(255, 255, 255, 0.05); |
| border: 1px solid var(--panel-border); |
| color: white; |
| padding: 12px; |
| border-radius: 8px; |
| font-family: 'Outfit', sans-serif; |
| font-size: 1rem; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.2s ease; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| button:hover { |
| background: rgba(255, 255, 255, 0.1); |
| transform: translateY(-2px); |
| } |
| |
| button:active { |
| transform: translateY(1px); |
| } |
| |
| button.primary { |
| background: var(--accent-color); |
| color: #0d1117; |
| border: none; |
| box-shadow: 0 0 15px var(--accent-glow); |
| } |
| |
| button.primary:hover { |
| background: #79c0ff; |
| box-shadow: 0 0 20px var(--accent-glow); |
| } |
| |
| button.danger { |
| background: rgba(248, 81, 73, 0.1); |
| color: var(--red-light); |
| border-color: rgba(248, 81, 73, 0.3); |
| } |
| |
| button.danger:hover { |
| background: rgba(248, 81, 73, 0.2); |
| } |
| |
| |
| .visualizer { |
| position: relative; |
| background: #11161d; |
| border-radius: 16px; |
| border: 1px solid var(--panel-border); |
| overflow: hidden; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| box-shadow: inset 0 0 50px rgba(0,0,0,0.5); |
| } |
| |
| .road { |
| position: absolute; |
| background: #1e242c; |
| } |
| |
| .road-v { |
| width: 120px; |
| height: 100%; |
| border-left: 2px dashed #4b5363; |
| border-right: 2px dashed #4b5363; |
| } |
| |
| .road-h { |
| width: 100%; |
| height: 120px; |
| border-top: 2px dashed #4b5363; |
| border-bottom: 2px dashed #4b5363; |
| } |
| |
| .intersection { |
| width: 120px; |
| height: 120px; |
| background: #232933; |
| position: absolute; |
| z-index: 2; |
| } |
| |
| |
| .light { |
| width: 12px; |
| height: 12px; |
| border-radius: 50%; |
| position: absolute; |
| z-index: 5; |
| background: #30363d; |
| box-shadow: 0 0 0 2px #0d1117; |
| transition: all 0.3s ease; |
| } |
| |
| .light.green { |
| background: var(--green-light); |
| box-shadow: 0 0 15px var(--green-light), 0 0 0 2px #0d1117; |
| } |
| |
| .light.red { |
| background: var(--red-light); |
| box-shadow: 0 0 15px var(--red-light), 0 0 0 2px #0d1117; |
| } |
| |
| .light-n { top: -20px; left: 20px; } |
| .light-s { bottom: -20px; right: 20px; } |
| .light-e { right: -20px; top: 20px; } |
| .light-w { left: -20px; bottom: 20px; } |
| |
| |
| .queue-container { |
| position: absolute; |
| display: flex; |
| gap: 4px; |
| z-index: 3; |
| } |
| |
| .queue-n { top: 10px; right: 50%; margin-right: 5px; flex-direction: column-reverse; height: calc(50% - 70px); align-items: center; } |
| .queue-s { bottom: 10px; left: 50%; margin-left: 5px; flex-direction: column; height: calc(50% - 70px); align-items: center; } |
| .queue-e { right: 10px; bottom: 50%; margin-bottom: 5px; flex-direction: row-reverse; width: calc(50% - 70px); align-items: center; justify-content: flex-start; } |
| .queue-w { left: 10px; top: 50%; margin-top: 5px; flex-direction: row; width: calc(50% - 70px); align-items: center; justify-content: flex-start; } |
| |
| .car { |
| width: 14px; |
| height: 14px; |
| background: #8b949e; |
| border-radius: 3px; |
| transition: all 0.2s; |
| } |
| |
| .queue-n .car, .queue-s .car { width: 14px; height: 18px; } |
| .queue-e .car, .queue-w .car { width: 18px; height: 14px; } |
| |
| .car.emergency { |
| background: var(--ev-color); |
| box-shadow: 0 0 10px var(--ev-color); |
| animation: pulse 1s infinite alternate; |
| } |
| |
| @keyframes pulse { |
| 0% { box-shadow: 0 0 5px var(--ev-color); } |
| 100% { box-shadow: 0 0 20px var(--ev-color); background: #ff9999; } |
| } |
| |
| |
| #toast-container { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| display: flex; |
| flex-direction: column; |
| gap: 10px; |
| z-index: 100; |
| } |
| |
| .toast { |
| background: var(--panel-bg); |
| border: 1px solid var(--panel-border); |
| padding: 12px 20px; |
| border-radius: 8px; |
| backdrop-filter: blur(10px); |
| opacity: 0; |
| transform: translateY(20px); |
| animation: slideIn 0.3s forwards; |
| font-size: 0.9rem; |
| } |
| |
| @keyframes slideIn { |
| to { opacity: 1; transform: translateY(0); } |
| } |
| |
| .toggle-container { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 20px; |
| background: rgba(0,0,0,0.2); |
| padding: 12px; |
| border-radius: 8px; |
| } |
| |
| |
| .q-num { |
| position: absolute; |
| font-family: 'JetBrains Mono', monospace; |
| font-size: 14px; |
| font-weight: bold; |
| color: white; |
| background: rgba(0,0,0,0.6); |
| padding: 2px 6px; |
| border-radius: 4px; |
| z-index: 10; |
| } |
| .qn-n { top: 20px; right: 20px; } |
| .qn-s { bottom: 20px; left: 20px; } |
| .qn-e { bottom: 20px; right: 20px; } |
| .qn-w { top: 20px; left: 20px; } |
| |
| </style> |
| </head> |
| <body> |
|
|
| <div class="header"> |
| <h1>Traffic Signal Optimization</h1> |
| <div class="badge">OpenEnv Elite Submission</div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <h2 style="font-size: 1.1rem; margin-top: 0; border-bottom: 1px solid var(--panel-border); padding-bottom: 10px;">Simulation State</h2> |
| |
| <div class="metric-group" style="margin-top: 15px;"> |
| <div class="metric-label">Step Count</div> |
| <div class="metric-value" id="val-step">0</div> |
| </div> |
|
|
| <div class="metric-group"> |
| <div class="metric-label">Signal Phase</div> |
| <div class="metric-value" id="val-phase" style="color: #58a6ff;">NS GREEN</div> |
| </div> |
|
|
| <div style="flex: 1;"></div> |
| |
| <h3 style="font-size: 0.9rem; color: var(--text-muted); margin-bottom: 10px;">Waiting Time Pressure</h3> |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;"> |
| <div> |
| <div style="font-size: 0.7rem; color: var(--text-muted);">NORTH</div> |
| <div id="wait-n" style="font-family: monospace; font-size: 1.2rem;">0.0</div> |
| </div> |
| <div> |
| <div style="font-size: 0.7rem; color: var(--text-muted);">SOUTH</div> |
| <div id="wait-s" style="font-family: monospace; font-size: 1.2rem;">0.0</div> |
| </div> |
| <div> |
| <div style="font-size: 0.7rem; color: var(--text-muted);">EAST</div> |
| <div id="wait-e" style="font-family: monospace; font-size: 1.2rem;">0.0</div> |
| </div> |
| <div> |
| <div style="font-size: 0.7rem; color: var(--text-muted);">WEST</div> |
| <div id="wait-w" style="font-family: monospace; font-size: 1.2rem;">0.0</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="visualizer"> |
| <div class="road road-v"></div> |
| <div class="road road-h"></div> |
| <div class="intersection"> |
| <div class="light light-n" id="light-n"></div> |
| <div class="light light-s" id="light-s"></div> |
| <div class="light light-e" id="light-e"></div> |
| <div class="light light-w" id="light-w"></div> |
| </div> |
|
|
| <div class="q-num qn-n" id="qn-n">N: 0</div> |
| <div class="q-num qn-s" id="qn-s">S: 0</div> |
| <div class="q-num qn-e" id="qn-e">E: 0</div> |
| <div class="q-num qn-w" id="qn-w">W: 0</div> |
|
|
| <div class="queue-container queue-n" id="q-n"></div> |
| <div class="queue-container queue-s" id="q-s"></div> |
| <div class="queue-container queue-e" id="q-e"></div> |
| <div class="queue-container queue-w" id="q-w"></div> |
| </div> |
|
|
| |
| <div class="panel"> |
| <h2 style="font-size: 1.1rem; margin-top: 0; border-bottom: 1px solid var(--panel-border); padding-bottom: 10px;">Metrics</h2> |
| |
| <div class="metric-group" style="margin-top: 15px;"> |
| <div class="metric-label">Total Cleared</div> |
| <div class="metric-value good" id="val-cleared">0</div> |
| </div> |
|
|
| <div class="metric-group"> |
| <div class="metric-label">Fairness Score</div> |
| <div class="metric-value" id="val-fairness">1.00</div> |
| </div> |
|
|
| <div class="metric-group"> |
| <div class="metric-label">Congestion Base</div> |
| <div class="metric-value warn" id="val-congestion">0.00</div> |
| </div> |
|
|
| <div class="controls"> |
| <div class="toggle-container"> |
| <span style="font-weight: 600;">Agent Auto-Mode</span> |
| <label style="position: relative; display: inline-block; width: 40px; height: 20px;"> |
| <input type="checkbox" id="auto-play" style="opacity: 0; width: 0; height: 0;"> |
| <span style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(255,255,255,0.1); transition: .4s; border-radius: 20px; border: 1px solid var(--panel-border);" id="toggle-slider"></span> |
| </label> |
| </div> |
| |
| <button onclick="doStep(0)">Keep Phase (0)</button> |
| <button class="primary" onclick="doStep(1)">Switch Phase (1)</button> |
| <button class="danger" onclick="doReset()" style="margin-top: 10px;">Reset Env</button> |
| </div> |
| </div> |
|
|
| <div id="toast-container"></div> |
|
|
| <script> |
| let autoPlayInterval = null; |
| |
| document.getElementById('auto-play').addEventListener('change', function(e) { |
| const slider = document.getElementById('toggle-slider'); |
| if (e.target.checked) { |
| slider.style.backgroundColor = 'var(--accent-color)'; |
| autoPlayInterval = setInterval(() => { |
| doAutoStep(); |
| }, 300); |
| showToast('Agent Auto-Mode Enabled'); |
| } else { |
| slider.style.backgroundColor = 'rgba(255,255,255,0.1)'; |
| if (autoPlayInterval) { |
| clearInterval(autoPlayInterval); |
| autoPlayInterval = null; |
| } |
| showToast('Manual Control Restored'); |
| } |
| }); |
| |
| function showToast(msg) { |
| const container = document.getElementById('toast-container'); |
| const toast = document.createElement('div'); |
| toast.className = 'toast'; |
| toast.innerText = msg; |
| container.appendChild(toast); |
| setTimeout(() => { |
| toast.style.opacity = '0'; |
| setTimeout(() => toast.remove(), 300); |
| }, 2000); |
| } |
| |
| function updateUI(data) { |
| const state = data.state; |
| const info = data.info || {}; |
| |
| |
| document.getElementById('val-step').innerText = state.step_count; |
| |
| const pText = state.phase === 0 ? "NS GREEN" : "EW GREEN"; |
| const pColor = state.phase === 0 ? "var(--green-light)" : "var(--accent-color)"; |
| const pEl = document.getElementById('val-phase'); |
| pEl.innerText = pText; |
| pEl.style.color = pColor; |
| |
| |
| if (state.phase === 0) { |
| document.getElementById('light-n').className = 'light light-n green'; |
| document.getElementById('light-s').className = 'light light-s green'; |
| document.getElementById('light-e').className = 'light light-e red'; |
| document.getElementById('light-w').className = 'light light-w red'; |
| } else { |
| document.getElementById('light-n').className = 'light light-n red'; |
| document.getElementById('light-s').className = 'light light-s red'; |
| document.getElementById('light-e').className = 'light light-e green'; |
| document.getElementById('light-w').className = 'light light-w green'; |
| } |
| |
| |
| document.getElementById('wait-n').innerText = (state.waiting_times.north || 0).toFixed(1); |
| document.getElementById('wait-s').innerText = (state.waiting_times.south || 0).toFixed(1); |
| document.getElementById('wait-e').innerText = (state.waiting_times.east || 0).toFixed(1); |
| document.getElementById('wait-w').innerText = (state.waiting_times.west || 0).toFixed(1); |
| |
| |
| document.getElementById('qn-n').innerText = `N: ${state.north_cars}`; |
| document.getElementById('qn-s').innerText = `S: ${state.south_cars}`; |
| document.getElementById('qn-e').innerText = `E: ${state.east_cars}`; |
| document.getElementById('qn-w').innerText = `W: ${state.west_cars}`; |
| |
| |
| const drawQueue = (id, count, hasEV) => { |
| const q = document.getElementById(id); |
| q.innerHTML = ''; |
| const displayCount = Math.min(count, 10); |
| for(let i=0; i<displayCount; i++) { |
| const car = document.createElement('div'); |
| car.className = 'car'; |
| |
| if (i === 0 && hasEV) car.classList.add('emergency'); |
| q.appendChild(car); |
| } |
| }; |
| |
| const ev = state.emergency_flags; |
| drawQueue('q-n', state.north_cars, ev.north); |
| drawQueue('q-s', state.south_cars, ev.south); |
| drawQueue('q-e', state.east_cars, ev.east); |
| drawQueue('q-w', state.west_cars, ev.west); |
| |
| |
| if (info.total_cleared !== undefined) { |
| document.getElementById('val-cleared').innerText = info.total_cleared; |
| document.getElementById('val-fairness').innerText = (info.fairness_score || 0).toFixed(2); |
| document.getElementById('val-congestion').innerText = (info.congestion_score || 0).toFixed(2); |
| } |
| |
| if (data.done) { |
| showToast(`Episode Finished! Score: ${info.total_cleared}`); |
| if (document.getElementById('auto-play').checked) { |
| setTimeout(doReset, 1000); |
| } |
| } |
| } |
| |
| async function doReset() { |
| try { |
| const res = await fetch('/reset', { method: 'POST' }); |
| const data = await res.json(); |
| updateUI(data); |
| showToast("Environment Reset"); |
| } catch(e) { showToast("Error connecting to API"); } |
| } |
| |
| async function doStep(action) { |
| try { |
| const res = await fetch('/step', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ action: action }) |
| }); |
| const data = await res.json(); |
| updateUI(data); |
| } catch(e) { } |
| } |
| |
| async function doAutoStep() { |
| try { |
| const res = await fetch('/auto_step', { method: 'POST' }); |
| const data = await res.json(); |
| updateUI(data); |
| if (data.action_taken === 1) { |
| showToast("Agent triggered phase switch"); |
| } |
| } catch(e) { |
| document.getElementById('auto-play').click(); |
| showToast("Agent step failed"); |
| } |
| } |
| |
| |
| doReset(); |
| </script> |
| </body> |
| </html> |
|
|