Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1" /> | |
| <title>微电网调度闭环智能体</title> | |
| <link rel="preconnect" href="https://cdn.jsdelivr.net" /> | |
| <script src="https://cdn.jsdelivr.net/npm/markdown-it@14.1.0/dist/markdown-it.min.js"></script> | |
| <style> | |
| :root { | |
| color-scheme: dark; | |
| --bg: #070c1f; | |
| --surface: rgba(18, 29, 58, 0.75); | |
| --stroke: rgba(120, 145, 208, 0.2); | |
| --text: #e6eeff; | |
| --muted: #9fb2da; | |
| --accent: #7dd3fc; | |
| --accent-strong: #38bdf8; | |
| --warn: #fda4af; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| font-family: "PingFang SC", "Microsoft YaHei", system-ui, -apple-system, sans-serif; | |
| background: radial-gradient(circle at top, #1a2a6c 0%, #070c1f 60%); | |
| color: var(--text); | |
| } | |
| header { | |
| padding: 28px 20px 12px; | |
| text-align: center; | |
| } | |
| header h1 { | |
| margin: 0 0 6px; | |
| font-size: 26px; | |
| } | |
| header p { | |
| margin: 0; | |
| color: var(--muted); | |
| font-size: 14px; | |
| } | |
| main { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 16px 16px 40px; | |
| display: grid; | |
| gap: 16px; | |
| } | |
| .grid { | |
| display: grid; | |
| gap: 16px; | |
| } | |
| .grid.two { | |
| grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); | |
| } | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--stroke); | |
| border-radius: 16px; | |
| padding: 16px; | |
| backdrop-filter: blur(14px); | |
| } | |
| .card h3 { | |
| margin-top: 0; | |
| font-size: 18px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 12px; | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| input, | |
| select, | |
| textarea { | |
| width: 100%; | |
| margin-top: 6px; | |
| padding: 10px 12px; | |
| border-radius: 12px; | |
| border: 1px solid transparent; | |
| background: rgba(8, 16, 38, 0.7); | |
| color: var(--text); | |
| font-size: 14px; | |
| } | |
| textarea { | |
| min-height: 88px; | |
| resize: vertical; | |
| } | |
| button { | |
| padding: 10px 16px; | |
| background: linear-gradient(120deg, #0ea5e9, #6366f1); | |
| border: none; | |
| border-radius: 999px; | |
| color: #fff; | |
| font-weight: 600; | |
| cursor: pointer; | |
| width: 100%; | |
| } | |
| button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| .pill { | |
| display: inline-block; | |
| padding: 4px 10px; | |
| border-radius: 999px; | |
| background: rgba(125, 211, 252, 0.15); | |
| color: var(--accent); | |
| font-size: 12px; | |
| } | |
| .kpi { | |
| display: grid; | |
| gap: 8px; | |
| } | |
| .kpi span { | |
| color: var(--muted); | |
| font-size: 13px; | |
| } | |
| .kpi strong { | |
| font-size: 16px; | |
| } | |
| .log { | |
| background: rgba(10, 18, 40, 0.6); | |
| border: 1px solid var(--stroke); | |
| border-radius: 12px; | |
| padding: 10px 12px; | |
| margin-bottom: 8px; | |
| } | |
| .log header { | |
| padding: 0; | |
| text-align: left; | |
| font-size: 12px; | |
| color: var(--muted); | |
| margin-bottom: 6px; | |
| } | |
| .warn { | |
| color: var(--warn); | |
| } | |
| .cycle-list button { | |
| width: 100%; | |
| margin-bottom: 8px; | |
| background: rgba(56, 189, 248, 0.16); | |
| color: var(--text); | |
| } | |
| .toolbar { | |
| display: grid; | |
| gap: 10px; | |
| grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); | |
| } | |
| .stat-grid { | |
| display: grid; | |
| gap: 10px; | |
| grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); | |
| } | |
| .stat-card { | |
| padding: 10px 12px; | |
| border-radius: 12px; | |
| border: 1px solid var(--stroke); | |
| background: rgba(10, 16, 36, 0.55); | |
| } | |
| .stat-card span { | |
| display: block; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| .stat-card strong { | |
| font-size: 18px; | |
| } | |
| @media (max-width: 720px) { | |
| header h1 { | |
| font-size: 22px; | |
| } | |
| main { | |
| padding: 12px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>微电网调度闭环智能体</h1> | |
| <p>面向新能源微电网的推理/决策/行动/校验/迭代与回放系统</p> | |
| </header> | |
| <main> | |
| <section class="grid two"> | |
| <article class="card"> | |
| <h3>新建调度任务</h3> | |
| <form id="cycleForm"> | |
| <label> | |
| 任务标题 | |
| <input name="title" placeholder="例如:园区A峰值调度" required /> | |
| </label> | |
| <label> | |
| 需求负荷(MW) | |
| <input name="demand_mw" type="number" step="0.1" value="120" required /> | |
| </label> | |
| <label> | |
| 光伏出力(MW) | |
| <input name="solar_mw" type="number" step="0.1" value="40" required /> | |
| </label> | |
| <label> | |
| 风电出力(MW) | |
| <input name="wind_mw" type="number" step="0.1" value="30" required /> | |
| </label> | |
| <label> | |
| 储能容量(MWh) | |
| <input name="storage_mwh" type="number" step="0.1" value="80" required /> | |
| </label> | |
| <label> | |
| 储能SOC(0-1) | |
| <input name="storage_soc" type="number" step="0.01" value="0.45" required /> | |
| </label> | |
| <label> | |
| 优先级策略 | |
| <select name="priority"> | |
| <option value="成本优先">成本优先</option> | |
| <option value="碳优先">碳优先</option> | |
| <option value="可靠性优先">可靠性优先</option> | |
| </select> | |
| </label> | |
| <label> | |
| 业务备注(支持Markdown) | |
| <textarea name="notes" placeholder="例如:需要保障冷链仓储负荷、可接受需求响应10%"></textarea> | |
| </label> | |
| <button type="submit" id="runBtn">生成闭环调度</button> | |
| </form> | |
| <div style="margin-top: 14px" class="toolbar"> | |
| <button type="button" id="suggestBtn">生成建议场景</button> | |
| <button type="button" id="demoBtn">一键演示</button> | |
| </div> | |
| </article> | |
| <article class="card"> | |
| <h3>闭环结果概览</h3> | |
| <div id="overview"> | |
| <span class="pill">等待调度</span> | |
| <div class="kpi" style="margin-top: 12px"> | |
| <span>成本</span> | |
| <strong id="kpiCost">-</strong> | |
| <span>碳排影响</span> | |
| <strong id="kpiCarbon">-</strong> | |
| <span>可靠性</span> | |
| <strong id="kpiReliability">-</strong> | |
| </div> | |
| </div> | |
| <div style="margin-top: 14px"> | |
| <h4>记忆快照</h4> | |
| <div id="memoryBox" class="log"></div> | |
| </div> | |
| </article> | |
| </section> | |
| <section class="grid two"> | |
| <article class="card"> | |
| <h3>调度方案与校验</h3> | |
| <div id="planBox" class="log"></div> | |
| <div id="riskBox" class="log"></div> | |
| <div id="dispatchBox" class="log"></div> | |
| <div id="validationBox" class="log"></div> | |
| </article> | |
| <article class="card"> | |
| <h3>闭环日志回放</h3> | |
| <div id="logList"></div> | |
| </article> | |
| </section> | |
| <section class="grid two"> | |
| <article class="card"> | |
| <h3>历史调度</h3> | |
| <div id="cycleList" class="cycle-list"></div> | |
| </article> | |
| <article class="card"> | |
| <h3>Markdown 预览</h3> | |
| <div id="markdownPreview" class="log"></div> | |
| </article> | |
| </section> | |
| <section class="grid two"> | |
| <article class="card"> | |
| <h3>运行统计</h3> | |
| <div id="statGrid" class="stat-grid"></div> | |
| <button type="button" id="refreshStatsBtn" style="margin-top: 10px">刷新统计</button> | |
| </article> | |
| <article class="card"> | |
| <h3>文件与数据集</h3> | |
| <div class="toolbar"> | |
| <input type="file" id="fileInput" /> | |
| <button type="button" id="uploadBtn">上传文件</button> | |
| </div> | |
| <progress id="uploadProgress" value="0" max="100" style="width: 100%; margin-top: 10px"></progress> | |
| <div id="fileList" style="margin-top: 12px"></div> | |
| </article> | |
| </section> | |
| </main> | |
| <script> | |
| const md = window.markdownit({ html: false, breaks: true, linkify: true }); | |
| const form = document.getElementById("cycleForm"); | |
| const runBtn = document.getElementById("runBtn"); | |
| const suggestBtn = document.getElementById("suggestBtn"); | |
| const demoBtn = document.getElementById("demoBtn"); | |
| const kpiCost = document.getElementById("kpiCost"); | |
| const kpiCarbon = document.getElementById("kpiCarbon"); | |
| const kpiReliability = document.getElementById("kpiReliability"); | |
| const dispatchBox = document.getElementById("dispatchBox"); | |
| const validationBox = document.getElementById("validationBox"); | |
| const planBox = document.getElementById("planBox"); | |
| const riskBox = document.getElementById("riskBox"); | |
| const logList = document.getElementById("logList"); | |
| const cycleList = document.getElementById("cycleList"); | |
| const memoryBox = document.getElementById("memoryBox"); | |
| const markdownPreview = document.getElementById("markdownPreview"); | |
| const statGrid = document.getElementById("statGrid"); | |
| const refreshStatsBtn = document.getElementById("refreshStatsBtn"); | |
| const fileInput = document.getElementById("fileInput"); | |
| const uploadBtn = document.getElementById("uploadBtn"); | |
| const uploadProgress = document.getElementById("uploadProgress"); | |
| const fileList = document.getElementById("fileList"); | |
| function uuid() { | |
| if (window.crypto && window.crypto.randomUUID) { | |
| return window.crypto.randomUUID(); | |
| } | |
| return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { | |
| const r = (Math.random() * 16) | 0; | |
| const v = c === "x" ? r : (r & 0x3) | 0x8; | |
| return v.toString(16); | |
| }); | |
| } | |
| function renderMarkdown(target, content) { | |
| target.innerHTML = md.render(content || "暂无内容"); | |
| } | |
| function renderDecision(result) { | |
| const kpi = result.decision?.kpi || {}; | |
| kpiCost.textContent = kpi.cost ?? "-"; | |
| kpiCarbon.textContent = kpi.carbon ?? "-"; | |
| kpiReliability.textContent = kpi.reliability ?? "-"; | |
| const plan = result.decision?.plan || result.plan || {}; | |
| const risk = result.decision?.risk || result.risk || {}; | |
| renderMarkdown(planBox, "#### 推理规划\n\n```json\n" + JSON.stringify(plan, null, 2) + "\n```"); | |
| renderMarkdown(riskBox, "#### 风险评估\n\n```json\n" + JSON.stringify(risk, null, 2) + "\n```"); | |
| const hourly = result.decision?.dispatch?.hourly || []; | |
| if (hourly.length > 0) { | |
| const head = "|小时|负荷|光伏|风电|储能放电|储能充电|并网|需求响应|SOC|电价|\n|---|---|---|---|---|---|---|---|---|---|"; | |
| const rows = hourly | |
| .map( | |
| (h) => | |
| `|${h.hour}|${h.demand_mw}|${h.solar_mw}|${h.wind_mw}|${h.storage_discharge_mw}|${h.storage_charge_mw}|${h.grid_import_mw}|${h.demand_response_mw}|${h.soc}|${h.price}|` | |
| ) | |
| .join("\n"); | |
| renderMarkdown(dispatchBox, "#### 小时级调度表\n\n" + head + "\n" + rows); | |
| } else { | |
| renderMarkdown(dispatchBox, "暂无小时级调度结果"); | |
| } | |
| const validation = result.validation || {}; | |
| const message = validation.passed ? "校验通过" : "校验未通过"; | |
| const issues = (validation.issues || []).join("、"); | |
| renderMarkdown(validationBox, `**${message}**\n\n${issues}`); | |
| renderMarkdown(memoryBox, "```json\n" + JSON.stringify(result.memory || {}, null, 2) + "\n```"); | |
| const notes = result.scenario?.notes || ""; | |
| renderMarkdown(markdownPreview, notes || "暂无备注"); | |
| } | |
| function renderLogs(logs) { | |
| logList.innerHTML = ""; | |
| logs.forEach((item) => { | |
| const wrap = document.createElement("div"); | |
| wrap.className = "log"; | |
| const header = document.createElement("header"); | |
| header.textContent = `${item.role} · ${item.created_at}`; | |
| const body = document.createElement("div"); | |
| body.innerHTML = md.render(item.content || ""); | |
| wrap.appendChild(header); | |
| wrap.appendChild(body); | |
| logList.appendChild(wrap); | |
| }); | |
| } | |
| async function loadCycles() { | |
| const res = await fetch("/api/cycles"); | |
| const data = await res.json(); | |
| cycleList.innerHTML = ""; | |
| data.forEach((cycle) => { | |
| const btn = document.createElement("button"); | |
| btn.type = "button"; | |
| btn.textContent = `${cycle.title} · ${cycle.status}`; | |
| btn.addEventListener("click", async () => { | |
| const detailRes = await fetch(`/api/cycles/${cycle.id}`); | |
| const detail = await detailRes.json(); | |
| renderDecision(detail); | |
| const replayRes = await fetch(`/api/replay/${cycle.id}`); | |
| const replay = await replayRes.json(); | |
| renderLogs(replay); | |
| }); | |
| cycleList.appendChild(btn); | |
| }); | |
| } | |
| async function loadMemory() { | |
| const res = await fetch("/api/memory"); | |
| const data = await res.json(); | |
| renderMarkdown(memoryBox, "```json\n" + JSON.stringify(data || {}, null, 2) + "\n```"); | |
| } | |
| async function loadStats() { | |
| const res = await fetch("/api/stats"); | |
| const data = await res.json(); | |
| statGrid.innerHTML = ""; | |
| const items = [ | |
| { label: "任务总数", value: data.total }, | |
| { label: "完成任务", value: data.completed }, | |
| { label: "平均成本", value: data.avg_cost }, | |
| { label: "平均碳排", value: data.avg_carbon }, | |
| { label: "最近运行", value: data.last_run }, | |
| ]; | |
| items.forEach((item) => { | |
| const card = document.createElement("div"); | |
| card.className = "stat-card"; | |
| const span = document.createElement("span"); | |
| span.textContent = item.label; | |
| const strong = document.createElement("strong"); | |
| strong.textContent = item.value; | |
| card.appendChild(span); | |
| card.appendChild(strong); | |
| statGrid.appendChild(card); | |
| }); | |
| } | |
| async function loadFiles() { | |
| const res = await fetch("/api/files"); | |
| const data = await res.json(); | |
| fileList.innerHTML = ""; | |
| data.forEach((file) => { | |
| const row = document.createElement("div"); | |
| row.className = "log"; | |
| row.innerHTML = `<strong>${file.filename}</strong><br/>大小: ${Math.round( | |
| file.size_bytes / 1024 | |
| )} KB · 二进制: ${file.is_binary ? "是" : "否"}<br/>SHA256: ${file.sha256}`; | |
| fileList.appendChild(row); | |
| }); | |
| } | |
| async function fillSuggestedScenario() { | |
| const res = await fetch("/api/scenario_suggest"); | |
| const data = await res.json(); | |
| Object.keys(data).forEach((key) => { | |
| const field = form.querySelector(`[name="${key}"]`); | |
| if (field) field.value = data[key]; | |
| }); | |
| } | |
| async function runDemo() { | |
| const payload = { | |
| title: "一键演示调度", | |
| demand_mw: 125, | |
| solar_mw: 38, | |
| wind_mw: 26, | |
| storage_mwh: 78, | |
| storage_soc: 0.4, | |
| priority: "可靠性优先", | |
| notes: "演示场景:保障核心负荷、允许有限需求响应。", | |
| }; | |
| const res = await fetch("/api/run_cycle", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| renderDecision(data); | |
| const replayRes = await fetch(`/api/replay/${data.id}`); | |
| renderLogs(await replayRes.json()); | |
| await loadCycles(); | |
| await loadMemory(); | |
| await loadStats(); | |
| } | |
| async function uploadFile(file) { | |
| if (!file) return; | |
| uploadProgress.value = 0; | |
| const chunkSize = 4 * 1024 * 1024; | |
| const totalChunks = Math.ceil(file.size / chunkSize); | |
| if (file.size <= chunkSize) { | |
| const formData = new FormData(); | |
| formData.append("file", file); | |
| const res = await fetch("/api/upload", { method: "POST", body: formData }); | |
| await res.json(); | |
| uploadProgress.value = 100; | |
| await loadFiles(); | |
| return; | |
| } | |
| const fileId = uuid(); | |
| for (let i = 0; i < totalChunks; i++) { | |
| const start = i * chunkSize; | |
| const end = Math.min(file.size, start + chunkSize); | |
| const chunk = file.slice(start, end); | |
| await fetch("/api/upload_chunk", { | |
| method: "POST", | |
| headers: { | |
| "X-File-Id": fileId, | |
| "X-File-Name": file.name, | |
| "X-Chunk-Index": i, | |
| "X-Chunk-Total": totalChunks, | |
| "X-Content-Type": file.type, | |
| }, | |
| body: chunk, | |
| }); | |
| uploadProgress.value = Math.round(((i + 1) / totalChunks) * 100); | |
| } | |
| await loadFiles(); | |
| } | |
| form.addEventListener("submit", async (event) => { | |
| event.preventDefault(); | |
| runBtn.disabled = true; | |
| const payload = Object.fromEntries(new FormData(form).entries()); | |
| const res = await fetch("/api/run_cycle", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload), | |
| }); | |
| const data = await res.json(); | |
| renderDecision(data); | |
| const replayRes = await fetch(`/api/replay/${data.id}`); | |
| renderLogs(await replayRes.json()); | |
| await loadCycles(); | |
| await loadMemory(); | |
| runBtn.disabled = false; | |
| }); | |
| suggestBtn.addEventListener("click", fillSuggestedScenario); | |
| demoBtn.addEventListener("click", runDemo); | |
| refreshStatsBtn.addEventListener("click", loadStats); | |
| uploadBtn.addEventListener("click", () => uploadFile(fileInput.files[0])); | |
| loadCycles(); | |
| loadMemory(); | |
| loadStats(); | |
| loadFiles(); | |
| renderMarkdown(dispatchBox, "等待调度结果"); | |
| renderMarkdown(validationBox, "等待校验结果"); | |
| renderMarkdown(planBox, "等待推理规划"); | |
| renderMarkdown(riskBox, "等待风险评估"); | |
| </script> | |
| </body> | |
| </html> | |