123 lines
4.9 KiB
JavaScript
123 lines
4.9 KiB
JavaScript
// 技能 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 list = $("sk-list");
|
||
list.innerHTML = '<div class="muted" style="padding:8px;">加载中…</div>';
|
||
let data;
|
||
try {
|
||
data = await api("GET", "/v1/skills");
|
||
} catch (e) {
|
||
list.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");
|
||
|
||
let html = "";
|
||
html += `<div class="sk-group-title">平台 skill (${platform.length})</div>`;
|
||
html +=
|
||
platform.map(itemHtml).join("") ||
|
||
'<div class="muted" style="padding:4px 8px;">(无)</div>';
|
||
html += `<div class="sk-group-title mt">我的 skill (${mine.length})</div>`;
|
||
html += 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(";");
|
||
html += `<div class="sk-loaderr">⚠ ${data.load_errors.length} 个 skill 因格式问题未加载:${errs}</div>`;
|
||
}
|
||
list.innerHTML = html;
|
||
}
|
||
|
||
async function selectSkill(name, itemEl) {
|
||
// 左栏高亮
|
||
$("sk-list").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-list").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;
|
||
}
|
||
});
|