@kevin5251984/guild 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/bin/guildd.mjs +20 -0
- package/cordis.yml +24 -0
- package/package.json +52 -0
- package/src/agent-file.ts +125 -0
- package/src/browser.ts +668 -0
- package/src/catalog/default-bots.ts +263 -0
- package/src/catalog/skills.ts +128 -0
- package/src/catalog/subagents.ts +70 -0
- package/src/chat-parts.ts +71 -0
- package/src/cli-args.ts +75 -0
- package/src/cli.ts +60 -0
- package/src/compact.ts +355 -0
- package/src/cordis.d.ts +40 -0
- package/src/db.ts +653 -0
- package/src/generate.ts +673 -0
- package/src/handlers.ts +1623 -0
- package/src/harness.ts +326 -0
- package/src/host-agents.ts +137 -0
- package/src/host-browse.ts +199 -0
- package/src/host-skills.ts +150 -0
- package/src/image-gen.ts +270 -0
- package/src/index.ts +12 -0
- package/src/llm.ts +993 -0
- package/src/mcp.ts +563 -0
- package/src/memory.ts +159 -0
- package/src/mention.ts +176 -0
- package/src/oauth.ts +1474 -0
- package/src/plugins/api.ts +8 -0
- package/src/plugins/chat.ts +31 -0
- package/src/plugins/harness.ts +77 -0
- package/src/plugins/llm.ts +50 -0
- package/src/plugins/mcp.ts +58 -0
- package/src/plugins/memory.ts +42 -0
- package/src/plugins/oauth.ts +47 -0
- package/src/plugins/server.ts +126 -0
- package/src/plugins/store.ts +29 -0
- package/src/plugins/tools.ts +79 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +3045 -0
- package/src/public/chat.html +5834 -0
- package/src/public/favicon-16.png +0 -0
- package/src/public/favicon-16.svg +10 -0
- package/src/public/favicon-32.png +0 -0
- package/src/public/favicon.ico +0 -0
- package/src/public/favicon.svg +13 -0
- package/src/public/i18n.js +663 -0
- package/src/public/index.html +143 -0
- package/src/public/library.html +678 -0
- package/src/public/mcp-add.html +126 -0
- package/src/public/md.js +332 -0
- package/src/public/rpg/inn-street.jpg +0 -0
- package/src/public/settings.html +795 -0
- package/src/public/skills-add.html +212 -0
- package/src/public/studio.html +1181 -0
- package/src/public/style.css +1678 -0
- package/src/public/subagents-add.html +152 -0
- package/src/router.ts +978 -0
- package/src/send-budget.ts +52 -0
- package/src/server.ts +1 -0
- package/src/skill-import.ts +250 -0
- package/src/slash.ts +15 -0
- package/src/start.ts +103 -0
- package/src/store.ts +1208 -0
- package/src/subagent.ts +355 -0
- package/src/tools.ts +818 -0
- package/src/trajectory.ts +339 -0
- package/src/usage.ts +111 -0
- package/vendor/protocol/package.json +19 -0
- package/vendor/protocol/src/index.ts +159 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-Hant">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
|
7
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
8
|
+
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32" />
|
|
9
|
+
<link rel="icon" href="/favicon-16.png" type="image/png" sizes="16x16" />
|
|
10
|
+
<title>Guild — 編制</title>
|
|
11
|
+
<script src="/i18n.js"></script>
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
|
+
<link
|
|
15
|
+
href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap"
|
|
16
|
+
rel="stylesheet"
|
|
17
|
+
/>
|
|
18
|
+
<link rel="stylesheet" href="/style.css" />
|
|
19
|
+
</head>
|
|
20
|
+
<body class="app bar-page" data-i18n-page="studio">
|
|
21
|
+
<aside class="sidebar">
|
|
22
|
+
<div class="side-brand"><a class="logo" href="/">Guild</a></div>
|
|
23
|
+
<nav class="side-nav">
|
|
24
|
+
<a href="/" data-i18n="nav.chat">大廳</a>
|
|
25
|
+
<div class="side-group">
|
|
26
|
+
<span class="side-group-label" data-i18n="nav.studio">編制</span>
|
|
27
|
+
<a class="side-sub" id="side-hire" href="/studio" data-i18n="studio.hireChip">招募</a>
|
|
28
|
+
<div id="side-roster"></div>
|
|
29
|
+
</div>
|
|
30
|
+
<a href="/library" data-i18n="nav.library">工坊</a>
|
|
31
|
+
<a href="/settings" data-i18n="nav.settings">模型</a>
|
|
32
|
+
</nav>
|
|
33
|
+
<div class="sidebar-resizer" data-i18n-title="sidebar.resize" title="拖曳調整寬度"></div>
|
|
34
|
+
</aside>
|
|
35
|
+
<section class="pane">
|
|
36
|
+
<header class="pane-head">
|
|
37
|
+
<h1 data-i18n="nav.studio">編制</h1>
|
|
38
|
+
<div class="locale-switch">
|
|
39
|
+
<button type="button" data-locale="zh-Hant" data-i18n="lang.zh">中</button>
|
|
40
|
+
<button type="button" data-locale="en" data-i18n="lang.en">EN</button>
|
|
41
|
+
</div>
|
|
42
|
+
</header>
|
|
43
|
+
<div class="page-body">
|
|
44
|
+
<div class="rpg-wrap">
|
|
45
|
+
<div class="rpg-screen" id="rpg-screen" data-scene="street">
|
|
46
|
+
<div class="rpg-scene is-street" id="scene-street">
|
|
47
|
+
<img class="rpg-bg" src="/rpg/inn-street.jpg" alt="Guild 酒館門口" data-i18n-aria="studio.streetAlt" />
|
|
48
|
+
<button type="button" class="rpg-door" id="enter-inn" title="進酒館" data-i18n-title="studio.enter">
|
|
49
|
+
<span class="rpg-arrow" aria-hidden="true"></span>
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="rpg-scene is-inn" id="scene-inn" hidden>
|
|
53
|
+
<div class="inn-map">
|
|
54
|
+
<div class="inn-wall">
|
|
55
|
+
<span class="inn-window"></span>
|
|
56
|
+
<span class="inn-window"></span>
|
|
57
|
+
<span class="inn-window"></span>
|
|
58
|
+
<span class="inn-window"></span>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="inn-nook inn-nook-l">
|
|
61
|
+
<span class="inn-prop plant"></span>
|
|
62
|
+
<span class="inn-prop plant"></span>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="inn-nook inn-nook-r">
|
|
65
|
+
<div class="inn-bar"></div>
|
|
66
|
+
<div class="inn-bottles" id="inn-bottles"></div>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="inn-actors" id="inn-actors"></div>
|
|
69
|
+
</div>
|
|
70
|
+
<button type="button" class="rpg-exit" id="leave-inn" data-i18n="studio.leave">出門</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="rpg-dialog" id="rpg-dialog">
|
|
73
|
+
<div class="rpg-dialog-inner">
|
|
74
|
+
<img class="rpg-face" id="rpg-face" alt="" hidden />
|
|
75
|
+
<div class="rpg-copy">
|
|
76
|
+
<div class="rpg-name" id="rpg-name">酒館</div>
|
|
77
|
+
<p class="rpg-line" id="rpg-line">夜晚的編制還開著。點那扇門進去。</p>
|
|
78
|
+
<div class="rpg-acts" id="rpg-acts"></div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<main class="shell shell-wide" id="hire">
|
|
85
|
+
<p class="eyebrow" id="page-eyebrow">今晚</p>
|
|
86
|
+
<h1 id="page-title">招募冒險者</h1>
|
|
87
|
+
<p class="lede" id="page-lede">寫好 Soul / Agent / Position,勾技能(含本機 Codex、Grok 等 CLI),請他入座。</p>
|
|
88
|
+
<p id="flash" class="muted"></p>
|
|
89
|
+
<form id="create-bot">
|
|
90
|
+
<section class="card">
|
|
91
|
+
<label>Name</label>
|
|
92
|
+
<input name="name" required placeholder="Ada" />
|
|
93
|
+
<label>Handle</label>
|
|
94
|
+
<input name="handle" required placeholder="ada" />
|
|
95
|
+
<label data-i18n="studio.oneLiner">一句話</label>
|
|
96
|
+
<input name="oneLiner" placeholder="這名冒險者在編制裡做什麼" data-i18n-placeholder="studio.oneLinerPh" />
|
|
97
|
+
<div class="card-foot">
|
|
98
|
+
<button type="button" class="ghost danger" id="delete-bot" hidden data-i18n="bot.delete">請這名冒險者離席</button>
|
|
99
|
+
<button type="button" class="ghost" id="gen-look" hidden data-i18n="studio.genLook">生成形象</button>
|
|
100
|
+
<div class="save-row">
|
|
101
|
+
<p class="block-flash" hidden></p>
|
|
102
|
+
<button type="button" class="save-md" data-save="identity" hidden>儲存名字</button>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
</section>
|
|
106
|
+
|
|
107
|
+
<section class="card markdown-block" data-kind="soul">
|
|
108
|
+
<h2>Soul · Markdown</h2>
|
|
109
|
+
<p class="muted" data-i18n="studio.soulHint">性格、聲音、邊界。</p>
|
|
110
|
+
<label data-i18n="studio.desc">描述</label>
|
|
111
|
+
<input class="prompt" placeholder="謹慎、少廢話的資深工程師" data-i18n-placeholder="studio.soulPh" />
|
|
112
|
+
<div class="gen-row">
|
|
113
|
+
<button type="button" class="secondary generate" data-i18n="studio.genMd">用 AI 生成 Markdown</button>
|
|
114
|
+
<p class="block-flash" hidden></p>
|
|
115
|
+
</div>
|
|
116
|
+
<label>SOUL.md</label>
|
|
117
|
+
<textarea class="body" name="soulBody" required placeholder="# Soul"></textarea>
|
|
118
|
+
<input type="hidden" class="gen-name" name="soulName" />
|
|
119
|
+
<div class="save-row">
|
|
120
|
+
<p class="block-flash" hidden></p>
|
|
121
|
+
<button type="button" class="save-md" data-save="soul" hidden>儲存 SOUL.md</button>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
<section class="card markdown-block" data-kind="agent">
|
|
126
|
+
<h2>Agent · Markdown</h2>
|
|
127
|
+
<p class="muted" data-i18n="studio.agentHint">怎麼做事的 SOP。</p>
|
|
128
|
+
<label data-i18n="studio.desc">描述</label>
|
|
129
|
+
<input class="prompt" placeholder="先寫測試,再改最小程式" data-i18n-placeholder="studio.agentPh" />
|
|
130
|
+
<div class="gen-row">
|
|
131
|
+
<button type="button" class="secondary generate" data-i18n="studio.genMd">用 AI 生成 Markdown</button>
|
|
132
|
+
<p class="block-flash" hidden></p>
|
|
133
|
+
</div>
|
|
134
|
+
<label>AGENTS.md</label>
|
|
135
|
+
<textarea class="body" name="agentBody" required placeholder="# Agent"></textarea>
|
|
136
|
+
<input type="hidden" class="gen-name" name="agentName" />
|
|
137
|
+
<div class="save-row">
|
|
138
|
+
<p class="block-flash" hidden></p>
|
|
139
|
+
<button type="button" class="save-md" data-save="agent" hidden>儲存 AGENTS.md</button>
|
|
140
|
+
</div>
|
|
141
|
+
</section>
|
|
142
|
+
|
|
143
|
+
<section class="card markdown-block" data-kind="position">
|
|
144
|
+
<h2>Position · Markdown</h2>
|
|
145
|
+
<p class="muted" data-i18n="studio.posHint">職位職責與完成定義。</p>
|
|
146
|
+
<label data-i18n="studio.desc">描述</label>
|
|
147
|
+
<input class="prompt" placeholder="這個專案的 Engineer" data-i18n-placeholder="studio.posPh" />
|
|
148
|
+
<div class="gen-row">
|
|
149
|
+
<button type="button" class="secondary generate" data-i18n="studio.genMd">用 AI 生成 Markdown</button>
|
|
150
|
+
<p class="block-flash" hidden></p>
|
|
151
|
+
</div>
|
|
152
|
+
<label>POSITION.md</label>
|
|
153
|
+
<textarea class="body" name="positionBody" required placeholder="# Position"></textarea>
|
|
154
|
+
<input type="hidden" class="gen-name" name="positionName" />
|
|
155
|
+
<div class="save-row">
|
|
156
|
+
<p class="block-flash" hidden></p>
|
|
157
|
+
<button type="button" class="save-md" data-save="position" hidden>儲存 POSITION.md</button>
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
<section class="skill-picker">
|
|
162
|
+
<h2 data-i18n="studio.botSkills">這名冒險者的技能(含本機 CLI)</h2>
|
|
163
|
+
<p class="muted" data-i18n-html="studio.botSkillsHint">公會技能庫,加上本機 Claude、Codex、Pi、Grok、Cursor、DSH 的 SKILL.md。勾或取消會立刻存檔,拷進公會再配給這名冒險者。沒有的可到 <a href="/skills/add">追加技能</a> 安裝,不影響其他人。</p>
|
|
164
|
+
<div class="toolbar">
|
|
165
|
+
<div class="search">
|
|
166
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
167
|
+
<circle cx="7" cy="7" r="4.5" stroke="currentColor"/>
|
|
168
|
+
<path d="M10.5 10.5 14 14" stroke="currentColor"/>
|
|
169
|
+
</svg>
|
|
170
|
+
<input id="skill-search" type="search" placeholder="搜尋技能庫" data-i18n-placeholder="studio.searchSkills" />
|
|
171
|
+
</div>
|
|
172
|
+
<button type="button" class="secondary" id="skill-pick" data-i18n="studio.pickSkills">用 AI 挑選</button>
|
|
173
|
+
</div>
|
|
174
|
+
<p id="skill-pick-flash" class="block-flash" hidden></p>
|
|
175
|
+
<div id="tag-row" class="chips tag-row"></div>
|
|
176
|
+
<p class="section-label" id="skill-summary">尚未勾選</p>
|
|
177
|
+
<div id="skill-grid" class="skill-grid"></div>
|
|
178
|
+
</section>
|
|
179
|
+
|
|
180
|
+
<button type="submit" id="submit-btn">請他入座</button>
|
|
181
|
+
</form>
|
|
182
|
+
</main>
|
|
183
|
+
</div>
|
|
184
|
+
</section>
|
|
185
|
+
<script>
|
|
186
|
+
const flash = document.getElementById("flash");
|
|
187
|
+
const form = document.getElementById("create-bot");
|
|
188
|
+
const EDIT_ID = (location.pathname.match(/^\/edit\/([^/]+)$/) || [])[1];
|
|
189
|
+
const COLORS = ["#7c6af7", "#5b8def", "#2ea887", "#e07a3d", "#d4537e"];
|
|
190
|
+
let allSkills = [];
|
|
191
|
+
let selected = new Set();
|
|
192
|
+
let openId = "";
|
|
193
|
+
let activeTags = new Set();
|
|
194
|
+
let showAllSkills = false;
|
|
195
|
+
let rpgTalk = { name: "", line: "", acts: "", face: "" };
|
|
196
|
+
const PICKED = "__picked__";
|
|
197
|
+
const REST_CAP = 12;
|
|
198
|
+
|
|
199
|
+
function applyStudioCopy() {
|
|
200
|
+
const submit = document.getElementById("submit-btn");
|
|
201
|
+
const mdSaves = document.querySelectorAll(".save-md");
|
|
202
|
+
if (EDIT_ID) {
|
|
203
|
+
document.getElementById("page-eyebrow").textContent = t("studio.editEyebrow");
|
|
204
|
+
document.getElementById("page-title").textContent = t("studio.editTitle");
|
|
205
|
+
document.getElementById("page-lede").textContent = t("studio.editLede");
|
|
206
|
+
document.title = t("studio.editDocTitle");
|
|
207
|
+
submit.hidden = true;
|
|
208
|
+
document.getElementById("delete-bot").hidden = false;
|
|
209
|
+
document.getElementById("gen-look").hidden = false;
|
|
210
|
+
document.getElementById("gen-look").textContent = t("studio.genLook");
|
|
211
|
+
mdSaves.forEach((btn) => {
|
|
212
|
+
btn.hidden = false;
|
|
213
|
+
const kind = btn.getAttribute("data-save");
|
|
214
|
+
btn.textContent =
|
|
215
|
+
kind === "identity"
|
|
216
|
+
? t("studio.saveIdentity")
|
|
217
|
+
: t("studio.saveMd", { file: mdFile(kind) });
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
document.getElementById("page-eyebrow").textContent = t("studio.tonight");
|
|
221
|
+
document.getElementById("page-title").textContent = t("studio.hire");
|
|
222
|
+
document.getElementById("page-lede").textContent = t("studio.hireLede");
|
|
223
|
+
submit.hidden = false;
|
|
224
|
+
submit.textContent = t("studio.seat");
|
|
225
|
+
document.getElementById("delete-bot").hidden = true;
|
|
226
|
+
document.getElementById("gen-look").hidden = true;
|
|
227
|
+
document.title = t("title.studio");
|
|
228
|
+
mdSaves.forEach((btn) => {
|
|
229
|
+
btn.hidden = true;
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
document.getElementById("enter-inn").setAttribute("title", t("studio.enter"));
|
|
233
|
+
document.querySelector(".rpg-bg").setAttribute("alt", t("studio.streetAlt"));
|
|
234
|
+
}
|
|
235
|
+
applyStudioCopy();
|
|
236
|
+
|
|
237
|
+
function setFlash(kind, text) {
|
|
238
|
+
flash.className = kind;
|
|
239
|
+
flash.textContent = text;
|
|
240
|
+
}
|
|
241
|
+
function setNodeFlash(el, kind, text) {
|
|
242
|
+
if (!el) {
|
|
243
|
+
setFlash(kind, text);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
el.hidden = !text;
|
|
247
|
+
el.className = "block-flash" + (kind ? " " + kind : "");
|
|
248
|
+
el.textContent = text || "";
|
|
249
|
+
}
|
|
250
|
+
function setBlockFlash(block, kind, text) {
|
|
251
|
+
setNodeFlash(block && block.querySelector(".gen-row .block-flash"), kind, text);
|
|
252
|
+
}
|
|
253
|
+
function beginBusy(button, label) {
|
|
254
|
+
if (!button || button.disabled || button.getAttribute("aria-busy") === "true") {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
button.dataset.idleLabel = button.textContent;
|
|
258
|
+
button.disabled = true;
|
|
259
|
+
button.setAttribute("aria-busy", "true");
|
|
260
|
+
button.classList.add("is-busy");
|
|
261
|
+
if (label) button.textContent = label;
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
function endBusy(button, label) {
|
|
265
|
+
if (!button) return;
|
|
266
|
+
button.disabled = false;
|
|
267
|
+
button.removeAttribute("aria-busy");
|
|
268
|
+
button.classList.remove("is-busy");
|
|
269
|
+
button.textContent = label || button.dataset.idleLabel || button.textContent;
|
|
270
|
+
}
|
|
271
|
+
function saveFlashEl(kind) {
|
|
272
|
+
const btn = document.querySelector('.save-md[data-save="' + kind + '"]');
|
|
273
|
+
return btn && btn.parentElement ? btn.parentElement.querySelector(".block-flash") : null;
|
|
274
|
+
}
|
|
275
|
+
function hue(key) {
|
|
276
|
+
let n = 0;
|
|
277
|
+
for (const ch of key) n = (n + ch.charCodeAt(0) * 13) % COLORS.length;
|
|
278
|
+
return COLORS[n];
|
|
279
|
+
}
|
|
280
|
+
function initials(name) {
|
|
281
|
+
const parts = String(name).trim().split(/[\s-]+/);
|
|
282
|
+
const letters = parts.map((p) => p[0]).join("").slice(0, 2);
|
|
283
|
+
return letters.toUpperCase() || "?";
|
|
284
|
+
}
|
|
285
|
+
function esc(value) {
|
|
286
|
+
return String(value)
|
|
287
|
+
.replace(/&/g, "&")
|
|
288
|
+
.replace(/</g, "<")
|
|
289
|
+
.replace(/>/g, ">")
|
|
290
|
+
.replace(/"/g, """);
|
|
291
|
+
}
|
|
292
|
+
function checkedIds() {
|
|
293
|
+
return selected;
|
|
294
|
+
}
|
|
295
|
+
function skillKeyOf(skill) {
|
|
296
|
+
return String(skill.slug || skill.name || "").toLowerCase();
|
|
297
|
+
}
|
|
298
|
+
function isChecked(skill) {
|
|
299
|
+
if (!skill) return false;
|
|
300
|
+
if (selected.has(skill.id)) return true;
|
|
301
|
+
const key = skillKeyOf(skill);
|
|
302
|
+
if (!key) return false;
|
|
303
|
+
for (const id of selected) {
|
|
304
|
+
const other = allSkills.find((item) => item.id === id);
|
|
305
|
+
if (other && skillKeyOf(other) === key) return true;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
function relatedIds(id) {
|
|
310
|
+
const skill = allSkills.find((item) => item.id === id);
|
|
311
|
+
if (!skill) return [id];
|
|
312
|
+
const key = skillKeyOf(skill);
|
|
313
|
+
const ids = [id];
|
|
314
|
+
if (!key) return ids;
|
|
315
|
+
for (const item of allSkills) {
|
|
316
|
+
if (item.id !== id && skillKeyOf(item) === key) ids.push(item.id);
|
|
317
|
+
}
|
|
318
|
+
for (const sid of selected) {
|
|
319
|
+
if (ids.includes(sid)) continue;
|
|
320
|
+
const other = allSkills.find((item) => item.id === sid);
|
|
321
|
+
if (other && skillKeyOf(other) === key) ids.push(sid);
|
|
322
|
+
}
|
|
323
|
+
return ids;
|
|
324
|
+
}
|
|
325
|
+
function mergeSkills(guild, host) {
|
|
326
|
+
const out = [];
|
|
327
|
+
const slugs = new Set();
|
|
328
|
+
const names = new Set();
|
|
329
|
+
const push = (skill) => {
|
|
330
|
+
if (!skill || !skill.id) return;
|
|
331
|
+
const slug = String(skill.slug || "").toLowerCase();
|
|
332
|
+
const name = String(skill.name || "").toLowerCase();
|
|
333
|
+
if (slug && slugs.has(slug)) return;
|
|
334
|
+
if (name && names.has(name)) return;
|
|
335
|
+
if (slug) slugs.add(slug);
|
|
336
|
+
if (name) names.add(name);
|
|
337
|
+
out.push(skill);
|
|
338
|
+
};
|
|
339
|
+
for (const skill of Array.isArray(guild) ? guild : []) push(skill);
|
|
340
|
+
for (const skill of Array.isArray(host) ? host : []) push(skill);
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
343
|
+
function cardHtml(skill, checked) {
|
|
344
|
+
const desc = skill.description || "";
|
|
345
|
+
const open = openId === skill.id;
|
|
346
|
+
const hostTag = skill.source === "host" ? skill.host : "";
|
|
347
|
+
const pills = (skill.tags || [])
|
|
348
|
+
.filter((tag) => tag !== hostTag)
|
|
349
|
+
.map((tag) => {
|
|
350
|
+
const on = activeTags.has(tag) ? " on" : "";
|
|
351
|
+
return (
|
|
352
|
+
'<button type="button" class="mini-tag' +
|
|
353
|
+
on +
|
|
354
|
+
'" data-tag="' +
|
|
355
|
+
esc(tag) +
|
|
356
|
+
'">' +
|
|
357
|
+
esc(tagLabel(tag)) +
|
|
358
|
+
"</button>"
|
|
359
|
+
);
|
|
360
|
+
})
|
|
361
|
+
.join("");
|
|
362
|
+
const origin =
|
|
363
|
+
skill.source === "host"
|
|
364
|
+
? '<p class="hint" title="' +
|
|
365
|
+
esc(skill.path || "") +
|
|
366
|
+
'">' +
|
|
367
|
+
esc(skill.hostName || skill.host || t("library.local")) +
|
|
368
|
+
"</p>"
|
|
369
|
+
: "";
|
|
370
|
+
return (
|
|
371
|
+
'<article class="lib-card skill-card' +
|
|
372
|
+
(checked ? " on" : "") +
|
|
373
|
+
(open ? " open" : "") +
|
|
374
|
+
'" data-id="' +
|
|
375
|
+
esc(skill.id) +
|
|
376
|
+
'" data-slug="' +
|
|
377
|
+
esc(skill.slug || "") +
|
|
378
|
+
'">' +
|
|
379
|
+
'<div class="skill-ico" style="background:' +
|
|
380
|
+
hue(skill.slug || skill.id) +
|
|
381
|
+
'">' +
|
|
382
|
+
esc(initials(skill.name)) +
|
|
383
|
+
"</div>" +
|
|
384
|
+
'<div class="skill-meta"><strong>' +
|
|
385
|
+
esc(skill.name) +
|
|
386
|
+
'</strong><em class="lib-desc">' +
|
|
387
|
+
esc(desc) +
|
|
388
|
+
"</em>" +
|
|
389
|
+
origin +
|
|
390
|
+
(pills ? '<p class="tag-list">' + pills + "</p>" : "") +
|
|
391
|
+
"</div>" +
|
|
392
|
+
'<button type="button" class="skill-check" data-check="' +
|
|
393
|
+
esc(skill.id) +
|
|
394
|
+
'" aria-pressed="' +
|
|
395
|
+
(checked ? "true" : "false") +
|
|
396
|
+
'" aria-label="' +
|
|
397
|
+
esc(t("studio.checkSkill", { name: skill.name })) +
|
|
398
|
+
'"></button>' +
|
|
399
|
+
(open
|
|
400
|
+
? '<pre class="lib-body">' +
|
|
401
|
+
esc(skill.body || t("studio.skillLoading")) +
|
|
402
|
+
"</pre>"
|
|
403
|
+
: "") +
|
|
404
|
+
"</article>"
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
function visibleSkills() {
|
|
408
|
+
const q = document.getElementById("skill-search").value.toLowerCase();
|
|
409
|
+
const pickedOnly = activeTags.has(PICKED);
|
|
410
|
+
const tags = new Set(
|
|
411
|
+
[...activeTags].filter((tag) => tag && tag !== PICKED),
|
|
412
|
+
);
|
|
413
|
+
return allSkills.filter((skill) => {
|
|
414
|
+
if (pickedOnly && !isChecked(skill)) return false;
|
|
415
|
+
const skillTags = skill.tags || [];
|
|
416
|
+
const tagOk =
|
|
417
|
+
tags.size === 0 || skillTags.some((tag) => tags.has(tag));
|
|
418
|
+
const hay = (
|
|
419
|
+
skill.name +
|
|
420
|
+
" " +
|
|
421
|
+
(skill.description || "") +
|
|
422
|
+
" " +
|
|
423
|
+
(skill.hostName || "") +
|
|
424
|
+
" " +
|
|
425
|
+
(skill.host || "") +
|
|
426
|
+
" " +
|
|
427
|
+
(skill.path || "") +
|
|
428
|
+
" " +
|
|
429
|
+
skillTags.map(tagLabel).join(" ") +
|
|
430
|
+
" " +
|
|
431
|
+
skillTags.join(" ")
|
|
432
|
+
).toLowerCase();
|
|
433
|
+
return tagOk && (!q || hay.includes(q));
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function renderChips() {
|
|
437
|
+
const counts = new Map();
|
|
438
|
+
for (const skill of allSkills) {
|
|
439
|
+
for (const tag of skill.tags || []) {
|
|
440
|
+
counts.set(tag, (counts.get(tag) || 0) + 1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const tags = [...counts.entries()].sort(
|
|
444
|
+
(a, b) => b[1] - a[1] || a[0].localeCompare(b[0]),
|
|
445
|
+
);
|
|
446
|
+
const allOn = activeTags.size === 0;
|
|
447
|
+
const pickedN = allSkills.filter((s) => isChecked(s)).length;
|
|
448
|
+
const pickedOn = activeTags.has(PICKED) && activeTags.size === 1;
|
|
449
|
+
document.getElementById("tag-row").innerHTML =
|
|
450
|
+
'<button type="button" class="chip' +
|
|
451
|
+
(allOn ? " on" : "") +
|
|
452
|
+
'" data-tag="">' +
|
|
453
|
+
t("library.all") +
|
|
454
|
+
"</button>" +
|
|
455
|
+
'<button type="button" class="chip' +
|
|
456
|
+
(pickedOn ? " on" : "") +
|
|
457
|
+
'" data-tag="' +
|
|
458
|
+
PICKED +
|
|
459
|
+
'">' +
|
|
460
|
+
t("studio.pickedChip") +
|
|
461
|
+
' <span class="chip-n">' +
|
|
462
|
+
pickedN +
|
|
463
|
+
"</span></button>" +
|
|
464
|
+
tags
|
|
465
|
+
.map(([tag, n]) => {
|
|
466
|
+
const on = activeTags.has(tag) ? " on" : "";
|
|
467
|
+
return (
|
|
468
|
+
'<button type="button" class="chip' +
|
|
469
|
+
on +
|
|
470
|
+
'" data-tag="' +
|
|
471
|
+
tag +
|
|
472
|
+
'">' +
|
|
473
|
+
tagLabel(tag) +
|
|
474
|
+
' <span class="chip-n">' +
|
|
475
|
+
n +
|
|
476
|
+
"</span></button>"
|
|
477
|
+
);
|
|
478
|
+
})
|
|
479
|
+
.join("");
|
|
480
|
+
}
|
|
481
|
+
function sortPool(list) {
|
|
482
|
+
const q = document.getElementById("skill-search").value.trim();
|
|
483
|
+
return list.slice().sort((a, b) => {
|
|
484
|
+
if (!q) {
|
|
485
|
+
const ac = isChecked(a) ? 0 : 1;
|
|
486
|
+
const bc = isChecked(b) ? 0 : 1;
|
|
487
|
+
if (ac !== bc) return ac - bc;
|
|
488
|
+
}
|
|
489
|
+
return String(a.name).localeCompare(String(b.name), "zh");
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function updateSkillSummary() {
|
|
493
|
+
const guildN = allSkills.filter((s) => s.source !== "host").length;
|
|
494
|
+
const hostN = allSkills.filter((s) => s.source === "host").length;
|
|
495
|
+
const n = allSkills.filter((s) => isChecked(s)).length;
|
|
496
|
+
document.getElementById("skill-summary").textContent = allSkills.length
|
|
497
|
+
? t("studio.picked", { n: n, guild: guildN, host: hostN })
|
|
498
|
+
: t("studio.emptyLib");
|
|
499
|
+
}
|
|
500
|
+
function paintCardCheck(card, on) {
|
|
501
|
+
if (!card) return;
|
|
502
|
+
card.classList.toggle("on", on);
|
|
503
|
+
const btn = card.querySelector("[data-check]");
|
|
504
|
+
if (btn) btn.setAttribute("aria-pressed", on ? "true" : "false");
|
|
505
|
+
}
|
|
506
|
+
function syncCardChecks() {
|
|
507
|
+
document.querySelectorAll(".lib-card.skill-card").forEach((card) => {
|
|
508
|
+
const oldId = card.getAttribute("data-id");
|
|
509
|
+
const slug = String(card.getAttribute("data-slug") || "").toLowerCase();
|
|
510
|
+
let skill =
|
|
511
|
+
allSkills.find((item) => item.id === oldId) ||
|
|
512
|
+
allSkills.find((item) => String(item.slug || "").toLowerCase() === slug);
|
|
513
|
+
if (skill && skill.id !== oldId) {
|
|
514
|
+
card.setAttribute("data-id", skill.id);
|
|
515
|
+
const btn = card.querySelector("[data-check]");
|
|
516
|
+
if (btn) btn.setAttribute("data-check", skill.id);
|
|
517
|
+
if (openId === oldId) openId = skill.id;
|
|
518
|
+
}
|
|
519
|
+
paintCardCheck(card, isChecked(skill || { id: oldId, slug: slug }));
|
|
520
|
+
});
|
|
521
|
+
updateSkillSummary();
|
|
522
|
+
}
|
|
523
|
+
function render() {
|
|
524
|
+
const y = window.scrollY;
|
|
525
|
+
renderChips();
|
|
526
|
+
updateSkillSummary();
|
|
527
|
+
const q = document.getElementById("skill-search").value.trim();
|
|
528
|
+
const filtering = Boolean(q) || activeTags.size > 0;
|
|
529
|
+
let pool = sortPool(visibleSkills());
|
|
530
|
+
let moreHtml = "";
|
|
531
|
+
if (!filtering && !showAllSkills) {
|
|
532
|
+
const checked = pool.filter((s) => isChecked(s));
|
|
533
|
+
const rest = pool.filter((s) => !isChecked(s));
|
|
534
|
+
if (rest.length > REST_CAP) {
|
|
535
|
+
pool = checked.concat(rest.slice(0, REST_CAP));
|
|
536
|
+
moreHtml =
|
|
537
|
+
'<button type="button" class="more" id="skill-more">' +
|
|
538
|
+
t("studio.moreN", { n: rest.length - REST_CAP }) +
|
|
539
|
+
"</button>";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const grid = document.getElementById("skill-grid");
|
|
543
|
+
if (pool.length === 0) {
|
|
544
|
+
grid.innerHTML = '<p class="muted">' + t("studio.noMatch") + "</p>";
|
|
545
|
+
} else {
|
|
546
|
+
grid.innerHTML =
|
|
547
|
+
pool.map((s) => cardHtml(s, isChecked(s))).join("") + moreHtml;
|
|
548
|
+
}
|
|
549
|
+
window.scrollTo(0, y);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function uniquePickIds(ids) {
|
|
553
|
+
const seen = new Set();
|
|
554
|
+
const out = [];
|
|
555
|
+
for (const id of ids) {
|
|
556
|
+
const skill = allSkills.find((item) => item.id === id);
|
|
557
|
+
if (!skill) continue;
|
|
558
|
+
const key = skillKeyOf(skill);
|
|
559
|
+
if (key && seen.has(key)) continue;
|
|
560
|
+
if (key) seen.add(key);
|
|
561
|
+
out.push(id);
|
|
562
|
+
}
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
document.getElementById("skill-pick").addEventListener("click", async () => {
|
|
566
|
+
const button = document.getElementById("skill-pick");
|
|
567
|
+
const flashEl = document.getElementById("skill-pick-flash");
|
|
568
|
+
const soul = form.soulBody.value.trim();
|
|
569
|
+
const agent = form.agentBody.value.trim();
|
|
570
|
+
const position = form.positionBody.value.trim();
|
|
571
|
+
const oneLiner = form.oneLiner.value.trim();
|
|
572
|
+
if (!soul && !agent && !position && !oneLiner) {
|
|
573
|
+
setNodeFlash(flashEl, "error", t("studio.needMdForPick"));
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (!allSkills.length) {
|
|
577
|
+
setNodeFlash(flashEl, "error", t("studio.emptyLib"));
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!beginBusy(button, t("studio.picking"))) return;
|
|
581
|
+
try {
|
|
582
|
+
const response = await fetch("/generate/skills", {
|
|
583
|
+
method: "POST",
|
|
584
|
+
headers: { "content-type": "application/json" },
|
|
585
|
+
body: JSON.stringify({
|
|
586
|
+
name: form.name.value,
|
|
587
|
+
handle: form.handle.value,
|
|
588
|
+
oneLiner: oneLiner,
|
|
589
|
+
soul: soul,
|
|
590
|
+
agent: agent,
|
|
591
|
+
position: position,
|
|
592
|
+
skills: allSkills.map((skill) => ({
|
|
593
|
+
id: skill.id,
|
|
594
|
+
name: skill.name,
|
|
595
|
+
slug: skill.slug || "",
|
|
596
|
+
description: String(skill.description || "").slice(0, 180),
|
|
597
|
+
tags: skill.tags || [],
|
|
598
|
+
})),
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
const body = await response.json();
|
|
602
|
+
if (!response.ok) {
|
|
603
|
+
setNodeFlash(flashEl, "error", body.error || "pick failed");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const ids = uniquePickIds(Array.isArray(body.skillIds) ? body.skillIds : []);
|
|
607
|
+
if (!ids.length) {
|
|
608
|
+
setNodeFlash(flashEl, "error", t("studio.pickNone"));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
selected = new Set(ids);
|
|
612
|
+
activeTags = new Set([PICKED]);
|
|
613
|
+
showAllSkills = false;
|
|
614
|
+
render();
|
|
615
|
+
if (EDIT_ID) {
|
|
616
|
+
const saved = await saveSkills(flashEl);
|
|
617
|
+
if (!saved) return;
|
|
618
|
+
}
|
|
619
|
+
setNodeFlash(
|
|
620
|
+
flashEl,
|
|
621
|
+
"health",
|
|
622
|
+
t("studio.pickedDone", {
|
|
623
|
+
n: allSkills.filter((skill) => isChecked(skill)).length,
|
|
624
|
+
}),
|
|
625
|
+
);
|
|
626
|
+
} catch (err) {
|
|
627
|
+
setNodeFlash(flashEl, "error", err.message);
|
|
628
|
+
} finally {
|
|
629
|
+
endBusy(button, t("studio.pickSkills"));
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
document.getElementById("skill-search").addEventListener("input", () => {
|
|
633
|
+
showAllSkills = false;
|
|
634
|
+
render();
|
|
635
|
+
});
|
|
636
|
+
document.getElementById("skill-search").addEventListener("keydown", (event) => {
|
|
637
|
+
if (event.key === "Enter") event.preventDefault();
|
|
638
|
+
});
|
|
639
|
+
document.getElementById("tag-row").addEventListener("click", (event) => {
|
|
640
|
+
const chip = event.target.closest("[data-tag]");
|
|
641
|
+
if (!chip) return;
|
|
642
|
+
const tag = chip.getAttribute("data-tag");
|
|
643
|
+
showAllSkills = false;
|
|
644
|
+
if (!tag || (activeTags.has(tag) && activeTags.size === 1)) {
|
|
645
|
+
activeTags.clear();
|
|
646
|
+
} else {
|
|
647
|
+
activeTags = new Set(tag ? [tag] : []);
|
|
648
|
+
}
|
|
649
|
+
render();
|
|
650
|
+
});
|
|
651
|
+
document.getElementById("skill-grid").addEventListener("click", (event) => {
|
|
652
|
+
if (event.target.closest("#skill-more")) {
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
showAllSkills = true;
|
|
655
|
+
render();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const mini = event.target.closest(".mini-tag");
|
|
659
|
+
if (mini) {
|
|
660
|
+
event.preventDefault();
|
|
661
|
+
event.stopPropagation();
|
|
662
|
+
const tag = mini.getAttribute("data-tag");
|
|
663
|
+
activeTags =
|
|
664
|
+
tag && activeTags.has(tag) && activeTags.size === 1
|
|
665
|
+
? new Set()
|
|
666
|
+
: new Set(tag ? [tag] : []);
|
|
667
|
+
render();
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const check = event.target.closest("[data-check]");
|
|
671
|
+
if (check) {
|
|
672
|
+
event.preventDefault();
|
|
673
|
+
event.stopPropagation();
|
|
674
|
+
const id = check.getAttribute("data-check");
|
|
675
|
+
if (!id) return;
|
|
676
|
+
const group = relatedIds(id);
|
|
677
|
+
const on = group.some((sid) => selected.has(sid));
|
|
678
|
+
if (on) group.forEach((sid) => selected.delete(sid));
|
|
679
|
+
else selected.add(id);
|
|
680
|
+
if (EDIT_ID && allSkills.filter((s) => isChecked(s)).length === 0) {
|
|
681
|
+
selected.add(id);
|
|
682
|
+
setFlash("error", t("studio.needSkill"));
|
|
683
|
+
paintCardCheck(check.closest(".lib-card"), true);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
paintCardCheck(check.closest(".lib-card"), !on);
|
|
687
|
+
updateSkillSummary();
|
|
688
|
+
if (EDIT_ID) saveSkills();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const card = event.target.closest(".lib-card");
|
|
692
|
+
if (!card) return;
|
|
693
|
+
const id = card.getAttribute("data-id");
|
|
694
|
+
if (!id) return;
|
|
695
|
+
openId = openId === id ? "" : id;
|
|
696
|
+
render();
|
|
697
|
+
const skill = allSkills.find((item) => item.id === id);
|
|
698
|
+
if (openId === id && skill && skill.source === "host" && !skill.body) {
|
|
699
|
+
fetch("/library/skills/host?id=" + encodeURIComponent(id))
|
|
700
|
+
.then((r) => r.json())
|
|
701
|
+
.then((full) => {
|
|
702
|
+
if (!full || !full.body) return;
|
|
703
|
+
skill.body = full.body;
|
|
704
|
+
if (openId === id) render();
|
|
705
|
+
})
|
|
706
|
+
.catch(() => {});
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
document.querySelectorAll(".generate").forEach((button) => {
|
|
711
|
+
button.addEventListener("click", async () => {
|
|
712
|
+
const block = button.closest(".markdown-block");
|
|
713
|
+
const kind = block.getAttribute("data-kind");
|
|
714
|
+
const prompt = block.querySelector(".prompt").value.trim();
|
|
715
|
+
if (!prompt) {
|
|
716
|
+
setBlockFlash(block, "error", t("studio.needPrompt", { kind: kind }));
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!beginBusy(button, t("studio.generating"))) return;
|
|
720
|
+
try {
|
|
721
|
+
const response = await fetch("/generate", {
|
|
722
|
+
method: "POST",
|
|
723
|
+
headers: { "content-type": "application/json" },
|
|
724
|
+
body: JSON.stringify({ kind, prompt }),
|
|
725
|
+
});
|
|
726
|
+
const body = await response.json();
|
|
727
|
+
if (!response.ok) {
|
|
728
|
+
setBlockFlash(block, "error", body.error || "generate failed");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
block.querySelector(".body").value = body.body;
|
|
732
|
+
block.querySelector(".gen-name").value = body.name;
|
|
733
|
+
if (EDIT_ID) {
|
|
734
|
+
const saved = await saveMarkdown(
|
|
735
|
+
kind,
|
|
736
|
+
block.querySelector(".gen-row .block-flash"),
|
|
737
|
+
);
|
|
738
|
+
if (saved) {
|
|
739
|
+
setBlockFlash(
|
|
740
|
+
block,
|
|
741
|
+
"health",
|
|
742
|
+
body.source === "llm"
|
|
743
|
+
? t("studio.generatedSaved", { file: mdFile(kind) })
|
|
744
|
+
: t("studio.generatedSavedLocal", { file: mdFile(kind) }),
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
} else {
|
|
748
|
+
setBlockFlash(
|
|
749
|
+
block,
|
|
750
|
+
"health",
|
|
751
|
+
body.source === "llm"
|
|
752
|
+
? t("studio.generated", { kind: kind })
|
|
753
|
+
: t("studio.generatedLocal", { kind: kind }),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
setBlockFlash(block, "error", err.message);
|
|
758
|
+
} finally {
|
|
759
|
+
endBusy(button, t("studio.genMd"));
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
function mdFile(kind) {
|
|
765
|
+
if (kind === "soul") return "SOUL.md";
|
|
766
|
+
if (kind === "agent") return "AGENTS.md";
|
|
767
|
+
if (kind === "position") return "POSITION.md";
|
|
768
|
+
return kind;
|
|
769
|
+
}
|
|
770
|
+
function mdDraft(kind) {
|
|
771
|
+
const block = document.querySelector('[data-kind="' + kind + '"]');
|
|
772
|
+
if (!block) return null;
|
|
773
|
+
const body = block.querySelector(".body").value;
|
|
774
|
+
const name =
|
|
775
|
+
(block.querySelector(".gen-name") && block.querySelector(".gen-name").value) ||
|
|
776
|
+
block.querySelector(".prompt").value;
|
|
777
|
+
return { name: name, body: body };
|
|
778
|
+
}
|
|
779
|
+
async function patchBot(payload, okText, flashEl) {
|
|
780
|
+
const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID), {
|
|
781
|
+
method: "PATCH",
|
|
782
|
+
headers: { "content-type": "application/json" },
|
|
783
|
+
body: JSON.stringify(payload),
|
|
784
|
+
});
|
|
785
|
+
const body = await response.json();
|
|
786
|
+
if (!response.ok) {
|
|
787
|
+
setNodeFlash(flashEl, "error", body.error || "save failed");
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
setNodeFlash(flashEl, "health", okText);
|
|
791
|
+
return body;
|
|
792
|
+
}
|
|
793
|
+
async function saveMarkdown(kind, flashEl) {
|
|
794
|
+
if (!EDIT_ID) return null;
|
|
795
|
+
const el = flashEl || saveFlashEl(kind);
|
|
796
|
+
if (kind === "identity") {
|
|
797
|
+
return patchBot(
|
|
798
|
+
{
|
|
799
|
+
name: form.name.value,
|
|
800
|
+
handle: form.handle.value,
|
|
801
|
+
oneLiner: form.oneLiner.value,
|
|
802
|
+
},
|
|
803
|
+
t("studio.savedIdentity"),
|
|
804
|
+
el,
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const draft = mdDraft(kind);
|
|
808
|
+
if (!draft || !String(draft.body || "").trim()) {
|
|
809
|
+
setNodeFlash(el, "error", t("studio.needPrompt", { kind: kind }));
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
return patchBot({ [kind]: draft }, t("studio.savedMd", { file: mdFile(kind) }), el);
|
|
813
|
+
}
|
|
814
|
+
document.querySelectorAll(".save-md").forEach((button) => {
|
|
815
|
+
button.addEventListener("click", () => {
|
|
816
|
+
saveMarkdown(button.getAttribute("data-save"));
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
let skillSaveSeq = 0;
|
|
821
|
+
async function saveSkills(flashEl) {
|
|
822
|
+
const ids = [...checkedIds()];
|
|
823
|
+
if (ids.length === 0) {
|
|
824
|
+
setNodeFlash(flashEl, "error", t("studio.needSkill"));
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
const seq = ++skillSaveSeq;
|
|
828
|
+
try {
|
|
829
|
+
const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID), {
|
|
830
|
+
method: "PATCH",
|
|
831
|
+
headers: { "content-type": "application/json" },
|
|
832
|
+
body: JSON.stringify({ skillIds: ids }),
|
|
833
|
+
});
|
|
834
|
+
const body = await response.json();
|
|
835
|
+
if (seq !== skillSaveSeq) return false;
|
|
836
|
+
if (!response.ok) {
|
|
837
|
+
setNodeFlash(flashEl, "error", body.error || "save failed");
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
if (Array.isArray(body.skillIds)) {
|
|
841
|
+
const guild = await fetch("/library/skills")
|
|
842
|
+
.then((r) => r.json())
|
|
843
|
+
.catch(() => []);
|
|
844
|
+
const host = allSkills.filter((item) => item.source === "host");
|
|
845
|
+
allSkills = mergeSkills(Array.isArray(guild) ? guild : [], host);
|
|
846
|
+
selected = new Set(body.skillIds);
|
|
847
|
+
}
|
|
848
|
+
if (!flashEl) setFlash("health", t("studio.skillsSaved"));
|
|
849
|
+
syncCardChecks();
|
|
850
|
+
return true;
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (seq !== skillSaveSeq) return false;
|
|
853
|
+
setNodeFlash(flashEl, "error", err.message);
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
document.getElementById("delete-bot").addEventListener("click", async () => {
|
|
859
|
+
if (!EDIT_ID) return;
|
|
860
|
+
const handle = (form.handle.value || "").replace(/^@/, "");
|
|
861
|
+
if (!confirm(t("bot.deleteConfirm", { handle: handle || EDIT_ID }))) return;
|
|
862
|
+
try {
|
|
863
|
+
const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID), {
|
|
864
|
+
method: "DELETE",
|
|
865
|
+
});
|
|
866
|
+
const body = await response.json().catch(() => ({}));
|
|
867
|
+
if (!response.ok) throw new Error(body.error || t("deleteFailed"));
|
|
868
|
+
window.location.href = "/studio";
|
|
869
|
+
} catch (err) {
|
|
870
|
+
setFlash("error", err.message);
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
form.addEventListener("submit", async (event) => {
|
|
874
|
+
event.preventDefault();
|
|
875
|
+
if (EDIT_ID) {
|
|
876
|
+
saveMarkdown("identity");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const payload = {
|
|
880
|
+
name: form.name.value,
|
|
881
|
+
handle: form.handle.value,
|
|
882
|
+
oneLiner: form.oneLiner.value,
|
|
883
|
+
soul: {
|
|
884
|
+
name: form.soulName.value || form.querySelector('[data-kind="soul"] .prompt').value,
|
|
885
|
+
body: form.soulBody.value,
|
|
886
|
+
},
|
|
887
|
+
agent: {
|
|
888
|
+
name: form.agentName.value || form.querySelector('[data-kind="agent"] .prompt').value,
|
|
889
|
+
body: form.agentBody.value,
|
|
890
|
+
},
|
|
891
|
+
position: {
|
|
892
|
+
name:
|
|
893
|
+
form.positionName.value ||
|
|
894
|
+
form.querySelector('[data-kind="position"] .prompt').value,
|
|
895
|
+
body: form.positionBody.value,
|
|
896
|
+
},
|
|
897
|
+
skillIds: [...checkedIds()],
|
|
898
|
+
};
|
|
899
|
+
const response = await fetch("/bots", {
|
|
900
|
+
method: "POST",
|
|
901
|
+
headers: { "content-type": "application/json" },
|
|
902
|
+
body: JSON.stringify(payload),
|
|
903
|
+
},
|
|
904
|
+
);
|
|
905
|
+
const body = await response.json();
|
|
906
|
+
if (!response.ok) {
|
|
907
|
+
setFlash("error", body.error || "create failed");
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
flash.className = "health";
|
|
911
|
+
const chatHref = "/#d/" + encodeURIComponent(body.id);
|
|
912
|
+
flash.innerHTML = t("studio.seatedHtml", { handle: body.handle, href: chatHref });
|
|
913
|
+
const roster = await fetch("/bots").then((r) => r.json());
|
|
914
|
+
renderBar(Array.isArray(roster) ? roster : []);
|
|
915
|
+
setScene("inn");
|
|
916
|
+
talk(body.name, t("studio.hireToast"), npcActs(body), body.portrait);
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
let barBots = [];
|
|
920
|
+
function setScene(name) {
|
|
921
|
+
document.getElementById("scene-street").hidden = name !== "street";
|
|
922
|
+
document.getElementById("scene-inn").hidden = name !== "inn";
|
|
923
|
+
document.getElementById("rpg-screen").setAttribute("data-scene", name);
|
|
924
|
+
}
|
|
925
|
+
function talk(name, line, acts, face) {
|
|
926
|
+
rpgTalk = { name: name, line: line, acts: acts || "", face: face || "" };
|
|
927
|
+
document.getElementById("rpg-name").textContent = name;
|
|
928
|
+
document.getElementById("rpg-line").textContent = line;
|
|
929
|
+
document.getElementById("rpg-acts").innerHTML = acts || "";
|
|
930
|
+
const img = document.getElementById("rpg-face");
|
|
931
|
+
const url = String(face || "").trim();
|
|
932
|
+
if (url) {
|
|
933
|
+
img.hidden = false;
|
|
934
|
+
img.src = url;
|
|
935
|
+
} else {
|
|
936
|
+
img.removeAttribute("src");
|
|
937
|
+
img.hidden = true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function npcActs(bot) {
|
|
941
|
+
return (
|
|
942
|
+
'<a href="/#d/' +
|
|
943
|
+
encodeURIComponent(bot.id) +
|
|
944
|
+
'">' +
|
|
945
|
+
t("dm") +
|
|
946
|
+
'</a><a href="/edit/' +
|
|
947
|
+
encodeURIComponent(bot.id) +
|
|
948
|
+
'">' +
|
|
949
|
+
t("bot.settings") +
|
|
950
|
+
"</a>"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
function npcSprite(bot) {
|
|
954
|
+
if (bot.portrait) {
|
|
955
|
+
return (
|
|
956
|
+
'<img class="npc-bust" alt="" src="' +
|
|
957
|
+
esc(bot.portrait) +
|
|
958
|
+
'">'
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
return (
|
|
962
|
+
'<span class="npc-blank" aria-hidden="true">' +
|
|
963
|
+
esc(initials(bot.name || bot.handle)) +
|
|
964
|
+
"</span>"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const lookBusy = new Set();
|
|
968
|
+
async function requestLook(id) {
|
|
969
|
+
const bot = barBots.find((item) => item.id === id);
|
|
970
|
+
if (!bot || lookBusy.has(id)) return;
|
|
971
|
+
const formBtn = document.getElementById("gen-look");
|
|
972
|
+
if (formBtn && EDIT_ID === id && !beginBusy(formBtn, t("studio.genLookBusy"))) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
lookBusy.add(id);
|
|
976
|
+
const npc = document.querySelector('.rpg-npc[data-id="' + id + '"]');
|
|
977
|
+
if (npc) npc.classList.add("drawing");
|
|
978
|
+
talk(bot.name, t("studio.genLookBusy"), npcActs(bot), bot.portrait);
|
|
979
|
+
const idFlash = document.querySelector("#create-bot > .card .block-flash");
|
|
980
|
+
if (idFlash && EDIT_ID === id) {
|
|
981
|
+
setNodeFlash(idFlash, "muted", t("studio.genLookBusy"));
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const response = await fetch("/bots/" + encodeURIComponent(id) + "/look", {
|
|
985
|
+
method: "POST",
|
|
986
|
+
});
|
|
987
|
+
const body = await response.json();
|
|
988
|
+
if (!response.ok) {
|
|
989
|
+
talk(bot.name, body.error || t("studio.genLookFail"), npcActs(bot), bot.portrait);
|
|
990
|
+
if (idFlash && EDIT_ID === id) {
|
|
991
|
+
setNodeFlash(idFlash, "error", body.error || t("studio.genLookFail"));
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const i = barBots.findIndex((item) => item.id === id);
|
|
996
|
+
if (i >= 0) barBots[i] = Object.assign({}, barBots[i], body);
|
|
997
|
+
await renderBar(barBots);
|
|
998
|
+
document.querySelectorAll(".rpg-npc").forEach((el) => {
|
|
999
|
+
el.classList.toggle("on", el.getAttribute("data-id") === id);
|
|
1000
|
+
});
|
|
1001
|
+
talk(body.name || bot.name, t("studio.genLookDone"), npcActs(body), body.portrait);
|
|
1002
|
+
if (idFlash && EDIT_ID === id) {
|
|
1003
|
+
setNodeFlash(idFlash, "health", t("studio.genLookDone"));
|
|
1004
|
+
}
|
|
1005
|
+
} catch (err) {
|
|
1006
|
+
talk(bot.name, t("studio.genLookFail"), npcActs(bot), bot.portrait);
|
|
1007
|
+
if (idFlash && EDIT_ID === id) {
|
|
1008
|
+
setNodeFlash(idFlash, "error", t("studio.genLookFail"));
|
|
1009
|
+
}
|
|
1010
|
+
} finally {
|
|
1011
|
+
lookBusy.delete(id);
|
|
1012
|
+
if (npc) npc.classList.remove("drawing");
|
|
1013
|
+
if (formBtn && EDIT_ID === id) endBusy(formBtn, t("studio.genLook"));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function renderSideRoster(bots) {
|
|
1017
|
+
const hire = document.getElementById("side-hire");
|
|
1018
|
+
const box = document.getElementById("side-roster");
|
|
1019
|
+
if (hire) hire.classList.toggle("on", !EDIT_ID);
|
|
1020
|
+
if (!box) return;
|
|
1021
|
+
box.innerHTML = (bots || [])
|
|
1022
|
+
.map((bot) => {
|
|
1023
|
+
const on = EDIT_ID === bot.id ? " on" : "";
|
|
1024
|
+
return (
|
|
1025
|
+
'<a class="side-sub roster-bot' +
|
|
1026
|
+
on +
|
|
1027
|
+
'" href="/edit/' +
|
|
1028
|
+
encodeURIComponent(bot.id) +
|
|
1029
|
+
'"><strong>' +
|
|
1030
|
+
esc(bot.name || bot.handle) +
|
|
1031
|
+
"</strong><em>@" +
|
|
1032
|
+
esc(bot.handle) +
|
|
1033
|
+
"</em></a>"
|
|
1034
|
+
);
|
|
1035
|
+
})
|
|
1036
|
+
.join("");
|
|
1037
|
+
}
|
|
1038
|
+
async function renderBar(bots) {
|
|
1039
|
+
barBots = bots || [];
|
|
1040
|
+
renderSideRoster(barBots);
|
|
1041
|
+
const bottles = ["#c44", "#4a8", "#c84", "#48c", "#a4c", "#cc8", "#844", "#4cc"];
|
|
1042
|
+
document.getElementById("inn-bottles").innerHTML = bottles
|
|
1043
|
+
.map((c) => '<span style="background:' + c + '"></span>')
|
|
1044
|
+
.join("");
|
|
1045
|
+
const diving = {};
|
|
1046
|
+
try {
|
|
1047
|
+
const space = await fetch("/workspace").then((r) => r.json());
|
|
1048
|
+
(space.live || []).forEach((row) => {
|
|
1049
|
+
if (row && row.botId) diving[row.botId] = true;
|
|
1050
|
+
});
|
|
1051
|
+
} catch {
|
|
1052
|
+
/* roster still renders if live is down */
|
|
1053
|
+
}
|
|
1054
|
+
const station = (inner) =>
|
|
1055
|
+
'<div class="inn-station"><span class="inn-desk" aria-hidden="true"><span class="inn-monitor"></span></span>' +
|
|
1056
|
+
inner +
|
|
1057
|
+
"</div>";
|
|
1058
|
+
const nodes = barBots.map((bot) => {
|
|
1059
|
+
const on = EDIT_ID === bot.id ? " on" : "";
|
|
1060
|
+
const live = Boolean(diving[bot.id]);
|
|
1061
|
+
return station(
|
|
1062
|
+
'<button type="button" class="rpg-npc' +
|
|
1063
|
+
on +
|
|
1064
|
+
'" data-id="' +
|
|
1065
|
+
bot.id +
|
|
1066
|
+
'"><span class="npc-state' +
|
|
1067
|
+
(live ? " live" : "") +
|
|
1068
|
+
'">' +
|
|
1069
|
+
esc(live ? t("studio.diving") : t("studio.idle")) +
|
|
1070
|
+
"</span>" +
|
|
1071
|
+
npcSprite(bot) +
|
|
1072
|
+
'<span class="tag">' +
|
|
1073
|
+
esc(bot.name) +
|
|
1074
|
+
"</span></button>",
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
nodes.push(
|
|
1078
|
+
station(
|
|
1079
|
+
'<button type="button" class="rpg-npc vacant" data-id="hire"><span class="npc-state">' +
|
|
1080
|
+
esc(t("studio.idle")) +
|
|
1081
|
+
'</span><span class="npc-blank" aria-hidden="true">?</span><span class="tag">' +
|
|
1082
|
+
t("studio.hireChip") +
|
|
1083
|
+
"</span></button>",
|
|
1084
|
+
),
|
|
1085
|
+
);
|
|
1086
|
+
document.getElementById("inn-actors").innerHTML = nodes.join("");
|
|
1087
|
+
}
|
|
1088
|
+
document.getElementById("enter-inn").addEventListener("click", () => {
|
|
1089
|
+
setScene("inn");
|
|
1090
|
+
talk(t("studio.keeper"), t("studio.keeperLine"), "");
|
|
1091
|
+
});
|
|
1092
|
+
document.getElementById("leave-inn").addEventListener("click", () => {
|
|
1093
|
+
setScene("street");
|
|
1094
|
+
talk(t("studio.inn"), t("studio.streetLine"), "");
|
|
1095
|
+
});
|
|
1096
|
+
document.getElementById("inn-actors").addEventListener("click", (event) => {
|
|
1097
|
+
const npc = event.target.closest(".rpg-npc");
|
|
1098
|
+
if (!npc) return;
|
|
1099
|
+
document.querySelectorAll(".rpg-npc").forEach((el) => {
|
|
1100
|
+
el.classList.toggle("on", el === npc);
|
|
1101
|
+
});
|
|
1102
|
+
const id = npc.getAttribute("data-id");
|
|
1103
|
+
if (id === "hire") {
|
|
1104
|
+
talk(t("studio.vacant"), t("studio.vacantLine"), '<a href="#hire">' + t("studio.hireChip") + "</a>");
|
|
1105
|
+
document.getElementById("hire").hidden = false;
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const bot = barBots.find((item) => item.id === id);
|
|
1109
|
+
if (!bot) return;
|
|
1110
|
+
talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
|
|
1111
|
+
});
|
|
1112
|
+
document.getElementById("rpg-acts").addEventListener("click", (event) => {
|
|
1113
|
+
const link = event.target.closest('a[href="#hire"]');
|
|
1114
|
+
if (link) document.getElementById("hire").hidden = false;
|
|
1115
|
+
const look = event.target.closest("[data-look]");
|
|
1116
|
+
if (look) requestLook(look.getAttribute("data-look"));
|
|
1117
|
+
});
|
|
1118
|
+
document.getElementById("gen-look").addEventListener("click", () => {
|
|
1119
|
+
if (EDIT_ID) requestLook(EDIT_ID);
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
async function boot() {
|
|
1123
|
+
let guild = [];
|
|
1124
|
+
let host = [];
|
|
1125
|
+
try {
|
|
1126
|
+
const [g, h] = await Promise.all([
|
|
1127
|
+
fetch("/library/skills").then((r) => r.json()),
|
|
1128
|
+
fetch("/library/skills/host?body=0").then(async (r) => {
|
|
1129
|
+
if (!r.ok) throw new Error("host skills " + r.status);
|
|
1130
|
+
return r.json();
|
|
1131
|
+
}),
|
|
1132
|
+
]);
|
|
1133
|
+
guild = Array.isArray(g) ? g : [];
|
|
1134
|
+
host = Array.isArray(h) ? h : [];
|
|
1135
|
+
} catch (err) {
|
|
1136
|
+
setFlash("error", t("studio.hostFail") + " " + (err && err.message ? err.message : err));
|
|
1137
|
+
}
|
|
1138
|
+
allSkills = mergeSkills(guild, host);
|
|
1139
|
+
const bots = await fetch("/bots").then((r) => r.json());
|
|
1140
|
+
await renderBar(Array.isArray(bots) ? bots : []);
|
|
1141
|
+
if (location.hash === "#inn") {
|
|
1142
|
+
setScene("inn");
|
|
1143
|
+
talk(t("studio.keeper"), t("studio.keeperLine"), "");
|
|
1144
|
+
}
|
|
1145
|
+
if (EDIT_ID) {
|
|
1146
|
+
const response = await fetch("/bots/" + encodeURIComponent(EDIT_ID));
|
|
1147
|
+
const bot = await response.json();
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
setFlash("error", bot.error || t("studio.notFound"));
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
form.name.value = bot.name;
|
|
1153
|
+
form.handle.value = bot.handle;
|
|
1154
|
+
form.oneLiner.value = bot.oneLiner || "";
|
|
1155
|
+
form.soulBody.value = (bot.soul && bot.soul.body) || "";
|
|
1156
|
+
form.agentBody.value = (bot.agent && bot.agent.body) || "";
|
|
1157
|
+
form.positionBody.value = (bot.position && bot.position.body) || "";
|
|
1158
|
+
if (bot.soul) form.soulName.value = bot.soul.name;
|
|
1159
|
+
if (bot.agent) form.agentName.value = bot.agent.name;
|
|
1160
|
+
if (bot.position) form.positionName.value = bot.position.name;
|
|
1161
|
+
selected = new Set(bot.skillIds || []);
|
|
1162
|
+
setFlash("muted", t("studio.editing", { handle: bot.handle }));
|
|
1163
|
+
setScene("inn");
|
|
1164
|
+
talk(bot.name, bot.oneLiner || t("studio.drink"), npcActs(bot), bot.portrait);
|
|
1165
|
+
} else {
|
|
1166
|
+
setScene("street");
|
|
1167
|
+
document.getElementById("hire").hidden = true;
|
|
1168
|
+
}
|
|
1169
|
+
render();
|
|
1170
|
+
if (!EDIT_ID) talk(t("studio.inn"), t("studio.streetLine"), "");
|
|
1171
|
+
}
|
|
1172
|
+
window.addEventListener("guild-locale", () => {
|
|
1173
|
+
applyStudioCopy();
|
|
1174
|
+
render();
|
|
1175
|
+
renderBar(barBots);
|
|
1176
|
+
if (rpgTalk.name) talk(rpgTalk.name, rpgTalk.line, rpgTalk.acts, rpgTalk.face);
|
|
1177
|
+
});
|
|
1178
|
+
boot().catch((err) => setFlash("error", err.message));
|
|
1179
|
+
</script>
|
|
1180
|
+
</body>
|
|
1181
|
+
</html>
|