feat: add sproutclaw webui

This commit is contained in:
root
2026-05-10 14:47:51 +08:00
parent e25415dd5f
commit c5295773a1
19 changed files with 6299 additions and 69 deletions

View File

@@ -0,0 +1,177 @@
"use strict";
const systemPromptInput = document.getElementById("systemPromptInput");
const systemPromptPath = document.getElementById("systemPromptPath");
const saveSystemPromptBtn = document.getElementById("saveSystemPromptBtn");
const refreshSettingsBtn = document.getElementById("refreshSettingsBtn");
const settingsStatus = document.getElementById("settingsStatus");
const skillsList = document.getElementById("skillsList");
const skillsCount = document.getElementById("skillsCount");
const extensionsList = document.getElementById("extensionsList");
const extensionsMeta = document.getElementById("extensionsMeta");
const settingsNavBtns = document.querySelectorAll(".settings-nav-btn");
const settingsPanes = document.querySelectorAll(".settings-pane");
const SETTINGS_PANE_KEY = "sproutclaw-settings-pane";
function escHtml(s) {
const div = document.createElement("div");
div.textContent = s;
return div.innerHTML;
}
function setSettingsStatus(text, kind = "") {
if (!settingsStatus) return;
settingsStatus.textContent = text || "";
settingsStatus.className = `settings-status${kind ? ` ${kind}` : ""}`;
}
function showSettingsPane(paneId) {
const id = ["prompt", "skills", "extensions"].includes(paneId) ? paneId : "prompt";
settingsNavBtns.forEach((btn) => {
const active = btn.dataset.pane === id;
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-selected", active ? "true" : "false");
});
settingsPanes.forEach((pane) => {
const active = pane.id === `pane-${id}`;
pane.classList.toggle("is-active", active);
pane.toggleAttribute("hidden", !active);
});
try {
sessionStorage.setItem(SETTINGS_PANE_KEY, id);
} catch {
/* ignore */
}
}
function initSettingsNav() {
settingsNavBtns.forEach((btn) => {
btn.addEventListener("click", () => showSettingsPane(btn.dataset.pane));
});
let initial = "prompt";
try {
const saved = sessionStorage.getItem(SETTINGS_PANE_KEY);
if (["prompt", "skills", "extensions"].includes(saved)) initial = saved;
} catch {
/* ignore */
}
showSettingsPane(initial);
}
async function loadSettings() {
if (skillsList) skillsList.innerHTML = '<div class="settings-empty">加载中...</div>';
if (extensionsList) extensionsList.innerHTML = '<div class="settings-empty">加载中...</div>';
setSettingsStatus("");
try {
const res = await fetch("/api/settings");
const data = await res.json().catch(() => ({}));
if (!res.ok || data.error) throw new Error(data.error || res.statusText);
if (systemPromptInput) systemPromptInput.value = data.systemPrompt || "";
if (systemPromptPath) systemPromptPath.textContent = data.systemPromptPath || "";
renderSkills(data.skills || []);
renderExtensions(data.extensions || [], data.extensionsPath || "");
} catch (err) {
if (skillsList) skillsList.innerHTML = `<div class="settings-empty">加载失败: ${escHtml(err.message)}</div>`;
if (extensionsList) extensionsList.innerHTML = `<div class="settings-empty">加载失败: ${escHtml(err.message)}</div>`;
setSettingsStatus(`加载设置失败: ${err.message}`, "error");
}
}
function renderSkills(skills) {
if (!skillsList) return;
if (skillsCount) skillsCount.textContent = `${skills.length} 个已加载`;
if (!skills.length) {
skillsList.innerHTML = '<div class="settings-empty">暂无已加载 skills</div>';
return;
}
skillsList.innerHTML = skills.map((skill) => {
const meta = [skill.scope, skill.source].filter(Boolean).join(" · ");
const path = skill.path || "";
return `<div class="skill-item">
<div class="skill-title-row">
<div class="skill-name">${escHtml(skill.name || skill.command || "未命名")}</div>
${meta ? `<div class="skill-meta">${escHtml(meta)}</div>` : ""}
</div>
${skill.description ? `<div class="skill-desc">${escHtml(skill.description)}</div>` : ""}
${path ? `<div class="skill-path">${escHtml(path)}</div>` : ""}
</div>`;
}).join("");
}
function renderExtensions(extensions, extensionsPath) {
if (!extensionsList) return;
if (extensionsMeta) {
extensionsMeta.textContent = extensionsPath
? `${extensions.length} 个已加载 · ${extensionsPath}`
: `${extensions.length} 个已加载`;
}
if (!extensions.length) {
extensionsList.innerHTML = '<div class="settings-empty">暂无已加载插件扩展</div>';
return;
}
extensionsList.innerHTML = extensions.map((extension) => {
const meta = extension.kind || [extension.scope, extension.source].filter(Boolean).join(" · ");
const counts = [
[`命令`, extension.commands?.length || 0],
[`工具`, extension.tools?.length || 0],
[`事件`, extension.handlers?.length || 0],
[`Flags`, extension.flags?.length || 0],
[`快捷键`, extension.shortcuts?.length || 0],
].filter(([, count]) => count > 0);
const hasDetails = ["commands", "tools", "handlers", "flags", "shortcuts"].some((key) => extension[key]?.length);
return `<div class="extension-item">
<div class="extension-title-row">
<div class="extension-name">${escHtml(extension.name || extension.path || "未命名")}</div>
${meta ? `<div class="extension-meta">${escHtml(meta)}</div>` : ""}
</div>
${counts.length ? `<div class="extension-counts">${counts.map(([label, count]) => `<span>${escHtml(label)} ${count}</span>`).join("")}</div>` : ""}
${hasDetails ? `<details class="extension-details">
<summary>查看注册项</summary>
${renderNameList("命令", extension.commands)}
${renderNameList("工具", extension.tools)}
${renderNameList("事件", extension.handlers)}
${renderNameList("Flags", extension.flags)}
${renderNameList("快捷键", extension.shortcuts)}
</details>` : ""}
${extension.location ? `<div class="extension-path">${escHtml(extension.location)}</div>` : ""}
</div>`;
}).join("");
}
function renderNameList(label, items) {
if (!Array.isArray(items) || !items.length) return "";
return `<div class="extension-name-list">
<span>${escHtml(label)}</span>
<div>${items.map((item) => `<code>${escHtml(item)}</code>`).join("")}</div>
</div>`;
}
async function saveSystemPrompt() {
if (!systemPromptInput || !saveSystemPromptBtn) return;
saveSystemPromptBtn.disabled = true;
setSettingsStatus("保存中...");
try {
const res = await fetch("/api/settings/system-prompt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ systemPrompt: systemPromptInput.value }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.error) throw new Error(data.error || res.statusText);
if (systemPromptPath && data.systemPromptPath) systemPromptPath.textContent = data.systemPromptPath;
setSettingsStatus("已保存Agent 已重新加载", "ok");
} catch (err) {
setSettingsStatus(`保存失败: ${err.message}`, "error");
} finally {
saveSystemPromptBtn.disabled = false;
}
}
initSettingsNav();
saveSystemPromptBtn?.addEventListener("click", saveSystemPrompt);
refreshSettingsBtn?.addEventListener("click", loadSettings);
document.getElementById("refreshExtensionsBtn")?.addEventListener("click", loadSettings);
loadSettings();