@songsid/agend 2.0.11-beta.30 → 2.0.11-beta.32
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/dist/channel/adapters/discord.js +16 -15
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/fleet-manager.d.ts +2 -0
- package/dist/fleet-manager.js +100 -74
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.d.ts +19 -0
- package/dist/locale.js +137 -0
- package/dist/locale.js.map +1 -0
- package/dist/quickstart.js +7 -0
- package/dist/quickstart.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +1 -1
- package/dist/scheduler/scheduler.js +15 -4
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/settings-api.d.ts +30 -0
- package/dist/settings-api.js +48 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/topic-commands.js +18 -17
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/settings.html +236 -44
- package/dist/ui/view.html +60 -9
- package/package.json +1 -1
package/dist/ui/settings.html
CHANGED
|
@@ -72,35 +72,70 @@
|
|
|
72
72
|
textarea { width: 100%; min-height: 380px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px; color: var(--fg); background: #fff; resize: vertical; }
|
|
73
73
|
pre.preview { background: #f9fafb; border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; overflow: auto; max-height: 480px; white-space: pre; min-height: 380px; }
|
|
74
74
|
.banner { display: none; padding: 10px 24px; font-size: 13px; background: #ecfdf5; color: var(--green); border-bottom: 1px solid var(--border); }
|
|
75
|
+
.help-btn { width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: #f3f4f6; cursor: pointer; font-size: 16px; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
76
|
+
.help-btn:hover { background: var(--accent); border-color: var(--accent); }
|
|
77
|
+
.help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.3); display: none; z-index: 40; }
|
|
78
|
+
.help-backdrop.open { display: block; }
|
|
79
|
+
.help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 380px; max-width: 90vw; background: var(--bg); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.12); transform: translateX(100%); transition: transform .2s ease; z-index: 50; overflow-y: auto; padding: 20px 22px; }
|
|
80
|
+
.help-panel.open { transform: translateX(0); }
|
|
81
|
+
.help-panel h3 { font-size: 16px; margin-bottom: 8px; }
|
|
82
|
+
.help-panel h4 { font-size: 14px; margin: 16px 0 6px; color: var(--accent); }
|
|
83
|
+
.help-panel ul { margin: 0 0 8px 18px; }
|
|
84
|
+
.help-panel li { font-size: 13px; margin: 3px 0; }
|
|
85
|
+
.help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--muted); font-size: 22px; line-height: 1; background: none; border: none; }
|
|
86
|
+
.help-panel .tip { margin-top: 16px; padding: 10px 12px; background: var(--card); border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 13px; }
|
|
75
87
|
</style>
|
|
76
88
|
</head>
|
|
77
89
|
<body>
|
|
78
90
|
<header>
|
|
79
|
-
<h1>⚙️ AgEnD Settings</h1>
|
|
91
|
+
<h1 data-i18n="title">⚙️ AgEnD Settings</h1>
|
|
92
|
+
<span id="verBadge" title="What's New" style="font-size:12px;color:var(--muted);cursor:pointer;padding:2px 8px;border:1px solid var(--border);border-radius:12px">v…</span>
|
|
80
93
|
<div class="grow"></div>
|
|
81
|
-
<button
|
|
94
|
+
<button class="help-btn" id="langBtn" title="Language">🌐</button>
|
|
95
|
+
<button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
|
|
96
|
+
<button id="reloadBtn" class="primary" data-i18n="reloadFleet" title="Apply saved config to running instances (SIGHUP)">↻ Reload Fleet</button>
|
|
82
97
|
</header>
|
|
98
|
+
<div class="help-backdrop" id="helpBackdrop"></div>
|
|
99
|
+
<aside class="help-panel" id="helpPanel">
|
|
100
|
+
<button class="close" id="helpClose" title="Close">×</button>
|
|
101
|
+
<div id="helpContent"></div>
|
|
102
|
+
</aside>
|
|
83
103
|
<div class="banner" id="banner"></div>
|
|
84
104
|
<main>
|
|
85
105
|
<section>
|
|
86
|
-
<div class="sec-head"><h2>🤖 My Agents</h2><div class="grow"></div><input type="text" id="agentSearch" placeholder="🔍 Search…" style="width:180px;margin-right:8px"><button class="sm" id="newAgentBtn">+ New</button></div>
|
|
106
|
+
<div class="sec-head"><h2 data-i18n="myAgents">🤖 My Agents</h2><div class="grow"></div><input type="text" id="agentSearch" data-i18n-ph="searchPh" placeholder="🔍 Search…" style="width:180px;margin-right:8px"><button class="sm" id="newAgentBtn" data-i18n="new">+ New</button></div>
|
|
87
107
|
<div id="newAgent"></div>
|
|
88
108
|
<div class="card" id="agents"></div>
|
|
89
109
|
</section>
|
|
90
110
|
|
|
111
|
+
<section id="classicSection">
|
|
112
|
+
<div class="sec-head"><h2 data-i18n="classicChannels">🎮 ClassicBot Channels</h2></div>
|
|
113
|
+
<div class="card" id="classicList"></div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
91
116
|
<section>
|
|
92
|
-
<div class="sec-head"><h2>🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn">+ Add</button></div>
|
|
117
|
+
<div class="sec-head"><h2 data-i18n="bots">🔌 Bots</h2><div class="grow"></div><button class="sm" id="newBotBtn" data-i18n="add">+ Add</button></div>
|
|
93
118
|
<div id="newBot"></div>
|
|
94
119
|
<div class="card" id="bots"></div>
|
|
95
120
|
</section>
|
|
96
121
|
|
|
97
122
|
<section>
|
|
98
|
-
<div class="sec-head"><h2>⚙️ General Settings</h2></div>
|
|
123
|
+
<div class="sec-head"><h2 data-i18n="general">⚙️ General Settings</h2></div>
|
|
99
124
|
<div class="card" id="general"></div>
|
|
100
125
|
</section>
|
|
101
126
|
|
|
102
127
|
<section>
|
|
103
|
-
<div class="sec-head"><h2
|
|
128
|
+
<div class="sec-head"><h2 data-i18n="autoUpdate">🔄 Auto Update</h2></div>
|
|
129
|
+
<div class="card" id="autoUpdate"></div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<section>
|
|
133
|
+
<div class="sec-head"><h2 data-i18n="whatsNew">🆕 What's New</h2><div class="grow"></div><button class="sm" id="wnToggle" data-i18n="expand">▶ Expand</button></div>
|
|
134
|
+
<div class="card"><div class="adv-body" id="wnBody"></div></div>
|
|
135
|
+
</section>
|
|
136
|
+
|
|
137
|
+
<section>
|
|
138
|
+
<div class="sec-head"><h2 data-i18n="advanced">📋 Advanced</h2><div class="grow"></div><button class="sm" id="advToggle" data-i18n="expand">▶ Expand</button></div>
|
|
104
139
|
<div class="card"><div class="adv-body" id="advBody"></div></div>
|
|
105
140
|
</section>
|
|
106
141
|
</main>
|
|
@@ -112,6 +147,83 @@
|
|
|
112
147
|
const el = (tag, attrs = {}, ...kids) => { const e = document.createElement(tag); for (const k in attrs) { if (k === "class") e.className = attrs[k]; else if (k === "html") e.innerHTML = attrs[k]; else if (k.startsWith("on")) e.addEventListener(k.slice(2), attrs[k]); else e.setAttribute(k, attrs[k]); } for (const c of kids) if (c != null) e.append(c); return e; };
|
|
113
148
|
const esc = (s) => String(s).replace(/[&<>]/g, c => ({ "&":"&","<":"<",">":">" }[c]));
|
|
114
149
|
const BACKENDS = ["claude-code", "kiro-cli", "codex", "opencode", "gemini-cli", "antigravity"];
|
|
150
|
+
|
|
151
|
+
// ── i18n ──
|
|
152
|
+
const I18N = {
|
|
153
|
+
en: {
|
|
154
|
+
title: "⚙️ AgEnD Settings", reloadFleet: "↻ Reload Fleet", myAgents: "🤖 My Agents", bots: "🔌 Bots", classicChannels: "🎮 ClassicBot Channels", noClassic: "No ClassicBot channels — start one with /start in a chat.",
|
|
155
|
+
general: "⚙️ General Settings", autoUpdate: "🔄 Auto Update", whatsNew: "🆕 What's New", advanced: "📋 Advanced",
|
|
156
|
+
expand: "▶ Expand", collapse: "▼ Collapse", new: "+ New", add: "+ Add", searchPh: "🔍 Search…",
|
|
157
|
+
edit: "Edit", close: "Close", save: "Save", cancel: "Cancel", create: "Create", start: "Start", stop: "Stop",
|
|
158
|
+
backend: "Backend", model: "Model", workingDir: "Working directory", channelBinding: "Channel binding",
|
|
159
|
+
description: "Description", systemPrompt: "System prompt", tags: "Tags", generalTopicField: "General topic (fleet dispatcher)",
|
|
160
|
+
language: "Language (bot replies)", defaultBackend: "Default backend", defaultModel: "Default model", allowedUsers: "Allowed users (primary bot)",
|
|
161
|
+
classicAdmins: "ClassicBot admins", allowedGuilds: "Allowed guilds", type: "Type", adapterId: "Adapter id",
|
|
162
|
+
tokenEnv: "Bot token env var", groupId: "Group / guild id", nameField: "Name",
|
|
163
|
+
envHint: "Put the actual token in ~/.agend/.env under this env var, then Reload Fleet.",
|
|
164
|
+
noAgents: "No agents yet — click + New.", noBots: "No bots configured.", online: "✅ Online",
|
|
165
|
+
enable: "Enabled", schedule: "Schedule", channel: "Channel", daily: "Daily", weekly: "Weekly", manual: "Manual (off)",
|
|
166
|
+
beta: "Beta", stable: "Stable", currentVersion: "Current version", lastChecked: "Last checked", never: "never",
|
|
167
|
+
helpTitle: "📖 Help", tipReload: "💡 Click [↻ Reload Fleet] after changes to apply them.",
|
|
168
|
+
wnIntro: "Recent highlights:",
|
|
169
|
+
},
|
|
170
|
+
"zh-TW": {
|
|
171
|
+
title: "⚙️ AgEnD 設定", reloadFleet: "↻ 重新載入", myAgents: "🤖 我的 Agent", bots: "🔌 機器人", classicChannels: "🎮 ClassicBot 頻道", noClassic: "沒有 ClassicBot 頻道 — 在聊天中用 /start 建立。",
|
|
172
|
+
general: "⚙️ 一般設定", autoUpdate: "🔄 自動更新", whatsNew: "🆕 更新重點", advanced: "📋 進階",
|
|
173
|
+
expand: "▶ 展開", collapse: "▼ 收合", new: "+ 新增", add: "+ 加入", searchPh: "🔍 搜尋…",
|
|
174
|
+
edit: "編輯", close: "關閉", save: "儲存", cancel: "取消", create: "建立", start: "啟動", stop: "停止",
|
|
175
|
+
backend: "後端", model: "模型", workingDir: "工作目錄", channelBinding: "頻道綁定",
|
|
176
|
+
description: "描述", systemPrompt: "System prompt", tags: "標籤", generalTopicField: "General topic(fleet 分派器)",
|
|
177
|
+
language: "語言(bot 回覆)", defaultBackend: "預設後端", defaultModel: "預設模型", allowedUsers: "允許使用者(主 bot)",
|
|
178
|
+
classicAdmins: "ClassicBot 管理員", allowedGuilds: "允許的伺服器", type: "類型", adapterId: "Adapter id",
|
|
179
|
+
tokenEnv: "Bot token 環境變數", groupId: "群組 / guild id", nameField: "名稱",
|
|
180
|
+
envHint: "把實際 token 放到 ~/.agend/.env 的這個環境變數下,然後點重新載入。",
|
|
181
|
+
noAgents: "還沒有 agent — 點 + 新增。", noBots: "尚未設定機器人。", online: "✅ 上線",
|
|
182
|
+
enable: "啟用", schedule: "排程", channel: "頻道", daily: "每日", weekly: "每週", manual: "手動(關閉)",
|
|
183
|
+
beta: "Beta", stable: "穩定版", currentVersion: "目前版本", lastChecked: "上次檢查", never: "從未",
|
|
184
|
+
helpTitle: "📖 使用說明", tipReload: "💡 修改後點 [↻ 重新載入] 讓設定生效。",
|
|
185
|
+
wnIntro: "近期更新重點:",
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const HELP = {
|
|
189
|
+
en: [["🤖 My Agents", ["[+ New] add an agent (name, backend, working directory)", "[Edit] change settings (backend, model, tags, systemPrompt)", "[Start/Stop] run or stop an agent", "🗑️ delete an agent (shows on hover, needs confirm)", "🔍 search — live-filter the list"]],
|
|
190
|
+
["🔌 Bots", ["[+ Add] add a Discord/TG bot", "🗑️ remove a bot"]],
|
|
191
|
+
["⚙️ General Settings", ["Edit global backend/model, allowed users, ClassicBot admins", "Click [Save]"]],
|
|
192
|
+
["🔄 Auto Update", ["Enable a scheduled `agend update` (daily/weekly)", "Pick beta or stable channel"]],
|
|
193
|
+
["📋 Advanced", ["Edit YAML/JSON directly", "[⬇ fleet.yaml] download a backup"]]],
|
|
194
|
+
"zh-TW": [["🤖 我的 Agent", ["[+ 新增] 新增 agent(name、backend、working directory)", "[編輯] 修改設定(backend、model、tags、systemPrompt)", "[啟動/停止] 啟動或停止 agent", "🗑️ 刪除 agent(hover 顯示,需確認)", "🔍 搜尋 — 即時過濾列表"]],
|
|
195
|
+
["🔌 機器人", ["[+ 加入] 加入 Discord/TG bot", "🗑️ 移除 bot"]],
|
|
196
|
+
["⚙️ 一般設定", ["修改全域 backend/model、允許使用者、ClassicBot 管理員", "點 [儲存]"]],
|
|
197
|
+
["🔄 自動更新", ["啟用排程 `agend update`(每日/每週)", "選 beta 或穩定版頻道"]],
|
|
198
|
+
["📋 進階", ["直接編輯 YAML/JSON", "[⬇ fleet.yaml] 下載備份"]]],
|
|
199
|
+
};
|
|
200
|
+
const WHATSNEW = [
|
|
201
|
+
"Settings redesign — user-flow sections, white theme, i18n (EN / 繁中)",
|
|
202
|
+
"Auto Update scheduler + What's New + info help panel",
|
|
203
|
+
"Same-channel multi-bot ClassicBot; per-bot cancel-button + react fixes",
|
|
204
|
+
"/view avatar & ctx% fixes; config validator + `agend validate` + validate_config MCP tool",
|
|
205
|
+
];
|
|
206
|
+
let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
|
|
207
|
+
if (!I18N[lang]) lang = "en";
|
|
208
|
+
const t = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
|
|
209
|
+
function applyStaticI18n() {
|
|
210
|
+
document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = t(e.getAttribute("data-i18n")); });
|
|
211
|
+
document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", t(e.getAttribute("data-i18n-ph"))); });
|
|
212
|
+
// toggle labels that carry expand/collapse state
|
|
213
|
+
for (const [id, body] of [["advToggle", "advBody"], ["wnToggle", "wnBody"]]) {
|
|
214
|
+
const b = $(body); if (b) $(id).textContent = b.classList.contains("open") ? t("collapse") : t("expand");
|
|
215
|
+
}
|
|
216
|
+
renderHelp();
|
|
217
|
+
}
|
|
218
|
+
function renderHelp() {
|
|
219
|
+
const host = $("helpContent"); if (!host) return; host.innerHTML = "";
|
|
220
|
+
host.append(el("h3", {}, t("helpTitle")));
|
|
221
|
+
for (const [h, items] of (HELP[lang] || HELP.en)) {
|
|
222
|
+
host.append(el("h4", {}, h));
|
|
223
|
+
const ul = el("ul"); for (const it of items) ul.append(el("li", {}, it)); host.append(ul);
|
|
224
|
+
}
|
|
225
|
+
host.append(el("div", { class: "tip" }, t("tipReload")));
|
|
226
|
+
}
|
|
115
227
|
const CH_TYPES = ["discord", "telegram"];
|
|
116
228
|
|
|
117
229
|
// ── YAML (config subset; round-trip verified) ──
|
|
@@ -150,7 +262,7 @@
|
|
|
150
262
|
}
|
|
151
263
|
|
|
152
264
|
// ── State ──
|
|
153
|
-
const state = { fleet: {}, classic: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set() };
|
|
265
|
+
const state = { fleet: {}, classic: {}, classicStatus: {}, live: {}, fleetUp: false, advFmt: "yaml", advEdit: false, collapsedGroups: new Set(), updateSchedule: null, version: "", newAgentOpen: false, newBotOpen: false };
|
|
154
266
|
const channels = () => state.fleet.channels || (state.fleet.channel ? [state.fleet.channel] : []);
|
|
155
267
|
const channelIds = () => channels().map(c => c.id || c.type).filter(Boolean);
|
|
156
268
|
const chLabel = (i) => i === 0 ? "primary" : "persona";
|
|
@@ -195,9 +307,9 @@
|
|
|
195
307
|
el("span", { class: "sub", style: "margin:0" }, inst.backend || state.fleet.defaults?.backend || "claude-code"),
|
|
196
308
|
chTag ? el("span", { class: "tag" + (chTag.includes("persona") ? " persona" : chTag.includes("telegram") ? " tg" : "") }, chTag) : null,
|
|
197
309
|
el("span", { class: "spacer" }),
|
|
198
|
-
el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? "
|
|
199
|
-
el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? "
|
|
200
|
-
el("button", { class: "sm danger del-btn", title: "
|
|
310
|
+
el("button", { class: "sm ghost", onclick: () => { editing = editing === name ? null : name; renderAgents(); } }, editing === name ? t("close") : t("edit")),
|
|
311
|
+
el("button", { class: "sm", onclick: () => toggleAgent(name, running) }, running ? t("stop") : t("start")),
|
|
312
|
+
el("button", { class: "sm danger del-btn", title: t("close"), onclick: () => delAgent(name) }, "🗑️"),
|
|
201
313
|
));
|
|
202
314
|
if (editing === name) item.append(agentEditForm(name, inst));
|
|
203
315
|
return item;
|
|
@@ -205,7 +317,7 @@
|
|
|
205
317
|
function renderAgents() {
|
|
206
318
|
const host = $("agents"); host.innerHTML = "";
|
|
207
319
|
const insts = state.fleet.instances || {};
|
|
208
|
-
if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = "
|
|
320
|
+
if (!Object.keys(insts).length) { host.className = "card empty"; host.textContent = t("noAgents"); return; }
|
|
209
321
|
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
210
322
|
let names = Object.keys(insts);
|
|
211
323
|
if (filter) names = names.filter(n => n.toLowerCase().includes(filter) || shortName(n, insts[n]).toLowerCase().includes(filter));
|
|
@@ -241,12 +353,12 @@
|
|
|
241
353
|
const msg = el("span", { class: "msg" });
|
|
242
354
|
box.append(
|
|
243
355
|
el("div", { class: "sub", style: "margin:0 0 8px" }, "ID: " + name),
|
|
244
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
245
|
-
el("label", {}, "
|
|
246
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
247
|
-
el("label", {}, "
|
|
248
|
-
el("label", {}, "
|
|
249
|
-
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {},
|
|
356
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("backend")), fBackend), el("div", {}, el("label", {}, t("model")), fModel)),
|
|
357
|
+
el("label", {}, t("workingDir")), fWd,
|
|
358
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("channelBinding")), fChan), el("div", {}, el("label", {}, t("description")), fDesc)),
|
|
359
|
+
el("label", {}, t("systemPrompt")), fSys,
|
|
360
|
+
el("label", {}, t("tags")), chipList(tags, null, "tag"),
|
|
361
|
+
el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fGeneral, el("span", {}, t("generalTopicField"))),
|
|
250
362
|
el("div", { class: "actions" },
|
|
251
363
|
el("button", { class: "primary sm", onclick: async () => {
|
|
252
364
|
const patch = {
|
|
@@ -257,8 +369,8 @@
|
|
|
257
369
|
};
|
|
258
370
|
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(patch) });
|
|
259
371
|
resultMsg(msg, res); if (res.ok) await reload();
|
|
260
|
-
} }, "
|
|
261
|
-
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, "
|
|
372
|
+
} }, t("save")),
|
|
373
|
+
el("button", { class: "sm ghost", onclick: () => { editing = null; renderAgents(); } }, t("cancel")),
|
|
262
374
|
msg),
|
|
263
375
|
);
|
|
264
376
|
return box;
|
|
@@ -283,14 +395,14 @@
|
|
|
283
395
|
const fChan = select("", ["", ...channelIds()]);
|
|
284
396
|
const msg = el("span", { class: "msg" });
|
|
285
397
|
box.append(el("div", { class: "edit", style: "border:none" },
|
|
286
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
287
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
398
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("nameField")), fName), el("div", {}, el("label", {}, t("backend")), fBackend)),
|
|
399
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("workingDir")), fWd), el("div", {}, el("label", {}, t("channelBinding")), fChan)),
|
|
288
400
|
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
289
401
|
const name = fName.value.trim(); if (!name) { msg.innerHTML = `<span class="e">✗ name required</span>`; return; }
|
|
290
402
|
const body = { working_directory: fWd.value, backend: fBackend.value, channel_id: fChan.value || undefined };
|
|
291
403
|
const res = await api(`/api/settings/fleet/instances/${encodeURIComponent(name)}`, { method: "POST", body: JSON.stringify(body) });
|
|
292
|
-
resultMsg(msg, res); if (res.ok) { $("newAgentBtn").textContent = "
|
|
293
|
-
} }, "
|
|
404
|
+
resultMsg(msg, res); if (res.ok) { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); await reload(); }
|
|
405
|
+
} }, t("create")), el("button", { class: "sm ghost", onclick: () => { state.newAgentOpen = false; $("newAgentBtn").textContent = t("new"); renderNewAgent(false); } }, t("cancel")), msg),
|
|
294
406
|
));
|
|
295
407
|
host.append(box);
|
|
296
408
|
}
|
|
@@ -298,7 +410,7 @@
|
|
|
298
410
|
// ── Bots ──
|
|
299
411
|
function renderBots() {
|
|
300
412
|
const host = $("bots"); host.innerHTML = ""; const chs = channels();
|
|
301
|
-
if (!chs.length) { host.className = "card empty"; host.textContent = "
|
|
413
|
+
if (!chs.length) { host.className = "card empty"; host.textContent = t("noBots"); return; }
|
|
302
414
|
host.className = "card";
|
|
303
415
|
chs.forEach((ch, i) => {
|
|
304
416
|
const item = el("div", { class: "item" });
|
|
@@ -308,12 +420,40 @@
|
|
|
308
420
|
el("span", { class: "tag" + (i === 0 ? "" : " persona") + (type === "telegram" ? " tg" : "") }, chLabel(i)),
|
|
309
421
|
el("span", { class: "sub", style: "margin:0" }, ch.bot_token_env || "(no token env)"),
|
|
310
422
|
el("span", { class: "spacer" }),
|
|
311
|
-
el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? "
|
|
423
|
+
el("span", { style: "color:var(--green);font-size:13px" }, state.fleetUp ? t("online") : "—"),
|
|
312
424
|
el("button", { class: "sm danger", onclick: () => delBot(i) }, "🗑️"),
|
|
313
425
|
));
|
|
314
426
|
host.append(item);
|
|
315
427
|
});
|
|
316
428
|
}
|
|
429
|
+
|
|
430
|
+
// ── ClassicBot Channels (read-only; created dynamically via /start /stop) ──
|
|
431
|
+
function renderClassic() {
|
|
432
|
+
const host = $("classicList"); if (!host) return; host.innerHTML = "";
|
|
433
|
+
const chans = Object.values(state.classic?.channels || {});
|
|
434
|
+
if (!chans.length) { host.className = "card empty"; host.textContent = t("noClassic"); return; }
|
|
435
|
+
const filter = ($("agentSearch")?.value || "").trim().toLowerCase();
|
|
436
|
+
let list = chans;
|
|
437
|
+
if (filter) list = list.filter(c => String(c.name || "").toLowerCase().includes(filter) || String(c.instanceName || "").toLowerCase().includes(filter));
|
|
438
|
+
if (!list.length) { host.className = "card empty"; host.textContent = "—"; return; }
|
|
439
|
+
host.className = "card";
|
|
440
|
+
list.sort((a, b) => {
|
|
441
|
+
const sa = STATUS_ORDER[state.classicStatus[a.instanceName] || "stopped"], sb = STATUS_ORDER[state.classicStatus[b.instanceName] || "stopped"];
|
|
442
|
+
return sa !== sb ? sa - sb : String(a.name || "").localeCompare(String(b.name || ""));
|
|
443
|
+
});
|
|
444
|
+
for (const c of list) {
|
|
445
|
+
const st = state.classicStatus[c.instanceName] || "stopped";
|
|
446
|
+
const adapter = c.adapterId || "discord";
|
|
447
|
+
host.append(el("div", { class: "item" }, el("div", { class: "item-row" },
|
|
448
|
+
el("span", { class: "dot " + st, title: STATUS_TEXT[st] || st }),
|
|
449
|
+
el("span", { class: "name", title: c.instanceName || "" }, c.name || c.instanceName || "?"),
|
|
450
|
+
el("span", { class: "sub", style: "margin:0" }, c.backend || state.classic?.defaults?.backend || "claude-code"),
|
|
451
|
+
el("span", { class: "tag" + (adapter !== "discord" ? " persona" : "") }, adapter),
|
|
452
|
+
c.collab ? el("span", { class: "tag", style: "background:#ecfdf5;color:#065f46" }, "collab") : null,
|
|
453
|
+
)));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
317
457
|
async function saveChannels(msgEl) {
|
|
318
458
|
const res = await api("/api/settings/fleet/channels", { method: "PUT", body: JSON.stringify(channels()) });
|
|
319
459
|
if (msgEl) resultMsg(msgEl, res);
|
|
@@ -335,16 +475,16 @@
|
|
|
335
475
|
const fGroup = el("input", { type: "text", placeholder: "guild / supergroup id" });
|
|
336
476
|
const msg = el("span", { class: "msg" });
|
|
337
477
|
box.append(el("div", { class: "edit", style: "border:none" },
|
|
338
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
339
|
-
el("div", { class: "grid2" }, el("div", {}, el("label", {}, "
|
|
340
|
-
el("div", { class: "hint" }, "
|
|
478
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("type")), fType), el("div", {}, el("label", {}, t("adapterId")), fId)),
|
|
479
|
+
el("div", { class: "grid2" }, el("div", {}, el("label", {}, t("tokenEnv")), fEnv), el("div", {}, el("label", {}, t("groupId")), fGroup)),
|
|
480
|
+
el("div", { class: "hint" }, t("envHint")),
|
|
341
481
|
el("div", { class: "actions" }, el("button", { class: "primary sm", onclick: async () => {
|
|
342
482
|
if (!fEnv.value.trim()) { msg.innerHTML = `<span class="e">✗ token env required</span>`; return; }
|
|
343
483
|
const chs = channels();
|
|
344
484
|
chs.push({ id: fId.value.trim() || undefined, type: fType.value, bot_token_env: fEnv.value.trim(), group_id: fGroup.value.trim() || undefined, access: { mode: "locked", allowed_users: [] } });
|
|
345
485
|
state.fleet.channels = chs; delete state.fleet.channel;
|
|
346
|
-
const res = await saveChannels(msg); if (res.ok) { $("newBotBtn").textContent = "
|
|
347
|
-
} }, "
|
|
486
|
+
const res = await saveChannels(msg); if (res.ok) { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); await reload(); }
|
|
487
|
+
} }, t("save")), el("button", { class: "sm ghost", onclick: () => { state.newBotOpen = false; $("newBotBtn").textContent = t("add"); renderNewBot(false); } }, t("cancel")), msg),
|
|
348
488
|
));
|
|
349
489
|
host.append(box);
|
|
350
490
|
}
|
|
@@ -362,25 +502,65 @@
|
|
|
362
502
|
|
|
363
503
|
const fBackend = select(d.backend || "claude-code", BACKENDS);
|
|
364
504
|
const fModel = el("input", { type: "text", value: d.model || "", placeholder: "e.g. claude-opus-4-8" });
|
|
505
|
+
const fLocale = el("select");
|
|
506
|
+
[["en", "English"], ["zh-TW", "繁體中文 (zh-TW)"]].forEach(([v, lbl]) => { const o = el("option", {}, lbl); o.value = v; if ((d.locale || "") === v) o.selected = true; fLocale.append(o); });
|
|
365
507
|
const msg = el("span", { class: "msg" });
|
|
366
508
|
host.append(
|
|
367
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
368
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
369
|
-
el("div", { class: "field" }, el("label", {},
|
|
370
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
371
|
-
el("div", { class: "field" }, el("label", {}, "
|
|
509
|
+
el("div", { class: "field" }, el("label", {}, t("language")), fLocale),
|
|
510
|
+
el("div", { class: "field" }, el("label", {}, t("defaultBackend")), fBackend),
|
|
511
|
+
el("div", { class: "field" }, el("label", {}, t("defaultModel")), fModel),
|
|
512
|
+
el("div", { class: "field" }, el("label", {}, t("allowedUsers")), chipList(allowedUsers, null, "user id")),
|
|
513
|
+
el("div", { class: "field" }, el("label", {}, t("classicAdmins")), chipList(c.admin_users, null, "user id")),
|
|
514
|
+
el("div", { class: "field" }, el("label", {}, t("allowedGuilds")), chipList(c.allowed_guilds, null, "guild id")),
|
|
372
515
|
el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
|
|
373
|
-
d.backend = fBackend.value; d.model = fModel.value || undefined;
|
|
374
|
-
const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model }) });
|
|
516
|
+
d.backend = fBackend.value; d.model = fModel.value || undefined; d.locale = fLocale.value;
|
|
517
|
+
const r1 = await api("/api/settings/fleet/defaults", { method: "PUT", body: JSON.stringify({ backend: d.backend, model: d.model, locale: d.locale }) });
|
|
375
518
|
let r2 = { ok: true, body: {} };
|
|
376
519
|
if (primary) r2 = await saveChannels();
|
|
377
520
|
const r3 = await api("/api/settings/classic/defaults", { method: "PUT", body: JSON.stringify({ backend: c.backend, admin_users: c.admin_users, allowed_guilds: c.allowed_guilds }) });
|
|
378
521
|
const bad = [r1, r2, r3].find(r => !r.ok);
|
|
379
522
|
resultMsg(msg, bad || { ok: true, body: { warnings: [...(r1.body?.warnings || []), ...(r3.body?.warnings || [])] } });
|
|
380
|
-
} }, "
|
|
523
|
+
} }, t("save")), msg)),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Auto Update ──
|
|
528
|
+
function renderAutoUpdate() {
|
|
529
|
+
const host = $("autoUpdate"); host.innerHTML = "";
|
|
530
|
+
const u = state.updateSchedule || { enabled: false, cron: "0 4 * * *", channel: "stable" };
|
|
531
|
+
const CRON = { daily: "0 4 * * *", weekly: "0 4 * * 1" };
|
|
532
|
+
const fEnable = el("input", { type: "checkbox" }); fEnable.checked = !!u.enabled;
|
|
533
|
+
const schedVal = u.cron === CRON.weekly ? "weekly" : (u.enabled ? "daily" : "manual");
|
|
534
|
+
const fSched = select(schedVal, ["daily", "weekly", "manual"]); // labels localized below
|
|
535
|
+
[...fSched.children].forEach(o => { o._txt = ""; o.textContent = t(o.value === "manual" ? "manual" : o.value); });
|
|
536
|
+
const fChannel = select(u.channel === "beta" ? "beta" : "stable", ["stable", "beta"]);
|
|
537
|
+
[...fChannel.children].forEach(o => o.textContent = t(o.value));
|
|
538
|
+
const msg = el("span", { class: "msg" });
|
|
539
|
+
host.append(
|
|
540
|
+
el("div", { class: "field" }, el("label", { style: "display:flex;align-items:center;gap:8px;cursor:pointer" }, fEnable, el("span", {}, t("enable")))),
|
|
541
|
+
el("div", { class: "field" }, el("label", {}, t("schedule")), fSched),
|
|
542
|
+
el("div", { class: "field" }, el("label", {}, t("channel")), fChannel),
|
|
543
|
+
el("div", { class: "field" }, el("div", { class: "sub", style: "margin:0" }, `${t("currentVersion")}: ${state.version || "?"} · ${t("lastChecked")}: ${u.lastChecked ? new Date(u.lastChecked).toLocaleString() : t("never")}`)),
|
|
544
|
+
el("div", { class: "field" }, el("div", { class: "actions" }, el("button", { class: "primary", onclick: async () => {
|
|
545
|
+
const sched = fSched.value;
|
|
546
|
+
const enabled = fEnable.checked && sched !== "manual";
|
|
547
|
+
const cron = sched === "weekly" ? CRON.weekly : CRON.daily;
|
|
548
|
+
const res = await api("/api/settings/update-schedule", { method: "PUT", body: JSON.stringify({ enabled, cron, channel: fChannel.value }) });
|
|
549
|
+
resultMsg(msg, res); if (res.ok) { state.updateSchedule = { enabled, cron, channel: fChannel.value, lastChecked: u.lastChecked }; }
|
|
550
|
+
} }, t("save")), msg)),
|
|
381
551
|
);
|
|
382
552
|
}
|
|
383
553
|
|
|
554
|
+
// ── What's New ──
|
|
555
|
+
function renderWhatsNew() {
|
|
556
|
+
const host = $("wnBody"); host.innerHTML = "";
|
|
557
|
+
host.append(el("div", { class: "sub", style: "margin:0 0 8px" }, `${t("currentVersion")}: ${state.version || "?"}`));
|
|
558
|
+
host.append(el("div", { style: "font-size:13px;margin-bottom:6px" }, t("wnIntro")));
|
|
559
|
+
const ul = el("ul", { style: "margin:0 0 0 18px" });
|
|
560
|
+
for (const line of WHATSNEW) ul.append(el("li", { style: "font-size:13px;margin:4px 0" }, line));
|
|
561
|
+
host.append(ul);
|
|
562
|
+
}
|
|
563
|
+
|
|
384
564
|
// ── Advanced (YAML/JSON) ──
|
|
385
565
|
const fleetModel = () => ({ ...(state.fleet.channels ? { channels: state.fleet.channels } : state.fleet.channel ? { channel: state.fleet.channel } : {}), defaults: state.fleet.defaults || {}, instances: state.fleet.instances || {} });
|
|
386
566
|
function renderAdvanced() {
|
|
@@ -428,26 +608,38 @@
|
|
|
428
608
|
}
|
|
429
609
|
|
|
430
610
|
// ── Load / render ──
|
|
431
|
-
function renderAll() { renderAgents(); renderBots(); renderGeneral(); renderAdvanced(); }
|
|
611
|
+
function renderAll() { renderAgents(); renderClassic(); renderBots(); renderGeneral(); renderAutoUpdate(); renderWhatsNew(); renderAdvanced(); applyStaticI18n(); }
|
|
432
612
|
async function reloadLive() {
|
|
433
613
|
const f = await api("/api/fleet");
|
|
434
614
|
state.fleetUp = f.ok;
|
|
435
615
|
state.live = {}; if (f.ok && f.body?.instances) for (const i of f.body.instances) state.live[i.name] = { status: i.status };
|
|
436
|
-
|
|
616
|
+
// /api/fleet is fleet-config only; classic instance statuses come from the
|
|
617
|
+
// merged roster (/api/profiles, open GET).
|
|
618
|
+
const pr = await api("/api/profiles");
|
|
619
|
+
state.classicStatus = {}; if (pr.ok && Array.isArray(pr.body)) for (const r of pr.body) state.classicStatus[r.instance_name] = r.status;
|
|
620
|
+
renderAgents(); renderBots(); renderClassic();
|
|
437
621
|
}
|
|
438
622
|
async function reload() {
|
|
439
623
|
const f = await api("/api/settings/fleet");
|
|
440
624
|
if (!f.ok) { $("banner").style.display = "block"; $("banner").style.background = "#fef2f2"; $("banner").style.color = "var(--red)"; $("banner").textContent = "Failed to load config — check the token in the URL."; return; }
|
|
441
625
|
state.fleet = f.body || {};
|
|
442
626
|
const c = await api("/api/settings/classic"); state.classic = c.body || {};
|
|
627
|
+
const u = await api("/api/settings/update-schedule"); if (u.ok) { state.updateSchedule = u.body; state.version = u.body.currentVersion || ""; $("verBadge").textContent = "v" + (state.version || "?"); }
|
|
443
628
|
await reloadLive();
|
|
444
629
|
renderAll();
|
|
445
630
|
}
|
|
446
631
|
|
|
447
|
-
$("
|
|
448
|
-
$("
|
|
449
|
-
$("
|
|
450
|
-
$("
|
|
632
|
+
const setHelp = (open) => { $("helpPanel").classList.toggle("open", open); $("helpBackdrop").classList.toggle("open", open); };
|
|
633
|
+
$("helpBtn").onclick = () => setHelp(!$("helpPanel").classList.contains("open"));
|
|
634
|
+
$("helpClose").onclick = () => setHelp(false);
|
|
635
|
+
$("helpBackdrop").onclick = () => setHelp(false);
|
|
636
|
+
$("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); renderAll(); };
|
|
637
|
+
$("verBadge").onclick = () => { const b = $("wnBody"); b.classList.add("open"); $("wnToggle").textContent = t("collapse"); b.scrollIntoView({ behavior: "smooth" }); };
|
|
638
|
+
$("agentSearch").oninput = () => { renderAgents(); renderClassic(); };
|
|
639
|
+
$("newAgentBtn").onclick = () => { state.newAgentOpen = !state.newAgentOpen; $("newAgentBtn").textContent = state.newAgentOpen ? t("cancel") : t("new"); renderNewAgent(state.newAgentOpen); };
|
|
640
|
+
$("newBotBtn").onclick = () => { state.newBotOpen = !state.newBotOpen; $("newBotBtn").textContent = state.newBotOpen ? t("cancel") : t("add"); renderNewBot(state.newBotOpen); };
|
|
641
|
+
$("advToggle").onclick = () => { const b = $("advBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("advToggle").textContent = open ? t("collapse") : t("expand"); };
|
|
642
|
+
$("wnToggle").onclick = () => { const b = $("wnBody"); const open = !b.classList.contains("open"); b.classList.toggle("open", open); $("wnToggle").textContent = open ? t("collapse") : t("expand"); };
|
|
451
643
|
$("reloadBtn").onclick = async () => { const r = await api("/api/settings/reload", { method: "POST" }); const b = $("banner"); b.style.display = "block"; b.style.background = "#ecfdf5"; b.style.color = "var(--green)"; b.textContent = r.ok ? "✓ Reload signal sent — instances reconciling." : "Reload failed."; setTimeout(() => b.style.display = "none", 4000); setTimeout(reloadLive, 1500); };
|
|
452
644
|
|
|
453
645
|
reload();
|
package/dist/ui/view.html
CHANGED
|
@@ -58,10 +58,27 @@
|
|
|
58
58
|
#edit .hint { color: var(--dim); font-size: 11px; }
|
|
59
59
|
#msg { font-size: 12px; }
|
|
60
60
|
.err { color: #f85149; } .ok { color: #3fb950; }
|
|
61
|
+
.help-btn { position: fixed; top: 10px; right: 14px; width: 34px; height: 34px; border-radius: 50%; border: 1px solid var(--border); background: var(--panel); color: var(--fg); cursor: pointer; font-size: 16px; z-index: 60; display: inline-flex; align-items: center; justify-content: center; padding: 0; }
|
|
62
|
+
.help-btn:hover { background: var(--accent); border-color: var(--accent); }
|
|
63
|
+
.help-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); display: none; z-index: 70; }
|
|
64
|
+
.help-backdrop.open { display: block; }
|
|
65
|
+
.help-panel { position: fixed; top: 0; right: 0; height: 100%; width: 360px; max-width: 90vw; background: var(--panel); border-left: 1px solid var(--border); box-shadow: -8px 0 24px rgba(0,0,0,.4); transform: translateX(100%); transition: transform .2s ease; z-index: 80; overflow-y: auto; padding: 20px 22px; }
|
|
66
|
+
.help-panel.open { transform: translateX(0); }
|
|
67
|
+
.help-panel h3 { font-size: 16px; margin-bottom: 8px; }
|
|
68
|
+
.help-panel ul { margin: 8px 0 0 18px; }
|
|
69
|
+
.help-panel li { font-size: 13px; margin: 6px 0; color: var(--fg); }
|
|
70
|
+
.help-panel .close { position: absolute; top: 14px; right: 16px; cursor: pointer; color: var(--dim); font-size: 22px; line-height: 1; background: none; border: none; }
|
|
61
71
|
</style>
|
|
62
72
|
</head>
|
|
63
73
|
<body>
|
|
64
|
-
<
|
|
74
|
+
<button class="help-btn" id="langBtn" title="Language" style="right:56px">🌐</button>
|
|
75
|
+
<button class="help-btn" id="helpBtn" title="Help">ℹ️</button>
|
|
76
|
+
<div class="help-backdrop" id="helpBackdrop"></div>
|
|
77
|
+
<aside class="help-panel" id="helpPanel">
|
|
78
|
+
<button class="close" id="helpClose" title="Close">×</button>
|
|
79
|
+
<div id="helpContent"></div>
|
|
80
|
+
</aside>
|
|
81
|
+
<div id="sidebar"><h1 data-i18n="viewTitle">AgEnD · View</h1><div id="list"></div></div>
|
|
65
82
|
<div id="main">
|
|
66
83
|
<div id="term"><pre id="pre"></pre></div>
|
|
67
84
|
<div id="card">
|
|
@@ -71,19 +88,19 @@
|
|
|
71
88
|
<div class="meta" id="meta"></div>
|
|
72
89
|
<div class="desc" id="desc"></div>
|
|
73
90
|
</div>
|
|
74
|
-
<button id="editBtn">✏️ Edit</button>
|
|
91
|
+
<button id="editBtn" data-i18n="editProfile">✏️ Edit</button>
|
|
75
92
|
<div id="edit">
|
|
76
|
-
<input id="fDisplay" placeholder="Display name">
|
|
77
|
-
<input id="fRole" placeholder="Role">
|
|
78
|
-
<textarea id="fDesc" placeholder="Description"></textarea>
|
|
93
|
+
<input id="fDisplay" data-i18n-ph="displayName" placeholder="Display name">
|
|
94
|
+
<input id="fRole" data-i18n-ph="role" placeholder="Role">
|
|
95
|
+
<textarea id="fDesc" data-i18n-ph="description" placeholder="Description"></textarea>
|
|
79
96
|
<input id="fAvatar" type="file" accept="image/png,image/jpeg,image/gif,image/webp">
|
|
80
|
-
<input id="fToken" type="password" placeholder="web.token (required to save)">
|
|
97
|
+
<input id="fToken" type="password" data-i18n-ph="tokenPh" placeholder="web.token (required to save)">
|
|
81
98
|
<div class="actions">
|
|
82
|
-
<button id="saveBtn">Save</button>
|
|
83
|
-
<button id="cancelBtn">Cancel</button>
|
|
99
|
+
<button id="saveBtn" data-i18n="save">Save</button>
|
|
100
|
+
<button id="cancelBtn" data-i18n="cancel">Cancel</button>
|
|
84
101
|
<span id="msg"></span>
|
|
85
102
|
</div>
|
|
86
|
-
<div class="hint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
|
|
103
|
+
<div class="hint" data-i18n="saveHint">Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).</div>
|
|
87
104
|
</div>
|
|
88
105
|
</div>
|
|
89
106
|
</div>
|
|
@@ -96,6 +113,33 @@
|
|
|
96
113
|
const urlToken = new URLSearchParams(location.search).get("token") || "";
|
|
97
114
|
const webToken = () => localStorage.getItem("agend_web_token") || urlToken;
|
|
98
115
|
const q = (id) => document.getElementById(id);
|
|
116
|
+
|
|
117
|
+
// ── i18n ──
|
|
118
|
+
const I18N = {
|
|
119
|
+
en: { viewTitle: "AgEnD · View", editProfile: "✏️ Edit", save: "Save", cancel: "Cancel",
|
|
120
|
+
displayName: "Display name", role: "Role", description: "Description", tokenPh: "web.token (required to save)",
|
|
121
|
+
saveHint: "Viewing is open (read-only). Saving requires the web.token (~/.agend/web.token).", helpTitle: "📖 Help",
|
|
122
|
+
help1: "Pick an instance on the left → its live terminal shows on the right",
|
|
123
|
+
help2: "Click a group header ▼/▶ to collapse/expand", help3: "✏️ Edit changes display name, role, description",
|
|
124
|
+
help4: "Drag to reorder (needs web.token permission)" },
|
|
125
|
+
"zh-TW": { viewTitle: "AgEnD · 檢視", editProfile: "✏️ 編輯", save: "儲存", cancel: "取消",
|
|
126
|
+
displayName: "顯示名稱", role: "角色", description: "描述", tokenPh: "web.token(儲存時需要)",
|
|
127
|
+
saveHint: "檢視為公開(唯讀)。儲存需要 web.token(~/.agend/web.token)。", helpTitle: "📖 使用說明",
|
|
128
|
+
help1: "左側選 instance → 右側顯示即時 terminal 畫面",
|
|
129
|
+
help2: "點 group header ▼/▶ 收合/展開", help3: "✏️ Edit 可修改顯示名稱、角色、描述",
|
|
130
|
+
help4: "拖曳可調整順序(需 web.token 權限)" },
|
|
131
|
+
};
|
|
132
|
+
let lang = localStorage.getItem("agend_lang") || (navigator.language && navigator.language.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
|
|
133
|
+
if (!I18N[lang]) lang = "en";
|
|
134
|
+
const T = (k) => (I18N[lang] && I18N[lang][k]) || I18N.en[k] || k;
|
|
135
|
+
function applyI18n() {
|
|
136
|
+
document.querySelectorAll("[data-i18n]").forEach(e => { e.textContent = T(e.getAttribute("data-i18n")); });
|
|
137
|
+
document.querySelectorAll("[data-i18n-ph]").forEach(e => { e.setAttribute("placeholder", T(e.getAttribute("data-i18n-ph"))); });
|
|
138
|
+
const hc = q("helpContent");
|
|
139
|
+
if (hc) { hc.innerHTML = ""; const h3 = document.createElement("h3"); h3.textContent = T("helpTitle"); hc.append(h3);
|
|
140
|
+
const ul = document.createElement("ul"); for (const k of ["help1","help2","help3","help4"]) { const li = document.createElement("li"); li.textContent = T(k); ul.append(li); } hc.append(ul); }
|
|
141
|
+
}
|
|
142
|
+
|
|
99
143
|
let roster = [];
|
|
100
144
|
let rosterByName = new Map();
|
|
101
145
|
let current = null;
|
|
@@ -467,6 +511,13 @@
|
|
|
467
511
|
q("cancelBtn").onclick = exitEdit;
|
|
468
512
|
q("saveBtn").onclick = save;
|
|
469
513
|
|
|
514
|
+
const setHelp = (open) => { q("helpPanel").classList.toggle("open", open); q("helpBackdrop").classList.toggle("open", open); };
|
|
515
|
+
q("helpBtn").onclick = () => setHelp(!q("helpPanel").classList.contains("open"));
|
|
516
|
+
q("helpClose").onclick = () => setHelp(false);
|
|
517
|
+
q("helpBackdrop").onclick = () => setHelp(false);
|
|
518
|
+
q("langBtn").onclick = () => { lang = lang === "en" ? "zh-TW" : "en"; localStorage.setItem("agend_lang", lang); applyI18n(); };
|
|
519
|
+
applyI18n();
|
|
520
|
+
|
|
470
521
|
// ── Boot ──────────────────────────────────────────────
|
|
471
522
|
loadRoster().then(() => { refreshPane(); });
|
|
472
523
|
poll = setInterval(refreshPane, 800);
|