178 lines
7.2 KiB
JavaScript
178 lines
7.2 KiB
JavaScript
"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();
|