| <!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> |
| |
| *, *::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 { |
| 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); } |
| |
| |
| .app { display: grid; grid-template-columns: 280px 1fr 260px; gap: 0; height: calc(100vh - 52px); } |
| |
| |
| .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); } |
| |
| |
| .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; } |
| |
| |
| .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); } } |
| |
| |
| .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 { |
| 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); } |
| |
| |
| .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-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-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); |
| } |
| |
| |
| @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> |
|
|
| |
| <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> |
|
|
| |
| <div class="slide-track" id="slideTrack"> |
|
|
| |
| <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;">🏠</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 · 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> — while the national grid automatically covers whatever is left over.</div> |
| </div> |
| </div> |
|
|
| |
| <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;">☀️</div><div style="font-size:7px;color:var(--yellow);">SOLAR</div></div> |
| <div style="text-align:center;"><div style="font-size:22px;">🔋</div><div style="font-size:7px;color:var(--blue);">BATTERY</div></div> |
| <div style="text-align:center;"><div style="font-size:22px;">⛽</div><div style="font-size:7px;color:var(--orange);">DIESEL</div></div> |
| <div style="text-align:center;"><div style="font-size:22px;">⚡</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 — 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> |
|
|
| |
| <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;">↓ ₹</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 · 25% lights · 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> — 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 · <strong>R</strong> to reset</div> |
| </div> |
| </div> |
|
|
| |
| <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"> |
| |
| <div class="panel-left"> |
| <div> |
| <div class="section-title">Coming Up Next</div> |
| <div class="info-row"><span class="label">🕐 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">🏠 Homes Will Need</span><span id="demandInfo">—</span></div> |
| <div class="info-row"><span class="label">☀️ Solar Expected</span><span id="solarInfo">—</span></div> |
| <div class="info-row"><span class="label">⚡ Grid Will Cover</span><span id="gridNeedInfo">—</span></div> |
| <div class="info-row"><span class="label">💰 Grid Price</span><span id="priceInfo" style="font-weight:700">—</span></div> |
| <div class="info-row"><span class="label">🔋 Battery Charge</span><span id="socInfo">—</span></div> |
| <div class="info-row"><span class="label">⛽ 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;">🏠 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;">☀️ 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;">💰 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>🔋 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>← Store energy</span><span>Use stored →</span> |
| </div> |
| </div> |
| <div class="control-group"> |
| <label>⛽ 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>🏠 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 →</button> |
| <button class="btn-reset" id="resetBtn">Restart Episode</button> |
| </div> |
|
|
| |
| <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;"> |
| |
| <div style="display:flex; gap:12px; align-items:stretch; flex:1; min-height:0;"> |
|
|
| |
| <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">☀️ Solar <b>0</b></div> |
| <div class="flow-chip supply" id="chipGridIn">⚡ Grid Import <b>0</b></div> |
| <div class="flow-chip supply" id="chipBattOut">🔋 Batt Out <b>0</b></div> |
| <div class="flow-chip supply" id="chipDiesel">⛽ 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">🏠 Homes <b>0</b></div> |
| <div class="flow-chip demand" id="chipGridOut">⚡ Grid Export <b>0</b></div> |
| <div class="flow-chip demand" id="chipBattIn">🔋 Batt Charge <b>0</b></div> |
| <div class="flow-chip danger" id="chipBlackout" style="display:none">⚠️ 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> |
|
|
| |
| <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;">● BALANCED</span> |
| <span id="balanceDetail" style="font-size:9px;color:var(--text-dim);"></span> |
| </div> |
| </div> |
|
|
| |
| <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">✅</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> |
|
|
| |
| <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">🏠 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">💰 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">🔋 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">⚠️ 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">⛽ 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> |
| |
| const API = window.location.origin + '/api'; |
| let obs = null; |
| let running = false; |
| |
| |
| 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 }, |
| } |
| } |
| }); |
| |
| |
| 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'; |
| }; |
| |
| |
| 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; |
| |
| 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; |
| |
| |
| pushChartPoint(obs); |
| chart.update(); |
| |
| updateUI(); |
| |
| if (obs.done) { |
| running = false; |
| document.getElementById('stepBtn').disabled = true; |
| |
| 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); |
| } |
| } |
| } |
| |
| |
| 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'; |
| |
| |
| 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)'; |
| |
| |
| const gridGap = demKw - solKw; |
| 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) + '%'; |
| |
| |
| 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'; |
| } |
| |
| 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'; } |
| } |
| |
| 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'; } |
| } |
| |
| 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'; } |
| } |
| |
| |
| |
| 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, |
| }; |
| |
| |
| 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 = '☀️ Solar <b>' + f.solar.toFixed(0) + '</b>'; |
| document.getElementById('chipGridIn').innerHTML = '⚡ Grid In <b>' + f.gridIn.toFixed(0) + '</b>'; |
| document.getElementById('chipBattOut').innerHTML = '🔋 Batt Out <b>' + f.battOut.toFixed(0) + '</b>'; |
| document.getElementById('chipDiesel').innerHTML = '⛽ Diesel <b>' + f.diesel.toFixed(0) + '</b>'; |
| document.getElementById('flowTotalSupply').textContent = '= ' + f.totalSupply.toFixed(0) + ' kW supply'; |
| |
| document.getElementById('chipDemand').innerHTML = '🏠 Homes <b>' + f.demand.toFixed(0) + '</b>'; |
| document.getElementById('chipGridOut').innerHTML = '⚡ Grid Out <b>' + f.gridOut.toFixed(0) + '</b>'; |
| document.getElementById('chipBattIn').innerHTML = '🔋 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 = '⚠️ Blackout <b>' + f.blackout.toFixed(0) + '</b>'; |
| chipBlackout.style.display = ''; |
| } else { |
| chipBlackout.style.display = 'none'; |
| } |
| |
| |
| if (f.shed > 0) { |
| document.getElementById('chipDemand').innerHTML = '🏠 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; |
| |
| |
| const blackout = obs.blackout_this_step || 0; |
| |
| |
| 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 = '⚠️ 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 = '✦ 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 = '⚡ TIGHT'; |
| verdictEl.style.color = 'var(--yellow)'; |
| detailEl.textContent = 'Grid ' + f.gridIn.toFixed(0) + '/200 kW | Cost ₹' + stepCost.toFixed(0) + '/hr'; |
| } else { |
| verdictEl.innerHTML = '● 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'); |
| |
| |
| 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'; |
| } |
| |
| |
| 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'; |
| } |
| } |
| |
| |
| document.getElementById('narration').innerHTML = '<strong>System:</strong> ' + (obs.narration || ''); |
| |
| |
| 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]; |
| } |
| |
| |
| document.getElementById('stepBtn').addEventListener('click', stepEnv); |
| document.getElementById('resetBtn').addEventListener('click', resetEnv); |
| document.getElementById('taskSelect').addEventListener('change', () => { updateTaskTag(); resetEnv(); }); |
| |
| |
| 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(); |
| }); |
| |
| |
| document.getElementById('btnStart').addEventListener('click', () => { |
| document.getElementById('introOverlay').classList.add('hidden'); |
| resetEnv(); |
| }); |
| |
| document.getElementById('introOverlay').addEventListener('keydown', e => { |
| if (e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| document.getElementById('introOverlay').classList.add('hidden'); |
| resetEnv(); |
| } |
| }); |
| |
| 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> |
|
|