feat: add sproutclaw webui
This commit is contained in:
177
.pi/extensions/webui/public/settings.js
Normal file
177
.pi/extensions/webui/public/settings.js
Normal 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();
|
||||
Reference in New Issue
Block a user