Spaces:
Sleeping
Sleeping
| <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); | |
| } | |
| /* Metrics */ | |
| .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 */ | |
| .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 */ | |
| .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; | |
| } | |
| /* Traffic Lights */ | |
| .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; } | |
| /* Queues */ | |
| .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; } | |
| } | |
| /* Toasts */ | |
| #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; | |
| } | |
| /* Queue Numbers */ | |
| .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> | |
| <!-- Left Panel: State --> | |
| <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> | |
| <!-- Center: Visualizer --> | |
| <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> | |
| <!-- Right Panel: Metrics & Controls --> | |
| <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 || {}; | |
| // Update State Top | |
| 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; | |
| // Lights | |
| 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'; | |
| } | |
| // Waiting | |
| 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); | |
| // Queues numbers | |
| 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}`; | |
| // Draw 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'; | |
| // Make the first car emergency if flag is true | |
| 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); | |
| // Audio Visuals (Metrics) | |
| 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(); // turn off | |
| showToast("Agent step failed"); | |
| } | |
| } | |
| // Initial Load | |
| doReset(); | |
| </script> | |
| </body> | |
| </html> | |