Bhaskar
updated the index file
1476327
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenEnv Mission Control</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=Roboto+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--panel-bg: rgba(15, 23, 42, 0.75);
--panel-border: rgba(56, 189, 248, 0.2);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent-blue: #38bdf8;
--accent-green: #10b981;
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 20px;
background-color: var(--bg-color);
background-image:
radial-gradient(circle at 15% 50%, rgba(56, 189, 248, 0.05), transparent 25%),
radial-gradient(circle at 85% 30%, rgba(16, 185, 129, 0.05), transparent 25%);
color: var(--text-main);
font-family: 'Inter', sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
header {
width: 100%;
max-width: 1200px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--panel-border);
}
h1 {
margin: 0;
font-size: 24px;
font-weight: 800;
letter-spacing: 1px;
text-transform: uppercase;
display: flex;
align-items: center;
gap: 12px;
}
h1 span { color: var(--accent-blue); }
.status-badge {
display: flex;
align-items: center;
gap: 8px;
background: rgba(16, 185, 129, 0.1);
color: var(--accent-green);
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
border: 1px solid rgba(16, 185, 129, 0.3);
transition: all 0.3s ease;
}
.status-badge.searching {
background: rgba(245, 158, 11, 0.1);
color: var(--accent-yellow);
border-color: rgba(245, 158, 11, 0.3);
}
.pulse {
width: 8px;
height: 8px;
background-color: currentColor;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 currentColor; }
70% { box-shadow: 0 0 0 6px rgba(0,0,0,0); }
100% { box-shadow: 0 0 0 0 rgba(0,0,0,0); }
}
.main-container {
display: flex;
gap: 32px;
width: 100%;
max-width: 1200px;
flex-wrap: wrap;
justify-content: center;
}
.sidebar {
flex: 1;
min-width: 320px;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 24px;
}
.panel {
background: var(--panel-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--panel-border);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.panel-title {
font-size: 14px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-top: 0;
margin-bottom: 16px;
font-weight: 600;
}
.telemetry-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.telemetry-card {
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.t-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
margin-bottom: 4px;
}
.t-value {
font-family: 'Roboto Mono', monospace;
font-size: 18px;
font-weight: 700;
color: var(--text-main);
}
.t-value.green { color: var(--accent-green); }
.t-value.blue { color: var(--accent-blue); }
.t-value.red { color: var(--accent-red); }
.control-group {
display: flex;
flex-direction: column;
gap: 12px;
}
select, button {
font-family: 'Inter', sans-serif;
font-size: 14px;
padding: 12px 16px;
border-radius: 8px;
outline: none;
transition: all 0.2s;
}
select {
background: rgba(0, 0, 0, 0.3);
color: var(--text-main);
border: 1px solid var(--panel-border);
appearance: none;
cursor: pointer;
}
select:focus { border-color: var(--accent-blue); }
button {
background: linear-gradient(135deg, var(--accent-blue) 0%, #2563eb 100%);
color: #fff;
border: none;
font-weight: 600;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.3);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.5);
}
button:active { transform: translateY(0); }
.keyboard-hint {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
font-size: 12px;
color: var(--text-muted);
}
.keys {
display: flex;
gap: 4px;
}
.key {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
padding: 2px 6px;
font-family: 'Roboto Mono', monospace;
color: #fff;
}
.radar-container {
flex: 2;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
canvas {
background: radial-gradient(circle at center, #020617 0%, #0b0f19 100%);
border-radius: 50%;
box-shadow: 0 0 40px rgba(56, 189, 248, 0.15),
inset 0 0 60px rgba(56, 189, 248, 0.1);
border: 2px solid var(--panel-border);
max-width: 100%;
height: auto;
}
.radar-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
width: 600px;
height: 600px;
max-width: 100%;
border-radius: 50%;
background: conic-gradient(
from 0deg,
transparent 70%,
rgba(56, 189, 248, 0.1) 80%,
rgba(56, 189, 248, 0.4) 100%
);
animation: scan 4s linear infinite;
}
@keyframes scan {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
.ep-id-display {
font-family: 'Roboto Mono', monospace;
font-size: 11px;
color: var(--text-muted);
margin-top: 8px;
word-break: break-all;
}
/* Mission Over Overlay */
#missionOverlay {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 600px;
height: 600px;
max-width: 100%;
background: rgba(11, 15, 25, 0.85);
backdrop-filter: blur(4px);
border-radius: 50%;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
}
#missionOverlay.active { display: flex; }
#overlayTitle { font-size: 32px; font-weight: 800; margin: 0 0 10px 0; text-transform: uppercase; }
#overlayReason { font-size: 16px; color: var(--text-muted); text-align: center; max-width: 70%; white-space: pre-line;}
</style>
</head>
<body>
<header>
<h1><span>🪐</span> OpenEnv Mission Control</h1>
<div id="statusBadge" class="status-badge searching">
<div class="pulse"></div>
<span id="statusText">Waiting for active episode...</span>
</div>
</header>
<div class="main-container">
<!-- Sidebar -->
<div class="sidebar">
<!-- Interactive Demo Panel -->
<div class="panel">
<h2 class="panel-title">Interactive Judge Demo</h2>
<div class="control-group">
<select id="taskSelect">
<option value="easy">Easy — Flat Plains Transit</option>
<option value="medium">Medium — Crater Avoidance</option>
<option value="hard">Hard — Battery Sprint</option>
</select>
<button id="startBtn" onclick="startManualMission()">Launch Mission</button>
</div>
<div class="keyboard-hint">
<span>Manual Override Controls:</span>
<div class="keys">
<span class="key"></span>
<span class="key"></span>
<span class="key"></span>
<span class="key"></span>
</div>
</div>
<div class="ep-id-display" id="epIdDisplay">Episode: NONE</div>
</div>
<!-- Telemetry Panel -->
<div class="panel">
<h2 class="panel-title">Live Telemetry</h2>
<div class="telemetry-grid">
<div class="telemetry-card">
<div class="t-label">Battery Level</div>
<div class="t-value green" id="t-battery">100.0%</div>
</div>
<div class="telemetry-card">
<div class="t-label">Target Dist</div>
<div class="t-value blue" id="t-dist">-- m</div>
</div>
<div class="telemetry-card">
<div class="t-label">Speed</div>
<div class="t-value" id="t-speed">0.0 m/s</div>
</div>
<div class="telemetry-card">
<div class="t-label">Nearest Obs</div>
<div class="t-value red" id="t-obs">-- m</div>
</div>
<div class="telemetry-card">
<div class="t-label">Steps</div>
<div class="t-value" id="t-steps">0</div>
</div>
<div class="telemetry-card">
<div class="t-label">Score</div>
<div class="t-value" id="t-score">0.000</div>
</div>
</div>
</div>
</div>
<!-- Radar Display -->
<div class="radar-container">
<canvas id="radar" width="600" height="600"></canvas>
<div class="radar-overlay"></div>
<div id="missionOverlay">
<h2 id="overlayTitle">Mission Over</h2>
<p id="overlayReason">Waypoint reached.</p>
</div>
</div>
</div>
<script>
// DOM Elements
const canvas = document.getElementById('radar');
const ctx = canvas.getContext('2d');
const statusBadge = document.getElementById('statusBadge');
const statusText = document.getElementById('statusText');
const epIdDisplay = document.getElementById('epIdDisplay');
const overlay = document.getElementById('missionOverlay');
const overlayTitle = document.getElementById('overlayTitle');
const overlayReason = document.getElementById('overlayReason');
// State
let currentEpisodeId = null;
let isManualMode = false;
let latestObs = null;
let autoSyncInterval = null;
let manualLoopInterval = null;
let pollingInterval = null;
const keys = { ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false };
// Initialize UI
async function init() {
// Populate task dropdown
try {
const res = await fetch('/tasks');
if(res.ok) {
const tasks = await res.json();
const select = document.getElementById('taskSelect');
select.innerHTML = '';
tasks.forEach(t => {
const opt = document.createElement('option');
opt.value = t.id;
opt.innerText = `${t.display_name} (Diff: ${t.difficulty})`;
select.appendChild(opt);
});
}
} catch(e) { console.error("Could not fetch tasks"); }
// Start Auto-Sync Polling
startAutoSync();
// Render Loop
requestAnimationFrame(renderLoop);
}
// ==========================================
// Mode 1: Auto-Sync (Live Training Viewer)
// ==========================================
function startAutoSync() {
if(autoSyncInterval) clearInterval(autoSyncInterval);
autoSyncInterval = setInterval(async () => {
if(isManualMode) return;
try {
const res = await fetch('/latest_episode');
if(res.ok) {
const data = await res.json();
if(data.episode_id && data.episode_id !== currentEpisodeId) {
currentEpisodeId = data.episode_id;
updateStatus(`Synced: ${currentEpisodeId.substring(0,8)}...`, true);
epIdDisplay.innerText = `Episode: ${currentEpisodeId}`;
startPollingState();
}
}
} catch(e) {}
}, 2000);
}
function startPollingState() {
if(pollingInterval) clearInterval(pollingInterval);
pollingInterval = setInterval(async () => {
if(isManualMode || !currentEpisodeId) return;
try {
const res = await fetch(`/state?episode_id=${currentEpisodeId}`);
if(res.ok) {
latestObs = await res.json();
updateTelemetry(latestObs, 0); // External score tracking needed for train run
}
} catch(e) {}
}, 100);
}
// ==========================================
// Mode 2: Interactive Judge Demo
// ==========================================
async function startManualMission() {
// Reset state
isManualMode = true;
if(pollingInterval) clearInterval(pollingInterval);
if(manualLoopInterval) clearInterval(manualLoopInterval);
overlay.classList.remove('active');
const taskId = document.getElementById('taskSelect').value;
updateStatus(`Initializing Manual Override...`, false);
try {
const res = await fetch('/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId })
});
if(res.ok) {
const data = await res.json();
currentEpisodeId = data.episode_id;
latestObs = data.obs;
updateStatus(`Manual Control Active`, true);
epIdDisplay.innerText = `Episode: ${currentEpisodeId}`;
updateTelemetry(latestObs, 0);
// Start manual step loop (150ms)
manualLoopInterval = setInterval(manualStep, 150);
}
} catch(e) {
updateStatus("Connection Failed", false);
}
}
async function manualStep() {
if(!isManualMode || !currentEpisodeId) return;
// Map keys to action
let thrust = 0.0;
let brake = 0;
let steering = 0.0;
if(keys.ArrowUp) thrust = 1.0;
if(keys.ArrowDown) brake = 1;
if(keys.ArrowLeft) steering = -1.0;
if(keys.ArrowRight) steering = 1.0;
const action = {
thrust: thrust,
steering: steering,
brake: brake,
vertical_thruster: 0.0
};
try {
const res = await fetch(`/step?episode_id=${currentEpisodeId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(action)
});
if(res.ok) {
const data = await res.json();
latestObs = data.obs;
updateTelemetry(latestObs, data.info.total_reward || 0);
if(data.done || data.truncated) {
endManualMission(data.info);
}
}
} catch(e) {
console.error("Step failed", e);
}
}
function endManualMission(info) {
clearInterval(manualLoopInterval);
isManualMode = false;
updateStatus("Mission Over", false);
overlay.classList.add('active');
let color = '#f8fafc';
if(info.termination_reason === 'waypoint_reached') {
overlayTitle.innerText = "MISSION SUCCESS";
overlayTitle.style.color = 'var(--accent-green)';
color = 'var(--accent-green)';
} else if (info.termination_reason === 'battery_dead') {
overlayTitle.innerText = "BATTERY DEPLETED";
overlayTitle.style.color = 'var(--accent-red)';
color = 'var(--accent-red)';
} else {
overlayTitle.innerText = "MISSION TERMINATED";
overlayTitle.style.color = 'var(--accent-yellow)';
color = 'var(--accent-yellow)';
}
overlayReason.innerText = `Reason: ${info.termination_reason}\nCollisions: ${info.collision_count}\nScore: ${info.total_reward.toFixed(3)}`;
// Resume looking for new episodes
startAutoSync();
}
// ==========================================
// UI & Render Logic
// ==========================================
function updateStatus(text, isGood) {
statusText.innerText = text;
if(isGood) {
statusBadge.classList.remove('searching');
} else {
statusBadge.classList.add('searching');
}
}
function updateTelemetry(obs, score) {
if(!obs) return;
const batEl = document.getElementById('t-battery');
const batPct = (obs.battery_level * 100).toFixed(1);
batEl.innerText = batPct + '%';
if(obs.battery_level < 0.2) batEl.className = 't-value red';
else if(obs.battery_level < 0.5) batEl.className = 't-value yellow';
else batEl.className = 't-value green';
document.getElementById('t-dist').innerText = obs.target_distance.toFixed(1) + ' m';
const speed = Math.hypot(obs.rover_velocity.x, obs.rover_velocity.y).toFixed(2);
document.getElementById('t-speed').innerText = speed + ' m/s';
const obsEl = document.getElementById('t-obs');
obsEl.innerText = (obs.nearest_obstacle_distance === 50.0 ? '--' : obs.nearest_obstacle_distance.toFixed(1)) + ' m';
if(obs.nearest_obstacle_distance < 5.0) obsEl.className = 't-value red';
else obsEl.className = 't-value';
document.getElementById('t-steps').innerText = `${Math.floor(obs.steps_taken)}`;
document.getElementById('t-score').innerText = score.toFixed(3);
}
function renderLoop() {
drawRadar();
requestAnimationFrame(renderLoop);
}
function drawRadar() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
const scale = 0.8; // pixels per meter
// Draw grid rings (100m, 200m, 300m)
ctx.strokeStyle = 'rgba(56, 189, 248, 0.15)';
ctx.lineWidth = 1;
for(let i=1; i<=3; i++) {
ctx.beginPath();
ctx.arc(cx, cy, i * 100 * scale, 0, Math.PI*2);
ctx.stroke();
}
// Crosshairs
ctx.beginPath(); ctx.moveTo(cx, 0); ctx.lineTo(cx, canvas.height); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, cy); ctx.lineTo(canvas.width, cy); ctx.stroke();
if(!latestObs) return;
// Target Waypoint
const tx = cx + latestObs.target_position.x * scale;
const ty = cy - latestObs.target_position.y * scale;
ctx.fillStyle = '#10b981';
ctx.shadowBlur = 15; ctx.shadowColor = '#10b981';
ctx.beginPath(); ctx.arc(tx, ty, 6, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
// Rover
const rx = cx + latestObs.rover_position.x * scale;
const ry = cy - latestObs.rover_position.y * scale;
// Obstacles
ctx.fillStyle = '#ef4444';
ctx.shadowBlur = 10; ctx.shadowColor = '#ef4444';
latestObs.obstacle_map.forEach(obsData => {
if(obsData.dist_norm < 1.0) {
const ox = rx + (obsData.dx_norm * 50) * scale;
const oy = ry - (obsData.dy_norm * 50) * scale;
ctx.beginPath(); ctx.arc(ox, oy, 4, 0, Math.PI*2); ctx.fill();
}
});
ctx.shadowBlur = 0;
// Rover Heading Indicator (Triangle)
ctx.save();
ctx.translate(rx, ry);
ctx.rotate(-latestObs.rover_heading);
ctx.fillStyle = '#38bdf8';
ctx.shadowBlur = 15; ctx.shadowColor = '#38bdf8';
ctx.beginPath();
ctx.moveTo(14, 0);
ctx.lineTo(-10, 10);
ctx.lineTo(-6, 0);
ctx.lineTo(-10, -10);
ctx.fill();
ctx.restore();
// Draw sight cone
ctx.save();
ctx.translate(rx, ry);
ctx.rotate(-latestObs.rover_heading);
ctx.fillStyle = 'rgba(56, 189, 248, 0.05)';
ctx.beginPath();
ctx.moveTo(0,0);
ctx.arc(0, 0, 50 * scale, -Math.PI/4, Math.PI/4);
ctx.fill();
ctx.restore();
}
// Keyboard bindings
window.addEventListener('keydown', (e) => {
if(keys.hasOwnProperty(e.key)) {
keys[e.key] = true;
if(isManualMode) {
if(['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
e.preventDefault(); // stop scrolling
}
}
}
});
window.addEventListener('keyup', (e) => {
if(keys.hasOwnProperty(e.key)) {
keys[e.key] = false;
}
});
// Start
init();
</script>
</body>
</html>