grid-gent / app /web /index.html
James Afful
Add Grid-Gent Space code and Dockerfile (no binary assets)
458fa79
<!DOCTYPE html>
<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>