Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Grid-Gent Demo</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <style> | |
| :root { | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| background-color: #0f172a; | |
| color: #e5e7eb; | |
| } | |
| body { margin: 0; padding: 0; } | |
| .page { max-width: 1040px; margin: 0 auto; padding: 24px 16px 48px; } | |
| .card { | |
| background: #020617; | |
| border-radius: 16px; | |
| padding: 20px; | |
| box-shadow: 0 18px 40px rgba(15,23,42,0.8); | |
| border: 1px solid rgba(148, 163, 184, 0.2); | |
| } | |
| h1 { font-size: 1.8rem; margin-bottom: 0.25rem; } | |
| h2 { font-size: 1.1rem; margin: 0; color: #9ca3af; } | |
| textarea { | |
| width: 100%; | |
| min-height: 90px; | |
| padding: 10px 12px; | |
| border-radius: 10px; | |
| border: 1px solid #4b5563; | |
| background: #020617; | |
| color: #e5e7eb; | |
| resize: vertical; | |
| font-family: inherit; | |
| font-size: 0.95rem; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: #38bdf8; | |
| box-shadow: 0 0 0 1px #0ea5e9; | |
| } | |
| button { | |
| margin-top: 10px; | |
| padding: 9px 16px; | |
| border-radius: 999px; | |
| border: none; | |
| background: linear-gradient(135deg, #0ea5e9, #22c55e); | |
| color: white; | |
| font-weight: 600; | |
| cursor: pointer; | |
| font-size: 0.95rem; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| button:disabled { opacity: 0.5; cursor: not-allowed; } | |
| .answer { | |
| margin-top: 18px; | |
| white-space: pre-wrap; | |
| background: #020617; | |
| border-radius: 10px; | |
| padding: 12px 14px; | |
| border: 1px solid #4b5563; | |
| font-size: 0.9rem; | |
| } | |
| .answer-badge { | |
| display: inline-block; | |
| font-size: 0.75rem; | |
| padding: 3px 8px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(248, 250, 252, 0.2); | |
| background: rgba(127, 29, 29, 0.6); | |
| color: #fecaca; | |
| margin-bottom: 6px; | |
| } | |
| .steps { margin-top: 16px; font-size: 0.8rem; } | |
| .step { | |
| padding: 8px 10px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(75,85,99,0.7); | |
| background: rgba(15,23,42,0.8); | |
| margin-bottom: 6px; | |
| } | |
| .step strong { color: #a5b4fc; } | |
| .badge { | |
| display: inline-block; | |
| padding: 2px 7px; | |
| border-radius: 999px; | |
| font-size: 0.7rem; | |
| background: rgba(15,118,110,0.3); | |
| color: #a7f3d0; | |
| margin-left: 6px; | |
| } | |
| .badge-secondary { | |
| background: rgba(30,64,175,0.5); | |
| color: #bfdbfe; | |
| } | |
| .header-row { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: baseline; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .pill { | |
| font-size: 0.75rem; | |
| padding: 4px 10px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148,163,184,0.5); | |
| color: #e5e7eb; | |
| } | |
| .examples { | |
| margin-top: 10px; | |
| font-size: 0.8rem; | |
| color: #9ca3af; | |
| } | |
| .examples code { | |
| background: rgba(15,23,42,0.8); | |
| padding: 2px 6px; | |
| border-radius: 6px; | |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; | |
| } | |
| .layout { | |
| display: grid; | |
| grid-template-columns: minmax(0, 2.2fr) minmax(0, 1.2fr); | |
| gap: 18px; | |
| margin-top: 18px; | |
| } | |
| @media (max-width: 900px) { | |
| .layout { grid-template-columns: minmax(0, 1fr); } | |
| } | |
| .panel { | |
| border-radius: 12px; | |
| border: 1px solid rgba(148,163,184,0.25); | |
| padding: 12px 12px 14px; | |
| background: rgba(15,23,42,0.8); | |
| } | |
| .panel-title { | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| margin-bottom: 6px; | |
| } | |
| .panel-sub { | |
| font-size: 0.75rem; | |
| color: #9ca3af; | |
| margin-bottom: 6px; | |
| } | |
| input[type="file"] { | |
| font-size: 0.8rem; | |
| } | |
| .status { | |
| margin-top: 6px; | |
| font-size: 0.75rem; | |
| color: #9ca3af; | |
| white-space: pre-wrap; | |
| } | |
| .feeders-list { | |
| margin-top: 4px; | |
| font-size: 0.78rem; | |
| } | |
| .feeders-list code { | |
| background: rgba(15,23,42,0.8); | |
| padding: 1px 5px; | |
| border-radius: 6px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="page"> | |
| <div class="card"> | |
| <div class="header-row"> | |
| <div> | |
| <h1>Grid-Gent Demo</h1> | |
| <h2>Agentic assistant for city distribution grid scenarios (simplified)</h2> | |
| </div> | |
| <div class="pill">Offline demo · no real grid connection</div> | |
| </div> | |
| <div class="layout"> | |
| <div> | |
| <div> | |
| <label for="query">Describe a grid scenario or question:</label> | |
| <textarea id="query" placeholder="Example: What happens on feeder F2 if we add 5 MW of rooftop PV?"></textarea> | |
| <button id="ask-btn" onclick="sendQuery()"> | |
| <span id="btn-label">Run Grid-Gent</span> | |
| <span id="btn-spinner" style="display:none;">⏳</span> | |
| </button> | |
| </div> | |
| <div class="examples"> | |
| Try things like: | |
| <div><code>What happens on feeder F2 if we add 5 MW of rooftop PV?</code></div> | |
| <div><code>Simulate adding 3 MW of load on feeder F1.</code></div> | |
| </div> | |
| <div id="answer" class="answer" style="display:none;"> | |
| <div class="answer-badge">Demo only · not using your real grid data unless you upload a model · not for operational decisions</div> | |
| <div id="answer-text"></div> | |
| </div> | |
| <div id="steps" class="steps" style="display:none;"></div> | |
| </div> | |
| <div> | |
| <div class="panel"> | |
| <div class="panel-title">Upload grid model (JSON or CSV)</div> | |
| <div class="panel-sub"> | |
| We will replace the demo feeders with your uploaded ones (still using a simplified calculation). | |
| </div> | |
| <input type="file" id="grid-file" accept=".json,.csv" /> | |
| <button id="upload-btn" style="margin-top:8px;" onclick="uploadGrid()"> | |
| <span id="upload-label">Upload model</span> | |
| <span id="upload-spinner" style="display:none;">⏳</span> | |
| </button> | |
| <div id="upload-status" class="status"></div> | |
| </div> | |
| <div class="panel" style="margin-top:10px;"> | |
| <div class="panel-title">Currently loaded feeders</div> | |
| <div class="panel-sub">Based on demo data or your last upload.</div> | |
| <div id="feeders" class="feeders-list">Loading...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| async function refreshFeeders() { | |
| const el = document.getElementById("feeders"); | |
| try { | |
| const resp = await fetch("/api/feeders"); | |
| if (!resp.ok) { | |
| el.textContent = "Error loading feeders."; | |
| return; | |
| } | |
| const data = await resp.json(); | |
| const feeders = data.feeders || {}; | |
| const ids = Object.keys(feeders); | |
| if (!ids.length) { | |
| el.textContent = "No feeders configured."; | |
| return; | |
| } | |
| const parts = ids.map(id => { | |
| const f = feeders[id]; | |
| const name = f.name || id; | |
| const peak = f.peak_mw; | |
| return id + " – " + name + " (" + peak + " MW peak)"; | |
| }); | |
| el.innerHTML = parts.map(p => "<div><code>" + p + "</code></div>").join(""); | |
| } catch (e) { | |
| el.textContent = "Error loading feeders: " + e; | |
| } | |
| } | |
| async function sendQuery() { | |
| const textarea = document.getElementById("query"); | |
| const btn = document.getElementById("ask-btn"); | |
| const label = document.getElementById("btn-label"); | |
| const spinner = document.getElementById("btn-spinner"); | |
| const answerBox = document.getElementById("answer"); | |
| const answerText = document.getElementById("answer-text"); | |
| const stepsEl = document.getElementById("steps"); | |
| const query = textarea.value.trim(); | |
| if (!query) { | |
| alert("Please enter a scenario or question."); | |
| return; | |
| } | |
| btn.disabled = true; | |
| spinner.style.display = "inline-block"; | |
| label.textContent = "Running..."; | |
| try { | |
| const resp = await fetch("/api/ask", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ query }) | |
| }); | |
| const data = await resp.json(); | |
| if (!resp.ok) { | |
| answerBox.style.display = "block"; | |
| answerText.textContent = "Error: " + (data.error || resp.statusText); | |
| stepsEl.style.display = "none"; | |
| return; | |
| } | |
| answerBox.style.display = "block"; | |
| answerText.textContent = data.answer || "(No answer returned)"; | |
| stepsEl.innerHTML = ""; | |
| if (Array.isArray(data.steps)) { | |
| stepsEl.style.display = "block"; | |
| data.steps.forEach(step => { | |
| const div = document.createElement("div"); | |
| div.className = "step"; | |
| const role = step.role || "agent"; | |
| const meta = step.meta || {}; | |
| const intent = meta.intent || ""; | |
| const feeder = meta.feeder || ""; | |
| let badgesHtml = ""; | |
| if (intent) { | |
| badgesHtml += "<span class=\"badge\">" + intent + "</span>"; | |
| } | |
| if (feeder) { | |
| badgesHtml += "<span class=\"badge badge-secondary\">" + feeder + "</span>"; | |
| } | |
| div.innerHTML = "<div><strong>" + role + "</strong> " + badgesHtml + "</div>" + | |
| "<div>" + step.content + "</div>"; | |
| stepsEl.appendChild(div); | |
| }); | |
| } else { | |
| stepsEl.style.display = "none"; | |
| } | |
| } catch (err) { | |
| answerBox.style.display = "block"; | |
| answerText.textContent = "Request failed: " + err; | |
| stepsEl.style.display = "none"; | |
| } finally { | |
| btn.disabled = false; | |
| spinner.style.display = "none"; | |
| label.textContent = "Run Grid-Gent"; | |
| } | |
| } | |
| async function uploadGrid() { | |
| const input = document.getElementById("grid-file"); | |
| const status = document.getElementById("upload-status"); | |
| const btn = document.getElementById("upload-btn"); | |
| const label = document.getElementById("upload-label"); | |
| const spinner = document.getElementById("upload-spinner"); | |
| const file = input.files[0]; | |
| if (!file) { | |
| alert("Please choose a JSON or CSV file first."); | |
| return; | |
| } | |
| let fmt = "json"; | |
| if (file.name.toLowerCase().endsWith(".csv")) { | |
| fmt = "csv"; | |
| } else if (file.name.toLowerCase().endsWith(".json")) { | |
| fmt = "json"; | |
| } else { | |
| alert("File extension must be .json or .csv"); | |
| return; | |
| } | |
| btn.disabled = true; | |
| spinner.style.display = "inline-block"; | |
| label.textContent = "Uploading..."; | |
| const reader = new FileReader(); | |
| reader.onload = async () => { | |
| const raw = reader.result; | |
| try { | |
| const resp = await fetch("/api/upload-grid", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ raw, format: fmt }) | |
| }); | |
| const data = await resp.json(); | |
| if (!resp.ok) { | |
| status.textContent = "Upload failed: " + (data.error || resp.statusText); | |
| } else { | |
| status.textContent = "Upload succeeded. Feeders loaded: " + (data.feeders_loaded || []).join(", "); | |
| refreshFeeders(); | |
| } | |
| } catch (e) { | |
| status.textContent = "Upload failed: " + e; | |
| } finally { | |
| btn.disabled = false; | |
| spinner.style.display = "none"; | |
| label.textContent = "Upload model"; | |
| } | |
| }; | |
| reader.onerror = () => { | |
| status.textContent = "Could not read file."; | |
| btn.disabled = false; | |
| spinner.style.display = "none"; | |
| label.textContent = "Upload model"; | |
| }; | |
| reader.readAsText(file); | |
| } | |
| refreshFeeders(); | |
| </script> | |
| </body> | |
| </html> | |