zcbot/web/static/js/skills.js

125 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 技能 modal:两栏 master-detail。左栏「平台 skill / 我的 skill」两组列表,
// 右栏展示选中 skill 的完整 SKILL.md;删除按钮在右栏正文头部(只对「我的」)。
// 左侧 rail 底部「技能」按钮触发。
// 后端:GET /v1/skills(列表)、GET /v1/skills/{name}(正文)、DELETE /v1/skills/{name}(只删 user 源)。
// 创建 / 改 / fork 仍走对话(save_skill / fork_skill / skill-creator)。
import { $ } from "./dom.js";
import { state } from "./state.js";
import { api } from "./api.js";
import { escapeHtml } from "./format.js";
import { renderMd, highlightIn } from "./markdown.js";
const PLACEHOLDER = '<div class="sk-empty">← 从左侧选一个 skill 查看完整说明</div>';
function openSkillsModal() {
$("skills-modal").classList.add("show");
$("sk-detail").innerHTML = PLACEHOLDER;
renderList();
}
export function closeSkillsModal() {
$("skills-modal").classList.remove("show");
}
function itemHtml(s) {
const badge = s.overrides_builtin
? ' <span class="sk-badge">已覆盖平台同名</span>'
: "";
return `<div class="sk-item" data-name="${escapeHtml(s.name)}">
<div class="sk-name">${escapeHtml(s.name)}${badge}</div>
<div class="sk-desc">${escapeHtml(s.description || "")}</div>
</div>`;
}
async function renderList() {
const colB = $("sk-builtin");
const colU = $("sk-user");
colB.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
colU.innerHTML = "";
let data;
try {
data = await api("GET", "/v1/skills");
} catch (e) {
colB.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
return;
}
state.skills = data.skills || []; // 顺手刷新新建任务下拉的缓存
const platform = state.skills.filter((s) => s.source === "builtin");
const mine = state.skills.filter((s) => s.source === "user");
colB.innerHTML =
`<div class="sk-group-title">平台 skill (${platform.length})</div>` +
(platform.map(itemHtml).join("") ||
'<div class="muted" style="padding:4px 8px;">(无)</div>');
let userHtml = `<div class="sk-group-title">我的 skill (${mine.length})</div>`;
userHtml += mine.length
? mine.map(itemHtml).join("")
: '<div class="muted" style="padding:4px 8px;font-size:12px;">还没有。让助手「帮我做个 skill」或「把某个平台 skill fork 成我的」即可创建。</div>';
if (data.load_errors && data.load_errors.length) {
const errs = data.load_errors
.map((e) => `${escapeHtml(e.name)}(${escapeHtml(e.reason)})`)
.join("");
userHtml += `<div class="sk-loaderr">⚠ ${data.load_errors.length} 个未加载:${errs}</div>`;
}
colU.innerHTML = userHtml;
}
async function selectSkill(name, itemEl) {
// 跨两个列表栏清高亮
$("sk-cols").querySelectorAll(".sk-item.active").forEach((el) => el.classList.remove("active"));
if (itemEl) itemEl.classList.add("active");
const detail = $("sk-detail");
detail.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
let data;
try {
data = await api("GET", "/v1/skills/" + encodeURIComponent(name));
} catch (e) {
detail.innerHTML = `<div class="err" style="padding:8px;">加载失败: ${escapeHtml(e.message)}</div>`;
return;
}
const isUser = data.source === "user";
const srcBadge = isUser
? `<span class="sk-badge">${data.overrides_builtin ? "我的 · 已覆盖平台" : "我的"}</span>`
: `<span class="sk-badge">平台</span>`;
const delBtn = isUser
? `<button class="sk-del small danger" data-del="${escapeHtml(data.name)}" title="删除我的这个 skill">删除</button>`
: "";
detail.innerHTML =
`<div class="sk-d-head"><span class="sk-d-name">${escapeHtml(data.name)}</span>${srcBadge}<span class="spacer"></span>${delBtn}</div>` +
`<div class="sk-detail-md">${renderMd(data.content)}</div>`;
highlightIn(detail);
}
// ───── 顶层绑定 ─────
$("hd-skills").onclick = openSkillsModal;
$("sk-close").onclick = closeSkillsModal;
$("skills-modal").addEventListener("click", (e) => {
if (e.target.id === "skills-modal") closeSkillsModal(); // 点遮罩关闭
});
// 列表栏(平台 + 我的):点 item 选中 → 右栏载正文。委托到 #sk-cols,
// detail 栏的点击不含 .sk-item 故不误触(删除按钮另由下方 #sk-detail 监听处理)。
$("sk-cols").addEventListener("click", (e) => {
const item = e.target.closest(".sk-item");
if (item) selectSkill(item.getAttribute("data-name"), item);
});
// 右栏:删除(冒泡到 [data-del])
$("sk-detail").addEventListener("click", async (e) => {
const del = e.target.closest("[data-del]");
if (!del) return;
const name = del.getAttribute("data-del");
if (!confirm(`删除你的 skill「${name}」?不可撤销(平台同名 skill 不受影响)。`)) return;
del.disabled = true;
try {
await api("DELETE", "/v1/skills/" + encodeURIComponent(name));
state.skills = null; // 失效缓存,新建任务下拉下次重拉
$("sk-detail").innerHTML = PLACEHOLDER;
renderList();
} catch (err) {
alert("删除失败: " + err.message);
del.disabled = false;
}
});