117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
"""极简 admin 页:单文件 HTML + 内联 JS,不引框架/CDN。中文文案,2.5s 轮询 stats。"""
|
||
|
||
ADMIN_HTML = """<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Sparkle 引擎控制台</title>
|
||
<style>
|
||
body { font-family: -apple-system, "PingFang SC", sans-serif; max-width: 640px;
|
||
margin: 32px auto; padding: 0 16px; color: #222; }
|
||
h1 { font-size: 20px; }
|
||
section { border: 1px solid #ddd; border-radius: 8px; padding: 16px;
|
||
margin-bottom: 16px; }
|
||
.row { margin: 8px 0; }
|
||
.label { display: inline-block; width: 180px; color: #555; }
|
||
input[type=number] { width: 90px; }
|
||
input[type=range] { width: 240px; vertical-align: middle; }
|
||
button { padding: 6px 20px; }
|
||
#msg { margin-left: 12px; color: #060; }
|
||
.hint { color: #888; font-size: 13px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>Sparkle 引擎控制台</h1>
|
||
|
||
<section>
|
||
<div class="row"><span class="label">引擎状态</span><span id="loaded">-</span></div>
|
||
<div class="row"><span class="label">模型</span><span id="model">-</span></div>
|
||
<div class="row"><span class="label">最近一轮吞吐</span><span id="tps">-</span> tok/s</div>
|
||
<div class="row"><span class="label">进程内存 RSS</span><span id="rss">-</span></div>
|
||
</section>
|
||
|
||
<section>
|
||
<div class="row">
|
||
<span class="label">常驻专家池 expert_slots</span>
|
||
<input type="range" id="slots" min="32" max="160" step="1" value="32">
|
||
<b id="slotsVal">32</b>
|
||
</div>
|
||
<div class="row hint">预估池内存:<span id="poolMem"></span>(slots × ~160MB(g64));修改后引擎后台重建,期间对话不可用</div>
|
||
<div class="row">
|
||
<span class="label">投机宽度 k</span>
|
||
<input type="number" id="k" min="1" max="8" step="1" value="3">
|
||
</div>
|
||
<div class="row">
|
||
<span class="label">最大生成 max_tokens</span>
|
||
<input type="number" id="maxTokens" min="1" max="32768" step="1" value="4096">
|
||
</div>
|
||
<div class="row">
|
||
<button id="apply">应用</button><span id="msg"></span>
|
||
</div>
|
||
</section>
|
||
|
||
<script>
|
||
const $ = id => document.getElementById(id);
|
||
|
||
function fmtBytes(b) {
|
||
if (b >= 1024 * 1024 * 1024) return (b / 1024 / 1024 / 1024).toFixed(2) + " GB";
|
||
return (b / 1024 / 1024).toFixed(0) + " MB";
|
||
}
|
||
|
||
function updateSlots() {
|
||
const v = +$("slots").value;
|
||
$("slotsVal").textContent = v;
|
||
$("poolMem").textContent = " " + fmtBytes(v * 160 * 1024 * 1024) + " ";
|
||
}
|
||
$("slots").addEventListener("input", updateSlots);
|
||
|
||
async function refresh() {
|
||
try {
|
||
const s = await (await fetch("/api/stats")).json();
|
||
$("loaded").textContent = s.loaded ? "已加载" : "未加载 / 重建中";
|
||
$("model").textContent = s.model || "-";
|
||
$("tps").textContent = (s.tok_per_s || 0).toFixed(1);
|
||
$("rss").textContent = fmtBytes(s.rss_bytes || 0);
|
||
if (document.activeElement !== $("slots")) {
|
||
$("slots").value = s.expert_slots;
|
||
updateSlots();
|
||
}
|
||
} catch (e) { /* 服务暂不可达时下轮再试 */ }
|
||
}
|
||
|
||
$("apply").addEventListener("click", async () => {
|
||
const body = {
|
||
expert_slots: +$("slots").value,
|
||
k: +$("k").value,
|
||
max_tokens: +$("maxTokens").value
|
||
};
|
||
try {
|
||
const r = await (await fetch("/api/engine/config", {
|
||
method: "POST",
|
||
headers: {"Content-Type": "application/json"},
|
||
body: JSON.stringify(body)
|
||
})).json();
|
||
$("msg").textContent = r.reloading ? "已应用,引擎正在后台重建…" : "已应用";
|
||
} catch (e) {
|
||
$("msg").textContent = "应用失败";
|
||
}
|
||
setTimeout(() => { $("msg").textContent = ""; }, 4000);
|
||
});
|
||
|
||
(async function init() {
|
||
try {
|
||
const c = await (await fetch("/api/engine/config")).json();
|
||
$("slots").value = c.expert_slots;
|
||
$("k").value = c.k;
|
||
$("maxTokens").value = c.max_tokens;
|
||
} catch (e) { /* 用默认值 */ }
|
||
updateSlots();
|
||
refresh();
|
||
setInterval(refresh, 2500);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|