77ethers's picture
Release GridOps demo Space
7d7b92e verified
Raw
History Blame Contribute Delete
54.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GridOps — Microgrid Operator</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
/* ── Reset & Base ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0e17;
--panel: #111827;
--border: #1e293b;
--cyan: #06b6d4;
--cyan-dim: #0891b2;
--red: #ef4444;
--yellow: #eab308;
--green: #22c55e;
--blue: #3b82f6;
--orange: #f97316;
--text: #e2e8f0;
--text-dim: #94a3b8;
--grid-bg: rgba(6,182,212,0.03);
}
body {
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
background-image:
linear-gradient(rgba(6,182,212,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(6,182,212,0.04) 1px, transparent 1px);
background-size: 40px 40px;
}
/* ── Header ───────────────────────────────────────────────── */
header {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 24px;
border-bottom: 1px solid var(--border);
background: rgba(17,24,39,0.9);
backdrop-filter: blur(8px);
}
.logo { display: flex; align-items: center; gap: 10px; }
.logo svg { width: 28px; height: 28px; }
.logo h1 { font-size: 18px; font-weight: 600; letter-spacing: 1px; }
.logo h1 span { color: var(--cyan); }
.header-info { display: flex; gap: 20px; font-size: 12px; color: var(--text-dim); }
.header-info a {
color: var(--cyan);
font-weight: 700;
text-decoration: none;
}
.header-info a:hover { color: var(--text); }
.header-info .tag {
padding: 3px 10px; border-radius: 4px;
border: 1px solid var(--border);
background: rgba(6,182,212,0.08);
}
.tag.easy { border-color: var(--green); color: var(--green); }
.tag.medium { border-color: var(--yellow); color: var(--yellow); }
.tag.hard { border-color: var(--red); color: var(--red); }
/* ── Layout ───────────────────────────────────────────────── */
.app { display: grid; grid-template-columns: 280px 1fr 260px; gap: 0; height: calc(100vh - 52px); }
/* ── Left Panel (Controls) ────────────────────────────────── */
.panel-left {
padding: 16px;
border-right: 1px solid var(--border);
background: var(--panel);
display: flex; flex-direction: column; gap: 14px;
overflow-y: auto;
}
.section-title {
font-size: 10px; text-transform: uppercase; letter-spacing: 2px;
color: var(--cyan); margin-bottom: 4px;
}
.control-group { display: flex; flex-direction: column; gap: 6px; }
.control-group label {
font-size: 11px; color: var(--text-dim);
display: flex; justify-content: space-between;
}
.control-group label .val { color: var(--cyan); font-weight: 600; }
input[type="range"] {
-webkit-appearance: none; width: 100%; height: 6px;
background: var(--border); border-radius: 3px; outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 16px; height: 16px;
border-radius: 50%; background: var(--cyan); cursor: pointer;
box-shadow: 0 0 8px rgba(6,182,212,0.5);
}
.btn-step {
width: 100%; padding: 12px; margin-top: 8px;
background: linear-gradient(135deg, var(--cyan-dim), var(--cyan));
color: #000; border: none; border-radius: 6px;
font-family: inherit; font-size: 13px; font-weight: 700;
cursor: pointer; letter-spacing: 1px;
transition: all 0.15s;
}
.btn-step:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(6,182,212,0.4); }
.btn-step:active { transform: translateY(0); }
.btn-step:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
.btn-reset {
width: 100%; padding: 8px;
background: transparent; color: var(--text-dim);
border: 1px solid var(--border); border-radius: 6px;
font-family: inherit; font-size: 11px; cursor: pointer;
}
.btn-reset:hover { border-color: var(--cyan); color: var(--cyan); }
select {
width: 100%; padding: 8px; background: var(--bg); color: var(--text);
border: 1px solid var(--border); border-radius: 4px;
font-family: inherit; font-size: 12px;
}
.info-row {
display: flex; justify-content: space-between;
font-size: 11px; padding: 4px 0;
border-bottom: 1px solid rgba(30,41,59,0.5);
}
.info-row .label { color: var(--text-dim); }
.fc-cell {
padding: 2px 4px; border-radius: 3px; font-weight: 600; font-size: 10px;
background: rgba(30,41,59,0.4);
}
.fc-cell.high { color: var(--red); background: rgba(239,68,68,0.1); }
.fc-cell.med { color: var(--yellow); background: rgba(234,179,8,0.08); }
.fc-cell.low { color: var(--green); background: rgba(34,197,94,0.08); }
.fc-cell.bright { color: var(--yellow); background: rgba(234,179,8,0.1); }
/* ── Center Panel (Visualization) ─────────────────────────── */
.panel-center {
display: flex; flex-direction: column; gap: 0;
overflow: hidden;
}
.chart-container {
flex: 6; padding: 12px; min-height: 0;
position: relative;
}
.chart-container canvas { width: 100% !important; height: 100% !important; }
/* Energy flow diagram */
.flow-section {
padding: 10px 20px;
border-top: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
overflow-y: auto;
}
.flow-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-template-rows: auto auto auto;
gap: 8px; width: 100%; max-width: 600px;
text-align: center; font-size: 11px;
}
.flow-node {
padding: 10px 6px; border-radius: 6px;
border: 1px solid var(--border);
background: rgba(17,24,39,0.8);
position: relative;
}
.flow-node .node-icon { font-size: 20px; display: block; margin-bottom: 2px; }
.flow-node .node-val { color: var(--cyan); font-weight: 600; font-size: 13px; }
.flow-node .node-label { color: var(--text-dim); font-size: 9px; text-transform: uppercase; letter-spacing: 1px; }
.flow-node.solar, .flow-node.battery, .flow-node.grid, .flow-node.diesel { border-color: var(--border); }
.flow-node.community { border-color: var(--cyan); background: rgba(6,182,212,0.08); }
@keyframes pulse-red { 0%,100% { box-shadow: 0 0 0 rgba(239,68,68,0); } 50% { box-shadow: 0 0 16px rgba(239,68,68,0.4); } }
/* House grid */
.flow-chip {
padding: 3px 8px; border-radius: 4px; font-size: 9px;
border: 1px solid var(--border); white-space: nowrap;
}
.flow-chip b { color: var(--cyan); font-size: 10px; margin-left: 2px; }
.flow-chip.supply { background: rgba(34,197,94,0.06); }
.flow-chip.supply b { color: var(--green); }
.flow-chip.demand { background: rgba(234,179,8,0.06); }
.flow-chip.demand b { color: var(--yellow); }
.flow-chip.danger { background: rgba(239,68,68,0.1); border-color: rgba(239,68,68,0.3); }
.flow-chip.danger b { color: var(--red); }
.house-grid {
display: grid; grid-template-columns: repeat(20, 1fr); gap: 2px;
width: fit-content;
}
.house-cell {
width: 14px; height: 14px; border-radius: 2px; font-size: 5px;
display: flex; align-items: center; justify-content: center;
transition: background 0.2s, box-shadow 0.2s;
}
.house-cell.powered {
background: rgba(34,197,94,0.25);
box-shadow: inset 0 0 3px rgba(34,197,94,0.3);
}
.house-cell.dark {
background: rgba(239,68,68,0.35);
box-shadow: inset 0 0 3px rgba(239,68,68,0.4);
animation: pulse-red 1.5s infinite;
}
.flow-arrow { color: var(--text-dim); font-size: 14px; display: flex; align-items: center; justify-content: center; }
/* Narration bar */
.narration {
padding: 8px 20px; font-size: 11px;
border-top: 1px solid var(--border);
background: rgba(6,182,212,0.04);
color: var(--text-dim);
flex-shrink: 0;
}
.narration strong { color: var(--cyan); }
/* ── Right Panel (Scores) ─────────────────────────────────── */
.panel-right {
padding: 16px;
border-left: 1px solid var(--border);
background: var(--panel);
display: flex; flex-direction: column; gap: 12px;
overflow-y: auto;
}
.score-card {
padding: 12px; border-radius: 6px;
border: 1px solid var(--border);
background: rgba(10,14,23,0.6);
}
.score-card .sc-label {
font-size: 9px; text-transform: uppercase; letter-spacing: 2px;
color: var(--text-dim); margin-bottom: 4px;
}
.score-card .sc-value {
font-size: 24px; font-weight: 700;
}
.score-card .sc-bar {
height: 4px; border-radius: 2px; margin-top: 6px;
background: var(--border);
overflow: hidden;
}
.score-card .sc-bar-fill {
height: 100%; border-radius: 2px;
transition: width 0.3s;
}
.sc-value.good { color: var(--green); }
.sc-value.warn { color: var(--yellow); }
.sc-value.bad { color: var(--red); }
.sc-value.cyan { color: var(--cyan); }
.grade-box {
padding: 16px; border-radius: 8px;
border: 2px solid var(--cyan);
background: rgba(6,182,212,0.06);
text-align: center;
}
.grade-box .grade-label { font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: var(--text-dim); }
.grade-box .grade-value { font-size: 42px; font-weight: 800; color: var(--cyan); margin: 4px 0; }
.grade-box .grade-sub { font-size: 11px; color: var(--text-dim); }
.grade-box.hidden { display: none; }
/* ── Intro Modal ───────────────────────────────────────────── */
.intro-overlay {
position: fixed; inset: 0; z-index: 1000;
background: rgba(10,14,23,0.92);
backdrop-filter: blur(12px);
display: flex; align-items: center; justify-content: center;
animation: fadeIn 0.4s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.intro-overlay.hidden { display: none; }
.intro-card {
max-width: 720px; width: 90%; max-height: 90vh;
overflow-y: auto;
padding: 40px 44px;
border-radius: 16px;
border: 1px solid var(--cyan);
background: linear-gradient(160deg, rgba(17,24,39,0.98), rgba(10,14,23,0.99));
box-shadow: 0 0 60px rgba(6,182,212,0.15), 0 0 120px rgba(6,182,212,0.05);
animation: cardIn 0.5s ease;
}
@keyframes cardIn { from { transform: scale(0.95) translateY(20px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
.intro-card .intro-bolt {
display: flex; align-items: center; gap: 14px;
margin-bottom: 24px;
}
.intro-card .intro-bolt svg { width: 42px; height: 42px; filter: drop-shadow(0 0 12px rgba(6,182,212,0.6)); }
.intro-card .intro-bolt h1 { font-size: 32px; font-weight: 800; letter-spacing: 2px; }
.intro-card .intro-bolt h1 span { color: var(--cyan); }
.intro-card .tagline {
font-size: 15px; color: var(--cyan); font-weight: 600;
margin-bottom: 20px; letter-spacing: 0.5px;
}
.intro-card .story {
font-size: 14px; line-height: 1.75; color: var(--text);
margin-bottom: 20px;
}
.intro-card .story p { margin-bottom: 12px; }
.intro-card .story strong { color: var(--cyan); font-weight: 600; }
.intro-card .story .highlight {
color: var(--yellow);
}
.intro-card .story .danger {
color: var(--red);
}
.intro-card .the-catch {
background: rgba(239,68,68,0.08);
border: 1px solid rgba(239,68,68,0.25);
border-radius: 8px;
padding: 14px 18px;
margin-bottom: 20px;
font-size: 13px; line-height: 1.7;
color: var(--text);
}
.intro-card .the-catch .catch-title {
color: var(--red); font-weight: 700; font-size: 12px;
text-transform: uppercase; letter-spacing: 2px;
margin-bottom: 6px;
}
.intro-card .controls-preview {
display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px;
margin-bottom: 22px;
}
.intro-card .cp-item {
background: rgba(6,182,212,0.06);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
text-align: center;
}
.cp-item .cp-icon { font-size: 22px; display: block; margin-bottom: 4px; }
.cp-item .cp-name { font-size: 11px; font-weight: 700; color: var(--cyan); text-transform: uppercase; letter-spacing: 1px; }
.cp-item .cp-desc { font-size: 10px; color: var(--text-dim); margin-top: 3px; }
.intro-card .scoring {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
margin-bottom: 24px;
}
.intro-card .sc-item {
text-align: center; padding: 10px;
border-radius: 6px;
border: 1px solid var(--border);
}
.sc-item .sc-pct { font-size: 22px; font-weight: 800; }
.sc-item .sc-what { font-size: 10px; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1px; }
.intro-card .tasks-preview {
display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
margin-bottom: 24px;
}
.intro-card .tp-item {
padding: 10px 12px; border-radius: 6px;
border: 1px solid var(--border);
font-size: 11px;
}
.tp-item .tp-name { font-weight: 700; margin-bottom: 3px; }
.tp-item .tp-desc { color: var(--text-dim); font-size: 10px; line-height: 1.5; }
.tp-easy .tp-name { color: var(--green); }
.tp-med .tp-name { color: var(--yellow); }
.tp-hard .tp-name { color: var(--red); }
/* Slide cards */
.slide-track {
display: flex; gap: 16px; overflow-x: auto; scroll-snap-type: x mandatory;
padding: 8px 0 16px; -webkit-overflow-scrolling: touch;
scrollbar-width: thin; scrollbar-color: var(--cyan-dim) transparent;
}
.slide-track::-webkit-scrollbar { height: 5px; }
.slide-track::-webkit-scrollbar-thumb { background: var(--cyan-dim); border-radius: 3px; }
.slide-card {
flex: 0 0 100%; scroll-snap-align: center;
padding: 28px 32px; border-radius: 14px;
border: 1px solid var(--border);
background: rgba(17,24,39,0.85);
display: flex; gap: 24px; align-items: center;
min-height: 160px;
}
.slide-card .sc-visual {
flex: 0 0 140px; height: 120px;
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 48px;
position: relative; overflow: hidden;
}
.slide-card .sc-content { flex: 1; min-width: 0; }
.slide-card .sc-step {
font-size: 10px; text-transform: uppercase; letter-spacing: 2px;
color: var(--cyan); margin-bottom: 6px;
}
.slide-card .sc-title { font-size: 18px; font-weight: 700; color: var(--text); margin-bottom: 8px; line-height: 1.3; }
.slide-card .sc-body { font-size: 13px; color: var(--text-dim); line-height: 1.7; }
.slide-card .sc-body strong { color: var(--cyan); font-weight: 600; }
.slide-card .sc-body .warn { color: var(--red); font-weight: 600; }
.slide-dots {
display: flex; justify-content: center; gap: 8px; margin-bottom: 16px;
}
.slide-dots .dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--border); transition: background 0.2s;
}
.slide-dots .dot.active { background: var(--cyan); }
.intro-card .btn-start {
width: 100%; padding: 14px;
background: linear-gradient(135deg, var(--cyan-dim), var(--cyan));
color: #000; border: none; border-radius: 8px;
font-family: inherit; font-size: 14px; font-weight: 800;
cursor: pointer; letter-spacing: 2px;
text-transform: uppercase;
transition: all 0.2s;
}
.btn-start:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(6,182,212,0.4); }
.intro-card .shortcut-hint {
text-align: center; margin-top: 8px;
font-size: 10px; color: var(--text-dim);
}
/* ── Responsive ───────────────────────────────────────────── */
@media (max-width: 960px) {
.app { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto; }
.panel-left, .panel-right { flex-direction: row; flex-wrap: wrap; border: none; border-bottom: 1px solid var(--border); }
.intro-card { padding: 24px; }
.controls-preview, .scoring, .tasks-preview { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ── Intro Overlay ───────────────────────────────────────────── -->
<div class="intro-overlay" id="introOverlay">
<div class="intro-card">
<div class="intro-bolt">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="2.5">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
<h1>GRID<span>OPS</span></h1>
</div>
<div class="tagline">An OpenEnv RL environment for Indian community microgrid operation</div>
<!-- Sliding cards -->
<div class="slide-track" id="slideTrack">
<!-- Card 1: The Scenario -->
<div class="slide-card">
<div class="sc-visual" style="background:linear-gradient(135deg, rgba(6,182,212,0.15), rgba(59,130,246,0.1)); border:1px solid rgba(6,182,212,0.2);">
<div style="text-align:center;">
<div style="font-size:36px;">&#127968;</div>
<div style="font-size:10px;color:var(--cyan);margin-top:4px;font-weight:600;">100 HOMES</div>
<div style="font-size:8px;color:var(--text-dim);">3 days &middot; 72 hours</div>
</div>
</div>
<div class="sc-content">
<div class="sc-step">The Scenario</div>
<div class="sc-title">You run a community microgrid in India during summer</div>
<div class="sc-body">Every hour for 3 days, you decide how to use the <strong>battery</strong>, <strong>diesel</strong>, and <strong>demand response</strong> &mdash; while the national grid automatically covers whatever is left over.</div>
</div>
</div>
<!-- Card 2: Your Resources -->
<div class="slide-card">
<div class="sc-visual" style="background:linear-gradient(135deg, rgba(234,179,8,0.12), rgba(34,197,94,0.08)); border:1px solid rgba(234,179,8,0.2);">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:8px;">
<div style="text-align:center;"><div style="font-size:22px;">&#9728;&#65039;</div><div style="font-size:7px;color:var(--yellow);">SOLAR</div></div>
<div style="text-align:center;"><div style="font-size:22px;">&#128267;</div><div style="font-size:7px;color:var(--blue);">BATTERY</div></div>
<div style="text-align:center;"><div style="font-size:22px;">&#9981;</div><div style="font-size:7px;color:var(--orange);">DIESEL</div></div>
<div style="text-align:center;"><div style="font-size:22px;">&#9889;</div><div style="font-size:7px;color:var(--green);">GRID</div></div>
</div>
</div>
<div class="sc-content">
<div class="sc-step">Your Resources</div>
<div class="sc-title">Choose the right energy source to match demand</div>
<div class="sc-body"><strong>Solar</strong> is free but only shines during the day. <strong>Battery</strong> stores 500 kWh &mdash; you decide when to charge or use it. <strong>Diesel</strong> is expensive (Rs 25/kWh). <strong>Grid</strong> auto-fills the gap up to 200 kW.</div>
</div>
</div>
<!-- Card 3: Your Goal -->
<div class="slide-card">
<div class="sc-visual" style="background:linear-gradient(135deg, rgba(34,197,94,0.12), rgba(6,182,212,0.08)); border:1px solid rgba(34,197,94,0.2);">
<div style="text-align:center;">
<div style="font-size:28px;color:var(--green);font-weight:800;">&#8595; &#8377;</div>
<div style="display:flex;gap:4px;justify-content:center;margin-top:8px;">
<div style="background:var(--green);width:40px;height:6px;border-radius:3px;opacity:0.8;"></div>
<div style="background:var(--yellow);width:25px;height:6px;border-radius:3px;opacity:0.8;"></div>
<div style="background:var(--cyan);width:25px;height:6px;border-radius:3px;opacity:0.8;"></div>
</div>
<div style="font-size:7px;color:var(--text-dim);margin-top:4px;">50% cost &middot; 25% lights &middot; 25% green</div>
</div>
</div>
<div class="sc-content">
<div class="sc-step">Your Goal</div>
<div class="sc-title">Lose as little money as possible while keeping lights on</div>
<div class="sc-body">Switch between energy sources smartly. Buy grid power <strong>cheap at night</strong>, use free solar during the day, and discharge battery during <strong>expensive evening peaks</strong>. Blackouts cost <span class="warn">Rs 150/kWh</span> &mdash; keep them near zero.</div>
</div>
</div>
</div>
<div class="slide-dots" id="slideDots">
<div class="dot active"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<button class="btn-start" id="btnStart">Start Operating</button>
<div class="shortcut-hint">Press <strong>Space</strong> or <strong>Enter</strong> to advance each hour &middot; <strong>R</strong> to reset</div>
</div>
</div>
<!-- ── Header ──────────────────────────────────────────────────── -->
<header>
<div class="logo">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--cyan)" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
<h1>GRID<span>OPS</span></h1>
</div>
<div class="header-info">
<span id="taskTag" class="tag easy">Task 1: Normal</span>
<span><strong id="hourDisp">6:00</strong></span>
<span>Day <strong id="dayDisp">1</strong></span>
<span>Step <strong id="stepDisp">0</strong>/72</span>
<a href="/case-study">Case Study</a>
</div>
</header>
<div class="app">
<!-- ── Left Panel ──────────────────────────────────────────── -->
<div class="panel-left">
<div>
<div class="section-title">Coming Up Next</div>
<div class="info-row"><span class="label">&#128336; Time</span><span id="timeInfo" style="font-weight:700;color:var(--cyan)">06:00 · Day 1</span></div>
<div class="info-row"><span class="label">&#127968; Homes Will Need</span><span id="demandInfo"></span></div>
<div class="info-row"><span class="label">&#9728;&#65039; Solar Expected</span><span id="solarInfo"></span></div>
<div class="info-row"><span class="label">&#9889; Grid Will Cover</span><span id="gridNeedInfo"></span></div>
<div class="info-row"><span class="label">&#128176; Grid Price</span><span id="priceInfo" style="font-weight:700"></span></div>
<div class="info-row"><span class="label">&#128267; Battery Charge</span><span id="socInfo"></span></div>
<div class="info-row"><span class="label">&#9981; Diesel Fuel Left</span><span id="fuelInfo"></span></div>
</div>
<div>
<div class="section-title">After That <span style="text-transform:none;letter-spacing:0;color:var(--text-dim);font-size:9px">(±15% noisy forecast)</span></div>
<div id="forecastTable" style="font-size:10px;">
<div style="display:grid;grid-template-columns:52px repeat(4,1fr);gap:2px;margin-bottom:3px;color:var(--text-dim);font-size:8px;text-transform:uppercase;letter-spacing:1px;">
<div></div>
<div id="fcH1" style="text-align:center">+1h</div>
<div id="fcH2" style="text-align:center">+2h</div>
<div id="fcH3" style="text-align:center">+3h</div>
<div id="fcH4" style="text-align:center">+4h</div>
</div>
<div style="display:grid;grid-template-columns:52px repeat(4,1fr);gap:2px;padding:3px 0;border-bottom:1px solid rgba(30,41,59,0.5);">
<div style="color:var(--text-dim);font-size:9px;">&#127968; Need</div>
<div id="fcD1" class="fc-cell" style="text-align:center"></div>
<div id="fcD2" class="fc-cell" style="text-align:center"></div>
<div id="fcD3" class="fc-cell" style="text-align:center"></div>
<div id="fcD4" class="fc-cell" style="text-align:center"></div>
</div>
<div style="display:grid;grid-template-columns:52px repeat(4,1fr);gap:2px;padding:3px 0;border-bottom:1px solid rgba(30,41,59,0.5);">
<div style="color:var(--text-dim);font-size:9px;">&#9728;&#65039; Sun</div>
<div id="fcS1" class="fc-cell" style="text-align:center"></div>
<div id="fcS2" class="fc-cell" style="text-align:center"></div>
<div id="fcS3" class="fc-cell" style="text-align:center"></div>
<div id="fcS4" class="fc-cell" style="text-align:center"></div>
</div>
<div style="display:grid;grid-template-columns:52px repeat(4,1fr);gap:2px;padding:3px 0;">
<div style="color:var(--text-dim);font-size:9px;">&#128176; Price</div>
<div id="fcP1" class="fc-cell" style="text-align:center"></div>
<div id="fcP2" class="fc-cell" style="text-align:center"></div>
<div id="fcP3" class="fc-cell" style="text-align:center"></div>
<div id="fcP4" class="fc-cell" style="text-align:center"></div>
</div>
</div>
</div>
<div>
<div class="section-title">Scenario</div>
<select id="taskSelect">
<option value="task_1_normal">Task 1 — Normal Summer (Easy)</option>
<option value="task_2_heatwave">Task 2 — Heatwave + Clouds (Medium)</option>
<option value="task_3_crisis">Task 3 — Extreme Crisis (Hard)</option>
</select>
</div>
<div>
<div class="section-title">Your 3 Decisions</div>
<div class="control-group">
<label>&#128267; Battery <span class="val" id="batteryVal">idle</span></label>
<input type="range" id="batterySlider" min="-100" max="100" value="0" step="1">
<div style="display:flex;justify-content:space-between;font-size:9px;color:var(--text-dim)">
<span>&#8592; Store energy</span><span>Use stored &#8594;</span>
</div>
</div>
<div class="control-group">
<label>&#9981; Diesel <span class="val" id="dieselVal">off</span></label>
<input type="range" id="dieselSlider" min="0" max="100" value="0" step="1">
<div style="font-size:9px;color:var(--orange)">Rs 25/kWh + Rs 100 to start</div>
</div>
<div class="control-group">
<label>&#127968; Ask residents to cut usage <span class="val" id="shedVal">none</span></label>
<input type="range" id="shedSlider" min="0" max="100" value="0" step="1">
<div style="font-size:9px;color:var(--yellow)">100% rebounds next hour! Rs 40/kWh penalty.</div>
</div>
</div>
<button class="btn-step" id="stepBtn">ADVANCE 1 HOUR &#8594;</button>
<button class="btn-reset" id="resetBtn">Restart Episode</button>
</div>
<!-- ── Center Panel ────────────────────────────────────────── -->
<div class="panel-center">
<div class="chart-container">
<canvas id="mainChart"></canvas>
</div>
<div class="flow-section" style="flex:4; padding:10px 16px; min-height:0; flex-direction:column; align-items:stretch;">
<!-- Row 1: Supply bar + Verdict + House grid -->
<div style="display:flex; gap:12px; align-items:stretch; flex:1; min-height:0;">
<!-- LEFT: Supply → Demand breakdown -->
<div style="flex:1.2; display:flex; flex-direction:column; gap:3px; font-size:10px; min-width:0;">
<div style="font-size:9px;color:var(--cyan);font-weight:600;margin-bottom:2px;" id="flowHourLabel">Last Hour's Energy Balance</div>
<div style="font-size:8px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;">Supply IN (kW)</div>
<div style="display:flex; gap:3px; flex-wrap:wrap;">
<div class="flow-chip supply" id="chipSolar">&#9728;&#65039; Solar <b>0</b></div>
<div class="flow-chip supply" id="chipGridIn">&#9889; Grid Import <b>0</b></div>
<div class="flow-chip supply" id="chipBattOut">&#128267; Batt Out <b>0</b></div>
<div class="flow-chip supply" id="chipDiesel">&#9981; Diesel <b>0</b></div>
</div>
<div style="display:flex;align-items:center;gap:6px;margin:2px 0;">
<div style="font-size:9px;color:var(--cyan);font-weight:700;" id="flowTotalSupply">= 0 kW supply</div>
</div>
<div style="font-size:8px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:2px;">Consumption OUT (kW)</div>
<div style="display:flex; gap:3px; flex-wrap:wrap;">
<div class="flow-chip demand" id="chipDemand">&#127968; Homes <b>0</b></div>
<div class="flow-chip demand" id="chipGridOut">&#9889; Grid Export <b>0</b></div>
<div class="flow-chip demand" id="chipBattIn">&#128267; Batt Charge <b>0</b></div>
<div class="flow-chip danger" id="chipBlackout" style="display:none">&#9888;&#65039; Blackout <b>0</b></div>
</div>
<div style="display:flex;align-items:center;gap:6px;margin:2px 0;">
<div style="font-size:9px;color:var(--yellow);font-weight:700;" id="flowTotalConsume">= 0 kW consumption</div>
</div>
<!-- Net balance -->
<div style="padding:4px 8px;border-radius:4px;border:1px solid var(--border);display:flex;align-items:center;gap:8px;margin-top:auto;">
<span id="balanceVerdict" style="font-size:10px;font-weight:800;">&#9679; BALANCED</span>
<span id="balanceDetail" style="font-size:9px;color:var(--text-dim);"></span>
</div>
</div>
<!-- RIGHT: 100 Homes grid -->
<div style="flex:0.8; min-width:0; display:flex; flex-direction:column;" id="flowCommunity">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;">
<div style="font-size:8px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;">100 Homes</div>
<div style="display:flex;align-items:center;gap:4px;">
<span id="statusIcon" style="font-size:12px">&#9989;</span>
<span id="statusText" style="font-size:10px;font-weight:700;color:var(--green)">ALL POWERED</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;font-size:10px;color:var(--text-dim);">
Need <span class="node-val" id="flowDemand" style="font-size:12px;margin:0 2px;">0 kW</span>
<span id="flowBlackoutHint" style="color:var(--red);font-weight:600;font-size:9px;display:none;"></span>
</div>
<div class="house-grid" id="houseGrid" style="flex:1;"></div>
<div style="display:flex;gap:10px;margin-top:3px;font-size:7px;color:var(--text-dim);">
<span><span style="display:inline-block;width:6px;height:6px;border-radius:1px;background:rgba(34,197,94,0.35);vertical-align:middle;margin-right:2px;"></span>Powered</span>
<span><span style="display:inline-block;width:6px;height:6px;border-radius:1px;background:rgba(239,68,68,0.45);vertical-align:middle;margin-right:2px;"></span>Blackout</span>
</div>
</div>
</div>
</div>
<div class="narration" id="narration">
<strong>Ready.</strong> Select a task and click "Advance 1 Hour" to begin.
</div>
</div>
<!-- ── Right Panel ─────────────────────────────────────────── -->
<div class="panel-right">
<div class="grade-box hidden" id="gradeBox">
<div class="grade-label">Final Episode Score</div>
<div class="grade-value" id="gradeValue"></div>
<div class="grade-sub" id="gradeSub"></div>
</div>
<div style="font-size:9px;text-transform:uppercase;letter-spacing:2px;color:var(--cyan);">Performance</div>
<div class="score-card">
<div class="sc-label">&#127968; Lights On (Reliability)</div>
<div class="sc-value good" id="relValue">100%</div>
<div class="sc-bar"><div class="sc-bar-fill" id="relBar" style="width:100%;background:var(--green)"></div></div>
<div style="font-size:9px;color:var(--text-dim);margin-top:4px">Blackout = Rs 150/kWh penalty</div>
</div>
<div class="score-card">
<div class="sc-label">&#128176; Total Spend</div>
<div class="sc-value cyan" id="costValue">Rs 0</div>
<div style="font-size:9px;color:var(--text-dim);margin-top:4px">Lower is better. Grid + diesel + penalties.</div>
</div>
<div class="score-card">
<div class="sc-label">&#128267; Battery</div>
<div class="sc-value cyan" id="socValue">50%</div>
<div class="sc-bar"><div class="sc-bar-fill" id="socBar" style="width:50%;background:var(--blue)"></div></div>
<div style="font-size:9px;color:var(--text-dim);margin-top:4px">Save charge for evening peak!</div>
</div>
<div class="score-card">
<div class="sc-label">&#9888;&#65039; Blackouts</div>
<div class="sc-value good" id="blackoutValue">0 kWh</div>
<div style="font-size:9px;color:var(--text-dim);margin-top:4px">Unmet demand. Must be near zero to score well.</div>
</div>
<div class="score-card">
<div class="sc-label">&#9981; Diesel Burned</div>
<div class="sc-value" id="dieselUsed" style="color:var(--orange)">0 kWh</div>
<div style="font-size:9px;color:var(--text-dim);margin-top:4px">Hurts green score + very expensive.</div>
</div>
<div class="score-card">
<div class="sc-label">This Hour's Reward</div>
<div class="sc-value cyan" id="rewardValue"></div>
</div>
</div>
</div>
<script>
// ── Config ──────────────────────────────────────────────────────
const API = window.location.origin + '/api';
let obs = null;
let running = false;
// ── Chart setup ─────────────────────────────────────────────────
const chartData = {
demand: [], solar: [], price: [], soc: [], hours: [], blackout: [], gridImport: []
};
const ctx = document.getElementById('mainChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: chartData.hours,
datasets: [
{ label: 'Demand (kW)', data: chartData.demand, borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.1)', borderWidth: 1.5, pointRadius: 0, tension: 0.3, yAxisID: 'y' },
{ label: 'Solar (kW)', data: chartData.solar, borderColor: '#eab308', backgroundColor: 'rgba(234,179,8,0.08)', borderWidth: 1.5, pointRadius: 0, tension: 0.3, yAxisID: 'y' },
{ label: 'Grid Import (kW)', data: chartData.gridImport, borderColor: '#06b6d4', backgroundColor: 'rgba(6,182,212,0.08)', borderWidth: 2, pointRadius: 0, tension: 0.3, fill: true, yAxisID: 'y' },
{ label: 'Price (₹/kWh)', data: chartData.price, borderColor: '#22c55e', borderDash: [4,2], borderWidth: 1.5, pointRadius: 0, tension: 0.3, yAxisID: 'y1' },
{ label: 'Battery SOC', data: chartData.soc, borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.12)', borderWidth: 1.5, pointRadius: 0, tension: 0.3, fill: true, yAxisID: 'y2' },
]
},
options: {
responsive: true, maintainAspectRatio: false,
animation: { duration: 150 },
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { position: 'top', labels: { color: '#94a3b8', font: { size: 10, family: 'monospace' }, boxWidth: 12, padding: 8 } }
},
scales: {
x: { grid: { color: 'rgba(30,41,59,0.5)' }, ticks: { color: '#64748b', font: { size: 9 }, maxTicksLimit: 24 } },
y: { position: 'left', grid: { color: 'rgba(30,41,59,0.3)' }, ticks: { color: '#94a3b8', font: { size: 9 } }, title: { display: true, text: 'kW', color: '#64748b' }, min: 0 },
y1: { position: 'right', grid: { display: false }, ticks: { color: '#22c55e', font: { size: 9 } }, title: { display: true, text: 'Rs/kWh', color: '#22c55e' }, min: 0, max: 25 },
y2: { display: false, min: 0, max: 1 },
}
}
});
// ── Slider labels ───────────────────────────────────────────────
const batterySlider = document.getElementById('batterySlider');
const dieselSlider = document.getElementById('dieselSlider');
const shedSlider = document.getElementById('shedSlider');
batterySlider.oninput = () => {
const v = batterySlider.value;
const kw = Math.abs((v / 100 * 100).toFixed(0));
const label = v < 0 ? `storing ${kw} kW` : v > 0 ? `using ${kw} kW` : 'idle';
document.getElementById('batteryVal').textContent = label;
};
dieselSlider.oninput = () => {
const kw = (dieselSlider.value / 100 * 100).toFixed(0);
document.getElementById('dieselVal').textContent = kw > 0 ? kw + ' kW' : 'off';
};
shedSlider.oninput = () => {
const pct = (shedSlider.value / 100 * 20).toFixed(0);
document.getElementById('shedVal').textContent = pct > 0 ? pct + '% cut' : 'none';
};
// ── API calls ───────────────────────────────────────────────────
async function apiPost(path, body) {
const res = await fetch(API + path, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
return res.json();
}
function pushChartPoint(o) {
const clockLabel = String((Math.floor(o.hour) + 6) % 24).padStart(2,'0') + 'h';
chartData.hours.push(clockLabel);
chartData.demand.push(o.demand_kw);
chartData.solar.push(o.solar_kw);
chartData.price.push(o.grid_price);
chartData.soc.push(o.battery_soc);
chartData.blackout.push(o.blackout_this_step || 0);
chartData.gridImport.push(o.flow_grid_import || 0);
}
async function resetEnv() {
const taskId = document.getElementById('taskSelect').value;
const body = { seed: 42, task_id: taskId };
const resp = await apiPost('/reset', body);
obs = resp.observation || resp;
chartData.demand.length = 0;
chartData.solar.length = 0;
chartData.price.length = 0;
chartData.soc.length = 0;
chartData.hours.length = 0;
chartData.blackout.length = 0;
chartData.gridImport.length = 0;
// Push initial state so chart has a starting point
pushChartPoint(obs);
chart.update();
updateUI();
document.getElementById('gradeBox').classList.add('hidden');
document.getElementById('stepBtn').disabled = false;
running = true;
updateTaskTag();
}
async function stepEnv() {
if (!running) return;
const action = {
battery_dispatch: batterySlider.value / 100,
diesel_dispatch: dieselSlider.value / 100,
demand_shedding: shedSlider.value / 100,
};
const resp = await apiPost('/step', { action });
obs = resp.observation || resp;
// Update chart
pushChartPoint(obs);
chart.update();
updateUI();
if (obs.done) {
running = false;
document.getElementById('stepBtn').disabled = true;
// Fetch final state for grade
try {
const stateResp = await fetch(API + '/state');
const stateData = await stateResp.json();
if (stateData.grade) showGrade(stateData.grade);
else if (obs.metadata && obs.metadata.grade) showGrade(obs.metadata.grade);
} catch(e) {
if (obs.metadata && obs.metadata.grade) showGrade(obs.metadata.grade);
}
}
}
// ── UI update ───────────────────────────────────────────────────
function updateUI() {
if (!obs) return;
const START_HOUR = 6;
const clockHour = (Math.floor(obs.hour) + START_HOUR) % 24;
const clockDay = Math.floor((Math.floor(obs.hour) + START_HOUR) / 24) + 1;
const clockStr = String(clockHour).padStart(2,'0') + ':00';
document.getElementById('hourDisp').textContent = clockStr;
document.getElementById('dayDisp').textContent = clockDay;
document.getElementById('stepDisp').textContent = Math.floor(obs.hour);
document.getElementById('timeInfo').textContent = clockStr + ' · Day ' + clockDay + ' · Step ' + Math.floor(obs.hour) + '/72';
// Info panel
const price = (obs.grid_price || 0);
const priceEl = document.getElementById('priceInfo');
priceEl.textContent = '₹' + price.toFixed(1) + '/kWh';
priceEl.style.color = price > 10 ? 'var(--red)' : price > 6 ? 'var(--yellow)' : 'var(--green)';
const demKw = (obs.demand_kw || 0);
const demEl = document.getElementById('demandInfo');
demEl.textContent = demKw.toFixed(0) + ' kW';
demEl.style.color = demKw > 200 ? 'var(--red)' : demKw > 120 ? 'var(--yellow)' : 'var(--text)';
const solKw = (obs.solar_kw || 0);
const solEl = document.getElementById('solarInfo');
solEl.textContent = solKw.toFixed(0) + ' kW';
solEl.style.color = solKw > 100 ? 'var(--yellow)' : 'var(--text-dim)';
// Grid: demand - solar = what grid needs to cover (before your battery/diesel decisions)
const gridGap = demKw - solKw; // positive = importing, negative = exporting
const gridNeedEl = document.getElementById('gridNeedInfo');
if (gridGap > 200) {
gridNeedEl.textContent = gridGap.toFixed(0) + ' kW — OVER 200 kW LIMIT! Use battery/diesel.';
gridNeedEl.style.color = 'var(--red)';
} else if (gridGap > 150) {
gridNeedEl.textContent = gridGap.toFixed(0) + ' kW — near limit, consider battery';
gridNeedEl.style.color = 'var(--yellow)';
} else if (gridGap < 0) {
gridNeedEl.textContent = gridGap.toFixed(0) + ' kW (exporting surplus)';
gridNeedEl.style.color = 'var(--green)';
} else {
gridNeedEl.textContent = gridGap.toFixed(0) + ' kW';
gridNeedEl.style.color = 'var(--text)';
}
const socPctRaw = ((obs.battery_soc || 0) * 100);
const socInfoEl = document.getElementById('socInfo');
socInfoEl.textContent = socPctRaw.toFixed(0) + '% (' + (socPctRaw * 5).toFixed(0) + ' kWh)';
socInfoEl.style.color = socPctRaw < 20 ? 'var(--red)' : socPctRaw > 70 ? 'var(--green)' : 'var(--text)';
const fuelPct = ((obs.diesel_fuel_remaining || 0) * 100);
document.getElementById('fuelInfo').textContent = fuelPct.toFixed(0) + '%';
// Forecasts — color-coded grid
const curStep = Math.floor(obs.hour);
for (let i = 0; i < 4; i++) {
const h = (curStep + i + 1 + START_HOUR) % 24;
document.getElementById('fcH' + (i+1)).textContent = String(h).padStart(2,'0') + ':00';
}
// Demand forecast
const dfc = obs.demand_forecast_4h || [];
for (let i = 0; i < 4; i++) {
const el = document.getElementById('fcD' + (i+1));
if (dfc[i] != null) {
const v = dfc[i];
el.innerHTML = v.toFixed(0) + '<span style="font-size:8px;opacity:0.6"> kW</span>';
el.className = 'fc-cell ' + (v > 200 ? 'high' : v > 120 ? 'med' : 'low');
} else { el.textContent = '—'; el.className = 'fc-cell'; }
}
// Solar forecast
const sfc = obs.solar_forecast_4h || [];
for (let i = 0; i < 4; i++) {
const el = document.getElementById('fcS' + (i+1));
if (sfc[i] != null) {
const v = sfc[i];
el.innerHTML = v.toFixed(0) + '<span style="font-size:8px;opacity:0.6"> kW</span>';
el.className = 'fc-cell ' + (v > 100 ? 'bright' : v > 0 ? 'low' : '');
} else { el.textContent = '—'; el.className = 'fc-cell'; }
}
// Price forecast
const pfc = obs.price_forecast_4h || [];
for (let i = 0; i < 4; i++) {
const el = document.getElementById('fcP' + (i+1));
if (pfc[i] != null) {
const v = pfc[i];
el.innerHTML = '₹' + v.toFixed(1) + '<span style="font-size:7px;opacity:0.6">/kWh</span>';
el.className = 'fc-cell ' + (v > 10 ? 'high' : v > 6 ? 'med' : 'low');
} else { el.textContent = '—'; el.className = 'fc-cell'; }
}
// Flow diagram
// Flow chips from real physics data
const f = {
solar: obs.flow_solar || 0,
gridIn: obs.flow_grid_import || 0,
gridOut: obs.flow_grid_export || 0,
battOut: obs.flow_battery_discharge || 0,
battIn: obs.flow_battery_charge || 0,
diesel: obs.flow_diesel || 0,
demand: obs.flow_demand || obs.demand_kw || 0,
blackout: obs.flow_blackout || 0,
shed: obs.flow_shed || 0,
totalSupply: obs.flow_total_supply || 0,
totalConsume: obs.flow_total_consumption || 0,
};
// Label which hour the flows are from
const prevClock = (Math.floor(obs.hour) - 1 + 6 + 24) % 24;
document.getElementById('flowHourLabel').textContent = String(prevClock).padStart(2,'0') + ':00 Energy Balance (last hour)';
document.getElementById('chipSolar').innerHTML = '&#9728;&#65039; Solar <b>' + f.solar.toFixed(0) + '</b>';
document.getElementById('chipGridIn').innerHTML = '&#9889; Grid In <b>' + f.gridIn.toFixed(0) + '</b>';
document.getElementById('chipBattOut').innerHTML = '&#128267; Batt Out <b>' + f.battOut.toFixed(0) + '</b>';
document.getElementById('chipDiesel').innerHTML = '&#9981; Diesel <b>' + f.diesel.toFixed(0) + '</b>';
document.getElementById('flowTotalSupply').textContent = '= ' + f.totalSupply.toFixed(0) + ' kW supply';
document.getElementById('chipDemand').innerHTML = '&#127968; Homes <b>' + f.demand.toFixed(0) + '</b>';
document.getElementById('chipGridOut').innerHTML = '&#9889; Grid Out <b>' + f.gridOut.toFixed(0) + '</b>';
document.getElementById('chipBattIn').innerHTML = '&#128267; Batt Charge <b>' + f.battIn.toFixed(0) + '</b>';
document.getElementById('flowTotalConsume').textContent = '= ' + f.totalConsume.toFixed(0) + ' kW consumption';
const chipBlackout = document.getElementById('chipBlackout');
if (f.blackout > 0) {
chipBlackout.innerHTML = '&#9888;&#65039; Blackout <b>' + f.blackout.toFixed(0) + '</b>';
chipBlackout.style.display = '';
} else {
chipBlackout.style.display = 'none';
}
// Shed display in chips
if (f.shed > 0) {
document.getElementById('chipDemand').innerHTML = '&#127968; Homes <b>' + f.demand.toFixed(0) + '</b> <span style="font-size:8px;color:var(--yellow)">(-' + f.shed.toFixed(0) + ' shed)</span>';
}
document.getElementById('flowDemand').textContent = f.demand.toFixed(0) + ' kW';
const gridKw = obs.grid_kw_this_step || 0;
// Status + house grid
const blackout = obs.blackout_this_step || 0;
// Energy balance verdict
const verdictEl = document.getElementById('balanceVerdict');
const detailEl = document.getElementById('balanceDetail');
const stepCost = obs.cost_this_step || 0;
const net = f.totalSupply - f.totalConsume;
if (f.blackout > 0) {
verdictEl.innerHTML = '&#9888;&#65039; DEFICIT';
verdictEl.style.color = 'var(--red)';
detailEl.textContent = f.blackout.toFixed(0) + ' kW unmet | ₹' + (150 * blackout).toFixed(0) + ' penalty';
} else if (f.gridOut > 10) {
verdictEl.innerHTML = '&#10022; SURPLUS';
verdictEl.style.color = 'var(--green)';
detailEl.textContent = 'Exporting ' + f.gridOut.toFixed(0) + ' kW | Earning ₹' + (f.gridOut * (obs.grid_price||0)).toFixed(0);
} else if (f.gridIn > 150) {
verdictEl.innerHTML = '&#9889; TIGHT';
verdictEl.style.color = 'var(--yellow)';
detailEl.textContent = 'Grid ' + f.gridIn.toFixed(0) + '/200 kW | Cost ₹' + stepCost.toFixed(0) + '/hr';
} else {
verdictEl.innerHTML = '&#9679; BALANCED';
verdictEl.style.color = 'var(--cyan)';
detailEl.textContent = 'Grid ' + f.gridIn.toFixed(0) + ' kW | Cost ₹' + stepCost.toFixed(0) + '/hr';
}
const statusIcon = document.getElementById('statusIcon');
const statusText = document.getElementById('statusText');
const blackoutHint = document.getElementById('flowBlackoutHint');
// Calculate how many homes are dark (proportional to blackout vs demand)
const demandNow = obs.demand_kw || 1;
const blackoutFrac = Math.min(1, blackout / (demandNow + 0.01));
const darkHomes = Math.round(blackoutFrac * 100);
if (darkHomes > 0) {
statusIcon.textContent = '\u{1F6A8}';
statusText.textContent = darkHomes + ' HOMES DARK';
statusText.style.color = 'var(--red)';
blackoutHint.textContent = darkHomes + ' homes without power!';
blackoutHint.style.display = 'inline';
} else {
statusIcon.textContent = '\u2705';
statusText.textContent = 'ALL POWERED';
statusText.style.color = 'var(--green)';
blackoutHint.style.display = 'none';
}
// Update 10x10 house grid
const grid = document.getElementById('houseGrid');
if (!grid.children.length) {
for (let i = 0; i < 100; i++) {
const cell = document.createElement('div');
cell.className = 'house-cell powered';
cell.textContent = '\u{1F3E0}';
grid.appendChild(cell);
}
}
for (let i = 0; i < 100; i++) {
const cell = grid.children[i];
if (i < (100 - darkHomes)) {
cell.className = 'house-cell powered';
} else {
cell.className = 'house-cell dark';
}
}
// Narration
document.getElementById('narration').innerHTML = '<strong>System:</strong> ' + (obs.narration || '');
// Right panel scores
const totalDemandApprox = Math.max(obs.hour * 300, 1);
const reliability = totalDemandApprox > 0 ? Math.max(0, 1 - (obs.cumulative_blackout_kwh || 0) / totalDemandApprox) : 1;
const relPct = (reliability * 100).toFixed(1);
const relEl = document.getElementById('relValue');
relEl.textContent = relPct + '%';
relEl.className = 'sc-value ' + (reliability > 0.95 ? 'good' : reliability > 0.9 ? 'warn' : 'bad');
document.getElementById('relBar').style.width = relPct + '%';
document.getElementById('relBar').style.background = reliability > 0.95 ? 'var(--green)' : reliability > 0.9 ? 'var(--yellow)' : 'var(--red)';
const cost = (obs.cumulative_cost || 0);
document.getElementById('costValue').textContent = '₹' + cost.toLocaleString('en-IN', {maximumFractionDigits:0});
document.getElementById('blackoutValue').textContent = (obs.cumulative_blackout_kwh || 0).toFixed(1) + ' kWh';
const bvEl = document.getElementById('blackoutValue');
bvEl.className = 'sc-value ' + ((obs.cumulative_blackout_kwh || 0) < 1 ? 'good' : 'bad');
const socPct = ((obs.battery_soc || 0) * 100).toFixed(0);
const socKwh = ((obs.battery_soc || 0) * 500).toFixed(0);
document.getElementById('socValue').textContent = socPct + '% (' + socKwh + ' kWh)';
document.getElementById('socBar').style.width = socPct + '%';
document.getElementById('rewardValue').textContent = (obs.reward != null ? obs.reward.toFixed(3) : '—');
}
function showGrade(g) {
const box = document.getElementById('gradeBox');
box.classList.remove('hidden');
document.getElementById('gradeValue').textContent = g.score.toFixed(3);
document.getElementById('gradeSub').innerHTML =
`Lights on: ${(g.reliability*100).toFixed(1)}% · Cost efficiency: ${(g.cost_efficiency*100).toFixed(0)}% · Green: ${(g.green_score*100).toFixed(0)}%<br>` +
`Spent ₹${g.actual_cost.toLocaleString('en-IN',{maximumFractionDigits:0})} vs baseline ₹${g.baseline_cost.toLocaleString('en-IN',{maximumFractionDigits:0})}`;
}
function updateTaskTag() {
const sel = document.getElementById('taskSelect');
const tag = document.getElementById('taskTag');
const i = sel.selectedIndex;
const labels = ['Task 1: Normal', 'Task 2: Heatwave', 'Task 3: Crisis'];
const classes = ['easy', 'medium', 'hard'];
tag.textContent = labels[i];
tag.className = 'tag ' + classes[i];
}
// ── Event handlers ──────────────────────────────────────────────
document.getElementById('stepBtn').addEventListener('click', stepEnv);
document.getElementById('resetBtn').addEventListener('click', resetEnv);
document.getElementById('taskSelect').addEventListener('change', () => { updateTaskTag(); resetEnv(); });
// Keyboard shortcuts (only when intro is dismissed)
document.addEventListener('keydown', e => {
if (!document.getElementById('introOverlay').classList.contains('hidden')) return;
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); stepEnv(); }
if (e.key === 'r' && !e.metaKey && !e.ctrlKey) resetEnv();
});
// ── Intro dismiss ───────────────────────────────────────────────
document.getElementById('btnStart').addEventListener('click', () => {
document.getElementById('introOverlay').classList.add('hidden');
resetEnv();
});
// Also dismiss on Enter/Space while intro is visible
document.getElementById('introOverlay').addEventListener('keydown', e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
document.getElementById('introOverlay').classList.add('hidden');
resetEnv();
}
});
// Slide dot indicators
const slideTrack = document.getElementById('slideTrack');
const dots = document.querySelectorAll('#slideDots .dot');
if (slideTrack && dots.length) {
slideTrack.addEventListener('scroll', () => {
const idx = Math.round(slideTrack.scrollLeft / slideTrack.clientWidth);
dots.forEach((d, i) => d.classList.toggle('active', i === idx));
});
dots.forEach((d, i) => d.addEventListener('click', () => {
slideTrack.scrollTo({ left: i * slideTrack.clientWidth, behavior: 'smooth' });
}));
}
document.getElementById('btnStart').focus();
</script>
</body>
</html>