@chrysb/alphaclaw 0.3.3 → 0.3.4
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/bin/alphaclaw.js +18 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +231 -28
- package/lib/public/js/components/file-viewer.js +193 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +25 -1
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
|
@@ -1,1365 +0,0 @@
|
|
|
1
|
-
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import { useState, useEffect } from "https://esm.sh/preact/hooks";
|
|
3
|
-
import htm from "https://esm.sh/htm";
|
|
4
|
-
import { Badge } from "./badge.js";
|
|
5
|
-
import { showToast } from "./toast.js";
|
|
6
|
-
import { ActionButton } from "./action-button.js";
|
|
7
|
-
|
|
8
|
-
const html = htm.bind(h);
|
|
9
|
-
|
|
10
|
-
const authFetch = async (url, opts = {}) => {
|
|
11
|
-
const res = await fetch(url, opts);
|
|
12
|
-
if (res.status === 401) {
|
|
13
|
-
window.location.href = "/setup";
|
|
14
|
-
throw new Error("Unauthorized");
|
|
15
|
-
}
|
|
16
|
-
return res;
|
|
17
|
-
};
|
|
18
|
-
const api = {
|
|
19
|
-
verifyBot: async () => {
|
|
20
|
-
const res = await authFetch("/api/telegram/bot");
|
|
21
|
-
return res.json();
|
|
22
|
-
},
|
|
23
|
-
workspace: async () => {
|
|
24
|
-
const res = await authFetch("/api/telegram/workspace");
|
|
25
|
-
return res.json();
|
|
26
|
-
},
|
|
27
|
-
resetWorkspace: async () => {
|
|
28
|
-
const res = await authFetch("/api/telegram/workspace/reset", {
|
|
29
|
-
method: "POST",
|
|
30
|
-
});
|
|
31
|
-
return res.json();
|
|
32
|
-
},
|
|
33
|
-
verifyGroup: async (groupId) => {
|
|
34
|
-
const res = await authFetch("/api/telegram/groups/verify", {
|
|
35
|
-
method: "POST",
|
|
36
|
-
headers: { "Content-Type": "application/json" },
|
|
37
|
-
body: JSON.stringify({ groupId }),
|
|
38
|
-
});
|
|
39
|
-
return res.json();
|
|
40
|
-
},
|
|
41
|
-
listTopics: async (groupId) => {
|
|
42
|
-
const res = await authFetch(
|
|
43
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics`,
|
|
44
|
-
);
|
|
45
|
-
return res.json();
|
|
46
|
-
},
|
|
47
|
-
createTopicsBulk: async (groupId, topics) => {
|
|
48
|
-
const res = await authFetch(
|
|
49
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/bulk`,
|
|
50
|
-
{
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: { "Content-Type": "application/json" },
|
|
53
|
-
body: JSON.stringify({ topics }),
|
|
54
|
-
},
|
|
55
|
-
);
|
|
56
|
-
return res.json();
|
|
57
|
-
},
|
|
58
|
-
deleteTopic: async (groupId, topicId) => {
|
|
59
|
-
const res = await authFetch(
|
|
60
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${topicId}`,
|
|
61
|
-
{ method: "DELETE" },
|
|
62
|
-
);
|
|
63
|
-
return res.json();
|
|
64
|
-
},
|
|
65
|
-
updateTopic: async (groupId, topicId, payload) => {
|
|
66
|
-
const res = await authFetch(
|
|
67
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/topics/${encodeURIComponent(topicId)}`,
|
|
68
|
-
{
|
|
69
|
-
method: "PUT",
|
|
70
|
-
headers: { "Content-Type": "application/json" },
|
|
71
|
-
body: JSON.stringify(payload),
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
return res.json();
|
|
75
|
-
},
|
|
76
|
-
configureGroup: async (groupId, payload) => {
|
|
77
|
-
const res = await authFetch(
|
|
78
|
-
`/api/telegram/groups/${encodeURIComponent(groupId)}/configure`,
|
|
79
|
-
{
|
|
80
|
-
method: "POST",
|
|
81
|
-
headers: { "Content-Type": "application/json" },
|
|
82
|
-
body: JSON.stringify(payload),
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
return res.json();
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const kSteps = [
|
|
90
|
-
{ id: "verify-bot", label: "Verify Bot" },
|
|
91
|
-
{ id: "create-group", label: "Create Group" },
|
|
92
|
-
{ id: "add-bot", label: "Add Bot" },
|
|
93
|
-
{ id: "topics", label: "Topics" },
|
|
94
|
-
{ id: "summary", label: "Summary" },
|
|
95
|
-
];
|
|
96
|
-
|
|
97
|
-
const kTelegramWorkspaceStorageKey = "telegram-workspace-state-v1";
|
|
98
|
-
const kTelegramWorkspaceCacheKey = "telegram-workspace-cache-v1";
|
|
99
|
-
const loadTelegramWorkspaceState = () => {
|
|
100
|
-
try {
|
|
101
|
-
const raw = window.localStorage.getItem(kTelegramWorkspaceStorageKey);
|
|
102
|
-
if (!raw) return {};
|
|
103
|
-
const parsed = JSON.parse(raw);
|
|
104
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
105
|
-
} catch {
|
|
106
|
-
return {};
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
const loadTelegramWorkspaceCache = () => {
|
|
110
|
-
try {
|
|
111
|
-
const raw = window.localStorage.getItem(kTelegramWorkspaceCacheKey);
|
|
112
|
-
if (!raw) return null;
|
|
113
|
-
const parsed = JSON.parse(raw);
|
|
114
|
-
const data = parsed?.data;
|
|
115
|
-
if (!data || typeof data !== "object") return null;
|
|
116
|
-
return data;
|
|
117
|
-
} catch {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
const saveTelegramWorkspaceCache = (data) => {
|
|
122
|
-
try {
|
|
123
|
-
window.localStorage.setItem(
|
|
124
|
-
kTelegramWorkspaceCacheKey,
|
|
125
|
-
JSON.stringify({ cachedAt: Date.now(), data }),
|
|
126
|
-
);
|
|
127
|
-
} catch {}
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const StepIndicator = ({ currentStep }) => html`
|
|
131
|
-
<div class="flex items-center gap-1 mb-6">
|
|
132
|
-
${kSteps.map(
|
|
133
|
-
(s, i) => html`
|
|
134
|
-
<div
|
|
135
|
-
class="h-1 flex-1 rounded-full transition-colors ${i <= currentStep
|
|
136
|
-
? "bg-accent"
|
|
137
|
-
: "bg-border"}"
|
|
138
|
-
style=${i <= currentStep ? "background: var(--accent)" : ""}
|
|
139
|
-
/>
|
|
140
|
-
`,
|
|
141
|
-
)}
|
|
142
|
-
</div>
|
|
143
|
-
`;
|
|
144
|
-
|
|
145
|
-
const BackButton = ({ onBack }) => html`
|
|
146
|
-
<button
|
|
147
|
-
onclick=${onBack}
|
|
148
|
-
class="flex items-center gap-1.5 text-sm text-gray-500 hover:text-gray-300 transition-colors mb-4"
|
|
149
|
-
>
|
|
150
|
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
151
|
-
<path
|
|
152
|
-
d="M10.354 3.354a.5.5 0 00-.708-.708l-5 5a.5.5 0 000 .708l5 5a.5.5 0 00.708-.708L5.707 8l4.647-4.646z"
|
|
153
|
-
/>
|
|
154
|
-
</svg>
|
|
155
|
-
Back
|
|
156
|
-
</button>
|
|
157
|
-
`;
|
|
158
|
-
|
|
159
|
-
// Step 1: Verify Bot
|
|
160
|
-
const VerifyBotStep = ({ botInfo, setBotInfo, onNext }) => {
|
|
161
|
-
const [loading, setLoading] = useState(false);
|
|
162
|
-
const [error, setError] = useState(null);
|
|
163
|
-
|
|
164
|
-
const verify = async () => {
|
|
165
|
-
setLoading(true);
|
|
166
|
-
setError(null);
|
|
167
|
-
try {
|
|
168
|
-
const data = await api.verifyBot();
|
|
169
|
-
if (!data.ok) throw new Error(data.error);
|
|
170
|
-
setBotInfo(data.bot);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
setError(e.message);
|
|
173
|
-
}
|
|
174
|
-
setLoading(false);
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
useEffect(() => {
|
|
178
|
-
if (!botInfo) verify();
|
|
179
|
-
}, []);
|
|
180
|
-
|
|
181
|
-
return html`
|
|
182
|
-
<div class="space-y-4">
|
|
183
|
-
<h3 class="text-sm font-semibold">Verify Bot Setup</h3>
|
|
184
|
-
|
|
185
|
-
${botInfo &&
|
|
186
|
-
html`
|
|
187
|
-
<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
188
|
-
<div class="flex items-center gap-2">
|
|
189
|
-
<span class="text-sm text-gray-300 font-medium">@${botInfo.username}</span>
|
|
190
|
-
<${Badge} tone="success">Connected</${Badge}>
|
|
191
|
-
</div>
|
|
192
|
-
<p class="text-xs text-gray-500 mt-1">${botInfo.first_name}</p>
|
|
193
|
-
</div>
|
|
194
|
-
`}
|
|
195
|
-
${error &&
|
|
196
|
-
html`
|
|
197
|
-
<div class="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
|
198
|
-
<p class="text-sm text-red-400">${error}</p>
|
|
199
|
-
</div>
|
|
200
|
-
`}
|
|
201
|
-
${!botInfo &&
|
|
202
|
-
!loading &&
|
|
203
|
-
!error &&
|
|
204
|
-
html` <p class="text-sm text-gray-400">Checking bot token...</p> `}
|
|
205
|
-
|
|
206
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
207
|
-
<p class="text-xs font-medium text-gray-300">
|
|
208
|
-
Before continuing, configure BotFather:
|
|
209
|
-
</p>
|
|
210
|
-
<ol class="text-xs text-gray-400 space-y-1.5 list-decimal list-inside">
|
|
211
|
-
<li>
|
|
212
|
-
Open <span class="text-gray-300">@BotFather</span> in Telegram
|
|
213
|
-
</li>
|
|
214
|
-
<li>
|
|
215
|
-
Send <code class="bg-black/40 px-1 rounded">/mybots</code> and
|
|
216
|
-
select your bot
|
|
217
|
-
</li>
|
|
218
|
-
<li>
|
|
219
|
-
Go to <span class="text-gray-300">Bot Settings</span> >
|
|
220
|
-
<span class="text-gray-300">Group Privacy</span>
|
|
221
|
-
</li>
|
|
222
|
-
<li>Turn it <span class="text-yellow-400 font-medium">OFF</span></li>
|
|
223
|
-
</ol>
|
|
224
|
-
</div>
|
|
225
|
-
|
|
226
|
-
<div class="grid grid-cols-2 gap-2">
|
|
227
|
-
<div />
|
|
228
|
-
<button
|
|
229
|
-
onclick=${onNext}
|
|
230
|
-
disabled=${!botInfo}
|
|
231
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan ${!botInfo
|
|
232
|
-
? "opacity-50 cursor-not-allowed"
|
|
233
|
-
: ""}"
|
|
234
|
-
>
|
|
235
|
-
Next
|
|
236
|
-
</button>
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
`;
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
// Step 2: Create Group
|
|
243
|
-
const CreateGroupStep = ({ onNext, onBack }) => html`
|
|
244
|
-
<div class="space-y-4">
|
|
245
|
-
<h3 class="text-sm font-semibold">Create a Telegram Group</h3>
|
|
246
|
-
|
|
247
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
248
|
-
<p class="text-xs font-medium text-gray-300">Create the group</p>
|
|
249
|
-
<ol class="text-xs text-gray-400 space-y-2 list-decimal list-inside">
|
|
250
|
-
<li>
|
|
251
|
-
Open Telegram and create a
|
|
252
|
-
<span class="text-gray-300">new group</span>
|
|
253
|
-
</li>
|
|
254
|
-
<li>
|
|
255
|
-
Search for and add <span class="text-gray-300">your bot</span> as a
|
|
256
|
-
member
|
|
257
|
-
</li>
|
|
258
|
-
<li>
|
|
259
|
-
Hit <span class="text-gray-300">Next</span>, give the group a name
|
|
260
|
-
(e.g. "My Workspace"), and create it
|
|
261
|
-
</li>
|
|
262
|
-
</ol>
|
|
263
|
-
</div>
|
|
264
|
-
|
|
265
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
266
|
-
<p class="text-xs font-medium text-gray-300">Enable topics</p>
|
|
267
|
-
<ol class="text-xs text-gray-400 space-y-2 list-decimal list-inside">
|
|
268
|
-
<li>Tap the group name at the top to open settings</li>
|
|
269
|
-
<li>
|
|
270
|
-
Tap <span class="text-gray-300">Edit</span> (pencil icon), scroll to
|
|
271
|
-
<span class="text-gray-300"> Topics</span>, toggle it
|
|
272
|
-
<span class="text-yellow-400 font-medium"> ON</span>
|
|
273
|
-
</li>
|
|
274
|
-
</ol>
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
278
|
-
<p class="text-xs font-medium text-gray-300">Make the bot an admin</p>
|
|
279
|
-
<ol class="text-xs text-gray-400 space-y-2 list-decimal list-inside">
|
|
280
|
-
<li>Go to <span class="text-gray-300">Members</span>, tap your bot</li>
|
|
281
|
-
<li>
|
|
282
|
-
Promote it to <span class="text-yellow-400 font-medium">Admin</span>
|
|
283
|
-
</li>
|
|
284
|
-
<li>
|
|
285
|
-
Make sure
|
|
286
|
-
<span class="text-yellow-400 font-medium"> Manage Topics </span>
|
|
287
|
-
permission is enabled
|
|
288
|
-
</li>
|
|
289
|
-
</ol>
|
|
290
|
-
</div>
|
|
291
|
-
|
|
292
|
-
<p class="text-xs text-gray-500">
|
|
293
|
-
Once all three steps are done, continue to verify the setup.
|
|
294
|
-
</p>
|
|
295
|
-
|
|
296
|
-
<div class="grid grid-cols-2 gap-2">
|
|
297
|
-
<button
|
|
298
|
-
onclick=${onBack}
|
|
299
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
300
|
-
>
|
|
301
|
-
Back
|
|
302
|
-
</button>
|
|
303
|
-
<button
|
|
304
|
-
onclick=${onNext}
|
|
305
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
|
|
306
|
-
>
|
|
307
|
-
Next
|
|
308
|
-
</button>
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
`;
|
|
312
|
-
|
|
313
|
-
// Step 3: Add Bot to Group / Verify Group
|
|
314
|
-
const AddBotStep = ({
|
|
315
|
-
groupId,
|
|
316
|
-
setGroupId,
|
|
317
|
-
groupInfo,
|
|
318
|
-
setGroupInfo,
|
|
319
|
-
userId,
|
|
320
|
-
setUserId,
|
|
321
|
-
verifyGroupError,
|
|
322
|
-
setVerifyGroupError,
|
|
323
|
-
onNext,
|
|
324
|
-
onBack,
|
|
325
|
-
}) => {
|
|
326
|
-
const [input, setInput] = useState(groupId || "");
|
|
327
|
-
const [loading, setLoading] = useState(false);
|
|
328
|
-
const [saving, setSaving] = useState(false);
|
|
329
|
-
const verifyWarnings = groupInfo
|
|
330
|
-
? [
|
|
331
|
-
...(!groupInfo.chat?.isForum
|
|
332
|
-
? ["Topics are OFF. Enable Topics in Telegram group settings."]
|
|
333
|
-
: []),
|
|
334
|
-
...(!groupInfo.bot?.isAdmin
|
|
335
|
-
? ["Bot is not an admin. Promote it to admin in group members."]
|
|
336
|
-
: []),
|
|
337
|
-
...(!groupInfo.bot?.canManageTopics
|
|
338
|
-
? [
|
|
339
|
-
"Bot is missing Manage Topics permission. Enable it in admin permissions.",
|
|
340
|
-
]
|
|
341
|
-
: []),
|
|
342
|
-
]
|
|
343
|
-
: [];
|
|
344
|
-
|
|
345
|
-
const verify = async () => {
|
|
346
|
-
const id = input.trim();
|
|
347
|
-
if (!id) return;
|
|
348
|
-
setLoading(true);
|
|
349
|
-
setVerifyGroupError(null);
|
|
350
|
-
try {
|
|
351
|
-
const data = await api.verifyGroup(id);
|
|
352
|
-
if (!data.ok) throw new Error(data.error);
|
|
353
|
-
setGroupId(id);
|
|
354
|
-
setGroupInfo(data);
|
|
355
|
-
if (!String(userId || "").trim() && data.suggestedUserId) {
|
|
356
|
-
setUserId(String(data.suggestedUserId));
|
|
357
|
-
}
|
|
358
|
-
} catch (e) {
|
|
359
|
-
setVerifyGroupError(e.message);
|
|
360
|
-
setGroupInfo(null);
|
|
361
|
-
}
|
|
362
|
-
setLoading(false);
|
|
363
|
-
};
|
|
364
|
-
const canContinue = !!(
|
|
365
|
-
groupInfo &&
|
|
366
|
-
groupInfo.chat?.isForum &&
|
|
367
|
-
groupInfo.bot?.isAdmin &&
|
|
368
|
-
groupInfo.bot?.canManageTopics
|
|
369
|
-
);
|
|
370
|
-
const continueWithConfig = async () => {
|
|
371
|
-
if (!canContinue || saving) return;
|
|
372
|
-
setVerifyGroupError(null);
|
|
373
|
-
setSaving(true);
|
|
374
|
-
try {
|
|
375
|
-
const userIdValue = String(userId || "").trim();
|
|
376
|
-
const data = await api.configureGroup(groupId, {
|
|
377
|
-
...(userIdValue ? { userId: userIdValue } : {}),
|
|
378
|
-
groupName: groupInfo?.chat?.title || groupId,
|
|
379
|
-
requireMention: false,
|
|
380
|
-
});
|
|
381
|
-
if (!data?.ok)
|
|
382
|
-
throw new Error(data?.error || "Failed to configure Telegram group");
|
|
383
|
-
if (data.userId) setUserId(String(data.userId));
|
|
384
|
-
onNext();
|
|
385
|
-
} catch (e) {
|
|
386
|
-
setVerifyGroupError(e.message);
|
|
387
|
-
}
|
|
388
|
-
setSaving(false);
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
return html`
|
|
392
|
-
<div class="space-y-4">
|
|
393
|
-
<h3 class="text-sm font-semibold">Verify Group</h3>
|
|
394
|
-
|
|
395
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
396
|
-
<p class="text-xs text-gray-400">To get your group chat ID:</p>
|
|
397
|
-
<ol class="text-xs text-gray-400 space-y-1 list-decimal list-inside">
|
|
398
|
-
<li>
|
|
399
|
-
Invite <span class="text-gray-300">@myidbot</span> to your group
|
|
400
|
-
</li>
|
|
401
|
-
<li>
|
|
402
|
-
Send <code class="bg-black/40 px-1 rounded">/getgroupid</code>
|
|
403
|
-
</li>
|
|
404
|
-
<li>
|
|
405
|
-
Copy the ID (starts with
|
|
406
|
-
<code class="bg-black/40 px-1 rounded">-100</code>)
|
|
407
|
-
</li>
|
|
408
|
-
</ol>
|
|
409
|
-
</div>
|
|
410
|
-
|
|
411
|
-
<div class="flex gap-2">
|
|
412
|
-
<input
|
|
413
|
-
type="text"
|
|
414
|
-
value=${input}
|
|
415
|
-
onInput=${(e) => setInput(e.target.value)}
|
|
416
|
-
placeholder="-100XXXXXXXXXX"
|
|
417
|
-
class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
418
|
-
/>
|
|
419
|
-
<${ActionButton}
|
|
420
|
-
onClick=${verify}
|
|
421
|
-
disabled=${!input.trim() || loading}
|
|
422
|
-
loading=${loading}
|
|
423
|
-
tone="secondary"
|
|
424
|
-
size="md"
|
|
425
|
-
idleLabel="Verify"
|
|
426
|
-
loadingMode="inline"
|
|
427
|
-
className="rounded-lg"
|
|
428
|
-
/>
|
|
429
|
-
</div>
|
|
430
|
-
|
|
431
|
-
${verifyGroupError &&
|
|
432
|
-
html`
|
|
433
|
-
<div class="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
|
434
|
-
<p class="text-sm text-red-400">${verifyGroupError}</p>
|
|
435
|
-
</div>
|
|
436
|
-
`}
|
|
437
|
-
${groupInfo &&
|
|
438
|
-
html`
|
|
439
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
440
|
-
<div class="flex items-center gap-2">
|
|
441
|
-
<span class="text-sm text-gray-300 font-medium">${groupInfo.chat.title}</span>
|
|
442
|
-
<${Badge} tone="success">Verified</${Badge}>
|
|
443
|
-
</div>
|
|
444
|
-
<div class="flex gap-3 text-xs text-gray-500">
|
|
445
|
-
<span>Topics: ${groupInfo.chat.isForum ? "ON" : "OFF"}</span>
|
|
446
|
-
<span>Bot: ${groupInfo.bot.status}</span>
|
|
447
|
-
</div>
|
|
448
|
-
</div>
|
|
449
|
-
`}
|
|
450
|
-
${groupInfo &&
|
|
451
|
-
verifyWarnings.length === 0 &&
|
|
452
|
-
html`
|
|
453
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-2">
|
|
454
|
-
<p class="text-xs text-gray-500">Your Telegram User ID</p>
|
|
455
|
-
<input
|
|
456
|
-
type="text"
|
|
457
|
-
value=${userId}
|
|
458
|
-
onInput=${(e) => setUserId(e.target.value)}
|
|
459
|
-
placeholder="e.g. 123456789"
|
|
460
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
461
|
-
/>
|
|
462
|
-
<p class="text-xs text-gray-500">
|
|
463
|
-
Auto-filled from Telegram admins. Edit if needed.
|
|
464
|
-
</p>
|
|
465
|
-
</div>
|
|
466
|
-
`}
|
|
467
|
-
${verifyWarnings.length > 0 &&
|
|
468
|
-
html`
|
|
469
|
-
<div
|
|
470
|
-
class="bg-red-500/10 border border-red-500/20 rounded-lg p-3 space-y-3"
|
|
471
|
-
>
|
|
472
|
-
<p class="text-xs font-medium text-red-300">
|
|
473
|
-
Fix these before continuing:
|
|
474
|
-
</p>
|
|
475
|
-
<ul class="text-xs text-red-200 space-y-1 list-disc list-inside">
|
|
476
|
-
${verifyWarnings.map((message) => html`<li>${message}</li>`)}
|
|
477
|
-
</ul>
|
|
478
|
-
<p class="text-xs text-red-300 ">Once fixed, hit Verify again.</p>
|
|
479
|
-
</div>
|
|
480
|
-
`}
|
|
481
|
-
|
|
482
|
-
<div class="grid grid-cols-2 gap-2">
|
|
483
|
-
<button
|
|
484
|
-
onclick=${onBack}
|
|
485
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
486
|
-
>
|
|
487
|
-
Back
|
|
488
|
-
</button>
|
|
489
|
-
<button
|
|
490
|
-
onclick=${continueWithConfig}
|
|
491
|
-
disabled=${!canContinue || saving}
|
|
492
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan ${!canContinue ||
|
|
493
|
-
saving
|
|
494
|
-
? "opacity-50 cursor-not-allowed"
|
|
495
|
-
: ""}"
|
|
496
|
-
>
|
|
497
|
-
${saving ? "Saving..." : "Next"}
|
|
498
|
-
</button>
|
|
499
|
-
</div>
|
|
500
|
-
</div>
|
|
501
|
-
`;
|
|
502
|
-
};
|
|
503
|
-
|
|
504
|
-
// Step 4: Create Topics
|
|
505
|
-
const TopicsStep = ({ groupId, topics, setTopics, onNext, onBack }) => {
|
|
506
|
-
const [newTopicName, setNewTopicName] = useState("");
|
|
507
|
-
const [newTopicInstructions, setNewTopicInstructions] = useState("");
|
|
508
|
-
const [creating, setCreating] = useState(false);
|
|
509
|
-
const [error, setError] = useState(null);
|
|
510
|
-
const [deleting, setDeleting] = useState(null);
|
|
511
|
-
|
|
512
|
-
const loadTopics = async () => {
|
|
513
|
-
const data = await api.listTopics(groupId);
|
|
514
|
-
if (data.ok) setTopics(data.topics);
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
useEffect(() => {
|
|
518
|
-
loadTopics();
|
|
519
|
-
}, [groupId]);
|
|
520
|
-
|
|
521
|
-
const createSingle = async () => {
|
|
522
|
-
const name = newTopicName.trim();
|
|
523
|
-
const systemInstructions = newTopicInstructions.trim();
|
|
524
|
-
if (!name) return;
|
|
525
|
-
setCreating(true);
|
|
526
|
-
setError(null);
|
|
527
|
-
try {
|
|
528
|
-
const data = await api.createTopicsBulk(groupId, [
|
|
529
|
-
{ name, ...(systemInstructions ? { systemInstructions } : {}) },
|
|
530
|
-
]);
|
|
531
|
-
if (!data.ok)
|
|
532
|
-
throw new Error(data.results?.[0]?.error || "Failed to create topic");
|
|
533
|
-
const failed = data.results.filter((r) => !r.ok);
|
|
534
|
-
if (failed.length > 0) throw new Error(failed[0].error);
|
|
535
|
-
setNewTopicName("");
|
|
536
|
-
setNewTopicInstructions("");
|
|
537
|
-
await loadTopics();
|
|
538
|
-
showToast(`Created topic: ${name}`, "success");
|
|
539
|
-
} catch (e) {
|
|
540
|
-
setError(e.message);
|
|
541
|
-
}
|
|
542
|
-
setCreating(false);
|
|
543
|
-
};
|
|
544
|
-
|
|
545
|
-
const handleDelete = async (topicId, topicName) => {
|
|
546
|
-
setDeleting(topicId);
|
|
547
|
-
try {
|
|
548
|
-
const data = await api.deleteTopic(groupId, topicId);
|
|
549
|
-
if (!data.ok) throw new Error(data.error);
|
|
550
|
-
await loadTopics();
|
|
551
|
-
if (data.removedFromRegistryOnly) {
|
|
552
|
-
showToast(`Removed stale topic from registry: ${topicName}`, "success");
|
|
553
|
-
} else {
|
|
554
|
-
showToast(`Deleted topic: ${topicName}`, "success");
|
|
555
|
-
}
|
|
556
|
-
} catch (e) {
|
|
557
|
-
showToast(`Failed to delete: ${e.message}`, "error");
|
|
558
|
-
}
|
|
559
|
-
setDeleting(null);
|
|
560
|
-
};
|
|
561
|
-
|
|
562
|
-
const topicEntries = Object.entries(topics || {});
|
|
563
|
-
|
|
564
|
-
return html`
|
|
565
|
-
<div class="space-y-4">
|
|
566
|
-
<h3 class="text-sm font-semibold">Create Topics</h3>
|
|
567
|
-
|
|
568
|
-
${topicEntries.length > 0 &&
|
|
569
|
-
html`
|
|
570
|
-
<div
|
|
571
|
-
class="bg-black/20 border border-border rounded-lg overflow-hidden"
|
|
572
|
-
>
|
|
573
|
-
<table class="w-full text-xs">
|
|
574
|
-
<thead>
|
|
575
|
-
<tr class="border-b border-border">
|
|
576
|
-
<th class="text-left px-3 py-2 text-gray-500 font-medium">
|
|
577
|
-
Topic
|
|
578
|
-
</th>
|
|
579
|
-
<th class="text-left px-3 py-2 text-gray-500 font-medium">
|
|
580
|
-
Thread ID
|
|
581
|
-
</th>
|
|
582
|
-
<th class="px-3 py-2 w-8" />
|
|
583
|
-
</tr>
|
|
584
|
-
</thead>
|
|
585
|
-
<tbody>
|
|
586
|
-
${topicEntries.map(
|
|
587
|
-
([id, t]) => html`
|
|
588
|
-
<tr class="border-b border-border last:border-0">
|
|
589
|
-
<td class="px-3 py-2 text-gray-300">${t.name}</td>
|
|
590
|
-
<td class="px-3 py-2 text-gray-500 font-mono">${id}</td>
|
|
591
|
-
<td class="px-3 py-2">
|
|
592
|
-
<button
|
|
593
|
-
onclick=${() => handleDelete(id, t.name)}
|
|
594
|
-
disabled=${deleting === id}
|
|
595
|
-
class="text-gray-600 hover:text-red-400 transition-colors ${deleting ===
|
|
596
|
-
id
|
|
597
|
-
? "opacity-50"
|
|
598
|
-
: ""}"
|
|
599
|
-
title="Delete topic"
|
|
600
|
-
>
|
|
601
|
-
<svg
|
|
602
|
-
width="14"
|
|
603
|
-
height="14"
|
|
604
|
-
viewBox="0 0 16 16"
|
|
605
|
-
fill="currentColor"
|
|
606
|
-
>
|
|
607
|
-
<path
|
|
608
|
-
d="M4.646 4.646a.5.5 0 01.708 0L8 7.293l2.646-2.647a.5.5 0 01.708.708L8.707 8l2.647 2.646a.5.5 0 01-.708.708L8 8.707l-2.646 2.647a.5.5 0 01-.708-.708L7.293 8 4.646 5.354a.5.5 0 010-.708z"
|
|
609
|
-
/>
|
|
610
|
-
</svg>
|
|
611
|
-
</button>
|
|
612
|
-
</td>
|
|
613
|
-
</tr>
|
|
614
|
-
`,
|
|
615
|
-
)}
|
|
616
|
-
</tbody>
|
|
617
|
-
</table>
|
|
618
|
-
</div>
|
|
619
|
-
`}
|
|
620
|
-
|
|
621
|
-
<div class="space-y-2">
|
|
622
|
-
<label class="text-xs text-gray-500">Add a topic</label>
|
|
623
|
-
<div class="space-y-2">
|
|
624
|
-
<div class="flex gap-2">
|
|
625
|
-
<input
|
|
626
|
-
type="text"
|
|
627
|
-
value=${newTopicName}
|
|
628
|
-
onInput=${(e) => setNewTopicName(e.target.value)}
|
|
629
|
-
onKeyDown=${(e) => {
|
|
630
|
-
if (e.key === "Enter") createSingle();
|
|
631
|
-
}}
|
|
632
|
-
placeholder="Topic name"
|
|
633
|
-
class="flex-1 bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
634
|
-
/>
|
|
635
|
-
</div>
|
|
636
|
-
<textarea
|
|
637
|
-
value=${newTopicInstructions}
|
|
638
|
-
onInput=${(e) => setNewTopicInstructions(e.target.value)}
|
|
639
|
-
placeholder="System instructions (optional)"
|
|
640
|
-
rows="4"
|
|
641
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
|
|
642
|
-
/>
|
|
643
|
-
<div class="flex justify-end">
|
|
644
|
-
<${ActionButton}
|
|
645
|
-
onClick=${createSingle}
|
|
646
|
-
disabled=${creating || !newTopicName.trim()}
|
|
647
|
-
loading=${creating}
|
|
648
|
-
tone="secondary"
|
|
649
|
-
size="lg"
|
|
650
|
-
idleLabel="Add"
|
|
651
|
-
loadingMode="inline"
|
|
652
|
-
className="min-w-[88px]"
|
|
653
|
-
/>
|
|
654
|
-
</div>
|
|
655
|
-
</div>
|
|
656
|
-
</div>
|
|
657
|
-
<div class="border-t border-white/10 pt-2" />
|
|
658
|
-
|
|
659
|
-
${error &&
|
|
660
|
-
html`
|
|
661
|
-
<div class="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
|
662
|
-
<p class="text-sm text-red-400">${error}</p>
|
|
663
|
-
</div>
|
|
664
|
-
`}
|
|
665
|
-
|
|
666
|
-
<div class="grid grid-cols-2 gap-2">
|
|
667
|
-
<button
|
|
668
|
-
onclick=${onBack}
|
|
669
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
670
|
-
>
|
|
671
|
-
Back
|
|
672
|
-
</button>
|
|
673
|
-
<button
|
|
674
|
-
onclick=${onNext}
|
|
675
|
-
disabled=${topicEntries.length === 0}
|
|
676
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
|
|
677
|
-
>
|
|
678
|
-
Next
|
|
679
|
-
</button>
|
|
680
|
-
</div>
|
|
681
|
-
</div>
|
|
682
|
-
`;
|
|
683
|
-
};
|
|
684
|
-
|
|
685
|
-
// Step 5: Summary
|
|
686
|
-
const SummaryStep = ({ groupId, groupInfo, topics, onBack, onDone }) => {
|
|
687
|
-
return html`
|
|
688
|
-
<div class="space-y-4">
|
|
689
|
-
<div class="max-w-xl mx-auto text-center space-y-10 mt-10">
|
|
690
|
-
<p class="text-sm font-medium text-green-300">🎉 Setup complete</p>
|
|
691
|
-
<p class="text-xs text-gray-400">
|
|
692
|
-
The topic registry has been injected into
|
|
693
|
-
<code class="bg-black/40 px-1 rounded">TOOLS.md</code> so your agent
|
|
694
|
-
knows which thread ID maps to which topic name.
|
|
695
|
-
</p>
|
|
696
|
-
|
|
697
|
-
<div class="bg-black/20 border border-border rounded-lg p-3">
|
|
698
|
-
<p class="text-xs text-gray-500">
|
|
699
|
-
If you used <span class="text-gray-300">@myidbot</span> to find IDs,
|
|
700
|
-
you can remove it from the group now.
|
|
701
|
-
</p>
|
|
702
|
-
</div>
|
|
703
|
-
</div>
|
|
704
|
-
|
|
705
|
-
<div class="grid grid-cols-2 gap-2">
|
|
706
|
-
<button
|
|
707
|
-
onclick=${onBack}
|
|
708
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
709
|
-
>
|
|
710
|
-
Back
|
|
711
|
-
</button>
|
|
712
|
-
<button
|
|
713
|
-
onclick=${onDone}
|
|
714
|
-
class="w-full text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"
|
|
715
|
-
>
|
|
716
|
-
Done
|
|
717
|
-
</button>
|
|
718
|
-
</div>
|
|
719
|
-
</div>
|
|
720
|
-
`;
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
const ManageTelegramWorkspace = ({
|
|
724
|
-
groupId,
|
|
725
|
-
groupName,
|
|
726
|
-
initialTopics,
|
|
727
|
-
configAgentMaxConcurrent,
|
|
728
|
-
configSubagentMaxConcurrent,
|
|
729
|
-
debugEnabled,
|
|
730
|
-
onResetOnboarding,
|
|
731
|
-
}) => {
|
|
732
|
-
const [topics, setTopics] = useState(initialTopics || {});
|
|
733
|
-
const [newTopicName, setNewTopicName] = useState("");
|
|
734
|
-
const [newTopicInstructions, setNewTopicInstructions] = useState("");
|
|
735
|
-
const [showCreateTopic, setShowCreateTopic] = useState(false);
|
|
736
|
-
const [creating, setCreating] = useState(false);
|
|
737
|
-
const [deleting, setDeleting] = useState(null);
|
|
738
|
-
const [editingTopicId, setEditingTopicId] = useState("");
|
|
739
|
-
const [editingTopicName, setEditingTopicName] = useState("");
|
|
740
|
-
const [editingTopicInstructions, setEditingTopicInstructions] = useState("");
|
|
741
|
-
const [renamingTopicId, setRenamingTopicId] = useState("");
|
|
742
|
-
const [error, setError] = useState(null);
|
|
743
|
-
|
|
744
|
-
const loadTopics = async () => {
|
|
745
|
-
const data = await api.listTopics(groupId);
|
|
746
|
-
if (data.ok) setTopics(data.topics || {});
|
|
747
|
-
};
|
|
748
|
-
|
|
749
|
-
useEffect(() => {
|
|
750
|
-
loadTopics();
|
|
751
|
-
}, [groupId]);
|
|
752
|
-
useEffect(() => {
|
|
753
|
-
if (initialTopics && Object.keys(initialTopics).length > 0) {
|
|
754
|
-
setTopics(initialTopics);
|
|
755
|
-
}
|
|
756
|
-
}, [initialTopics]);
|
|
757
|
-
|
|
758
|
-
const createSingle = async () => {
|
|
759
|
-
const name = newTopicName.trim();
|
|
760
|
-
const systemInstructions = newTopicInstructions.trim();
|
|
761
|
-
if (!name) return;
|
|
762
|
-
setCreating(true);
|
|
763
|
-
setError(null);
|
|
764
|
-
try {
|
|
765
|
-
const data = await api.createTopicsBulk(groupId, [
|
|
766
|
-
{ name, ...(systemInstructions ? { systemInstructions } : {}) },
|
|
767
|
-
]);
|
|
768
|
-
if (!data.ok)
|
|
769
|
-
throw new Error(data.results?.[0]?.error || "Failed to create topic");
|
|
770
|
-
const failed = data.results.filter((r) => !r.ok);
|
|
771
|
-
if (failed.length > 0) throw new Error(failed[0].error);
|
|
772
|
-
setNewTopicName("");
|
|
773
|
-
setNewTopicInstructions("");
|
|
774
|
-
setShowCreateTopic(false);
|
|
775
|
-
await loadTopics();
|
|
776
|
-
showToast(`Created topic: ${name}`, "success");
|
|
777
|
-
} catch (e) {
|
|
778
|
-
setError(e.message);
|
|
779
|
-
}
|
|
780
|
-
setCreating(false);
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
const handleDelete = async (topicId, topicName) => {
|
|
784
|
-
setDeleting(topicId);
|
|
785
|
-
try {
|
|
786
|
-
const data = await api.deleteTopic(groupId, topicId);
|
|
787
|
-
if (!data.ok) throw new Error(data.error);
|
|
788
|
-
await loadTopics();
|
|
789
|
-
if (data.removedFromRegistryOnly) {
|
|
790
|
-
showToast(`Removed stale topic from registry: ${topicName}`, "success");
|
|
791
|
-
} else {
|
|
792
|
-
showToast(`Deleted topic: ${topicName}`, "success");
|
|
793
|
-
}
|
|
794
|
-
} catch (e) {
|
|
795
|
-
showToast(`Failed to delete: ${e.message}`, "error");
|
|
796
|
-
}
|
|
797
|
-
setDeleting(null);
|
|
798
|
-
};
|
|
799
|
-
|
|
800
|
-
const startRename = (topicId, topicName, topicInstructions = "") => {
|
|
801
|
-
setEditingTopicId(String(topicId));
|
|
802
|
-
setEditingTopicName(String(topicName || ""));
|
|
803
|
-
setEditingTopicInstructions(String(topicInstructions || ""));
|
|
804
|
-
};
|
|
805
|
-
|
|
806
|
-
const cancelRename = () => {
|
|
807
|
-
setEditingTopicId("");
|
|
808
|
-
setEditingTopicName("");
|
|
809
|
-
setEditingTopicInstructions("");
|
|
810
|
-
};
|
|
811
|
-
|
|
812
|
-
const saveRename = async (topicId) => {
|
|
813
|
-
const nextName = editingTopicName.trim();
|
|
814
|
-
const nextSystemInstructions = editingTopicInstructions.trim();
|
|
815
|
-
if (!nextName) {
|
|
816
|
-
setError("Topic name is required");
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
setRenamingTopicId(String(topicId));
|
|
820
|
-
setError(null);
|
|
821
|
-
try {
|
|
822
|
-
const data = await api.updateTopic(groupId, topicId, {
|
|
823
|
-
name: nextName,
|
|
824
|
-
systemInstructions: nextSystemInstructions,
|
|
825
|
-
});
|
|
826
|
-
if (!data.ok) throw new Error(data.error || "Failed to rename topic");
|
|
827
|
-
await loadTopics();
|
|
828
|
-
showToast(`Renamed topic: ${nextName}`, "success");
|
|
829
|
-
cancelRename();
|
|
830
|
-
} catch (e) {
|
|
831
|
-
setError(e.message);
|
|
832
|
-
}
|
|
833
|
-
setRenamingTopicId("");
|
|
834
|
-
};
|
|
835
|
-
|
|
836
|
-
const topicEntries = Object.entries(topics || {});
|
|
837
|
-
const topicCount = topicEntries.length;
|
|
838
|
-
const computedMaxConcurrent = Math.max(topicCount * 3, 8);
|
|
839
|
-
const computedSubagentMaxConcurrent = Math.max(computedMaxConcurrent - 2, 4);
|
|
840
|
-
const maxConcurrent = Number.isFinite(configAgentMaxConcurrent)
|
|
841
|
-
? configAgentMaxConcurrent
|
|
842
|
-
: computedMaxConcurrent;
|
|
843
|
-
const subagentMaxConcurrent = Number.isFinite(configSubagentMaxConcurrent)
|
|
844
|
-
? configSubagentMaxConcurrent
|
|
845
|
-
: computedSubagentMaxConcurrent;
|
|
846
|
-
|
|
847
|
-
return html`
|
|
848
|
-
<div class="space-y-4">
|
|
849
|
-
${debugEnabled &&
|
|
850
|
-
html`
|
|
851
|
-
<div class="flex justify-end">
|
|
852
|
-
<button
|
|
853
|
-
onclick=${onResetOnboarding}
|
|
854
|
-
class="text-xs px-3 py-1.5 rounded-lg border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-colors"
|
|
855
|
-
>
|
|
856
|
-
Reset onboarding
|
|
857
|
-
</button>
|
|
858
|
-
</div>
|
|
859
|
-
`}
|
|
860
|
-
<div class="bg-black/20 border border-border rounded-lg p-3 space-y-1">
|
|
861
|
-
<p class="text-sm text-gray-300 font-medium">${groupName || groupId}</p>
|
|
862
|
-
<p class="text-xs text-gray-500 font-mono">${groupId}</p>
|
|
863
|
-
</div>
|
|
864
|
-
|
|
865
|
-
<div class="space-y-2">
|
|
866
|
-
<h2 class="card-label mb-3">Existing Topics</h2>
|
|
867
|
-
${topicEntries.length > 0
|
|
868
|
-
? html`
|
|
869
|
-
<div
|
|
870
|
-
class="bg-black/20 border border-border rounded-lg overflow-hidden"
|
|
871
|
-
>
|
|
872
|
-
<table class="w-full text-xs table-fixed">
|
|
873
|
-
<thead>
|
|
874
|
-
<tr class="border-b border-border">
|
|
875
|
-
<th class="text-left px-3 py-2 text-gray-500 font-medium">
|
|
876
|
-
Topic
|
|
877
|
-
</th>
|
|
878
|
-
<th
|
|
879
|
-
class="text-left px-3 py-2 text-gray-500 font-medium w-36"
|
|
880
|
-
>
|
|
881
|
-
Thread ID
|
|
882
|
-
</th>
|
|
883
|
-
<th class="px-3 py-2 w-28" />
|
|
884
|
-
</tr>
|
|
885
|
-
</thead>
|
|
886
|
-
<tbody>
|
|
887
|
-
${topicEntries.map(
|
|
888
|
-
([id, topic]) => html`
|
|
889
|
-
${editingTopicId === String(id)
|
|
890
|
-
? html`
|
|
891
|
-
<tr
|
|
892
|
-
class="border-b border-border last:border-0 align-top"
|
|
893
|
-
>
|
|
894
|
-
<td class="px-3 py-2" colspan="3">
|
|
895
|
-
<div class="space-y-2">
|
|
896
|
-
<input
|
|
897
|
-
type="text"
|
|
898
|
-
value=${editingTopicName}
|
|
899
|
-
onInput=${(e) =>
|
|
900
|
-
setEditingTopicName(e.target.value)}
|
|
901
|
-
onKeyDown=${(e) => {
|
|
902
|
-
if (e.key === "Enter") saveRename(id);
|
|
903
|
-
if (e.key === "Escape") cancelRename();
|
|
904
|
-
}}
|
|
905
|
-
class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
906
|
-
/>
|
|
907
|
-
<textarea
|
|
908
|
-
value=${editingTopicInstructions}
|
|
909
|
-
onInput=${(e) =>
|
|
910
|
-
setEditingTopicInstructions(
|
|
911
|
-
e.target.value,
|
|
912
|
-
)}
|
|
913
|
-
placeholder="System instructions (optional)"
|
|
914
|
-
rows="6"
|
|
915
|
-
class="w-full bg-black/30 border border-border rounded-lg px-2 py-1.5 text-xs text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
|
|
916
|
-
/>
|
|
917
|
-
<div class="flex items-center gap-2">
|
|
918
|
-
<button
|
|
919
|
-
onclick=${() => saveRename(id)}
|
|
920
|
-
disabled=${renamingTopicId ===
|
|
921
|
-
String(id)}
|
|
922
|
-
class="text-xs px-2 py-1 rounded transition-all ac-btn-cyan ${renamingTopicId ===
|
|
923
|
-
String(id)
|
|
924
|
-
? "opacity-50 cursor-not-allowed"
|
|
925
|
-
: ""}"
|
|
926
|
-
>
|
|
927
|
-
Save
|
|
928
|
-
</button>
|
|
929
|
-
<button
|
|
930
|
-
onclick=${cancelRename}
|
|
931
|
-
class="text-xs px-2 py-1 rounded border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500"
|
|
932
|
-
>
|
|
933
|
-
Cancel
|
|
934
|
-
</button>
|
|
935
|
-
</div>
|
|
936
|
-
</div>
|
|
937
|
-
</td>
|
|
938
|
-
</tr>
|
|
939
|
-
`
|
|
940
|
-
: html`
|
|
941
|
-
<tr
|
|
942
|
-
class="border-b border-border last:border-0 align-middle"
|
|
943
|
-
>
|
|
944
|
-
<td class="px-3 py-2 text-gray-300">
|
|
945
|
-
<div class="flex items-center gap-2">
|
|
946
|
-
<span>${topic.name}</span>
|
|
947
|
-
<button
|
|
948
|
-
onclick=${() =>
|
|
949
|
-
startRename(
|
|
950
|
-
id,
|
|
951
|
-
topic.name,
|
|
952
|
-
topic.systemInstructions,
|
|
953
|
-
)}
|
|
954
|
-
class="inline-flex items-center justify-center text-white/80 hover:text-white transition-colors"
|
|
955
|
-
title="Edit topic"
|
|
956
|
-
aria-label="Rename topic"
|
|
957
|
-
>
|
|
958
|
-
<svg
|
|
959
|
-
width="14"
|
|
960
|
-
height="14"
|
|
961
|
-
viewBox="0 0 16 16"
|
|
962
|
-
fill="currentColor"
|
|
963
|
-
aria-hidden="true"
|
|
964
|
-
>
|
|
965
|
-
<path
|
|
966
|
-
d="M11.854 1.146a.5.5 0 00-.708 0L3 9.293V13h3.707l8.146-8.146a.5.5 0 000-.708l-3-3zM3.5 12.5v-2.793l7-7L13.793 6l-7 7H3.5z"
|
|
967
|
-
/>
|
|
968
|
-
</svg>
|
|
969
|
-
</button>
|
|
970
|
-
</div>
|
|
971
|
-
${topic.systemInstructions &&
|
|
972
|
-
html`
|
|
973
|
-
<p
|
|
974
|
-
class="text-[11px] text-gray-500 mt-1 line-clamp-1"
|
|
975
|
-
>
|
|
976
|
-
${topic.systemInstructions}
|
|
977
|
-
</p>
|
|
978
|
-
`}
|
|
979
|
-
</td>
|
|
980
|
-
<td
|
|
981
|
-
class="px-3 py-2 text-gray-500 font-mono w-36"
|
|
982
|
-
>
|
|
983
|
-
${id}
|
|
984
|
-
</td>
|
|
985
|
-
<td class="px-3 py-2">
|
|
986
|
-
<div
|
|
987
|
-
class="flex items-center gap-2 justify-end"
|
|
988
|
-
>
|
|
989
|
-
<button
|
|
990
|
-
onclick=${() =>
|
|
991
|
-
handleDelete(id, topic.name)}
|
|
992
|
-
disabled=${deleting === id}
|
|
993
|
-
class="text-xs px-2 py-1 rounded border border-border text-gray-500 hover:text-red-300 hover:border-red-500 ${deleting ===
|
|
994
|
-
id
|
|
995
|
-
? "opacity-50 cursor-not-allowed"
|
|
996
|
-
: ""}"
|
|
997
|
-
title="Delete topic"
|
|
998
|
-
>
|
|
999
|
-
Delete
|
|
1000
|
-
</button>
|
|
1001
|
-
</div>
|
|
1002
|
-
</td>
|
|
1003
|
-
</tr>
|
|
1004
|
-
`}
|
|
1005
|
-
`,
|
|
1006
|
-
)}
|
|
1007
|
-
</tbody>
|
|
1008
|
-
</table>
|
|
1009
|
-
</div>
|
|
1010
|
-
`
|
|
1011
|
-
: html`<p class="text-xs text-gray-500">No topics yet.</p>`}
|
|
1012
|
-
</div>
|
|
1013
|
-
|
|
1014
|
-
${showCreateTopic &&
|
|
1015
|
-
html`
|
|
1016
|
-
<div class="space-y-2 bg-black/20 border border-border rounded-lg p-3">
|
|
1017
|
-
<label class="text-xs text-gray-500">Create new topic</label>
|
|
1018
|
-
<div class="space-y-2">
|
|
1019
|
-
<input
|
|
1020
|
-
type="text"
|
|
1021
|
-
value=${newTopicName}
|
|
1022
|
-
onInput=${(e) => setNewTopicName(e.target.value)}
|
|
1023
|
-
onKeyDown=${(e) => {
|
|
1024
|
-
if (e.key === "Enter") createSingle();
|
|
1025
|
-
}}
|
|
1026
|
-
placeholder="Topic name"
|
|
1027
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500"
|
|
1028
|
-
/>
|
|
1029
|
-
<textarea
|
|
1030
|
-
value=${newTopicInstructions}
|
|
1031
|
-
onInput=${(e) => setNewTopicInstructions(e.target.value)}
|
|
1032
|
-
placeholder="System instructions (optional)"
|
|
1033
|
-
rows="5"
|
|
1034
|
-
class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-500 resize-y"
|
|
1035
|
-
/>
|
|
1036
|
-
<div class="flex justify-end">
|
|
1037
|
-
<${ActionButton}
|
|
1038
|
-
onClick=${createSingle}
|
|
1039
|
-
disabled=${creating || !newTopicName.trim()}
|
|
1040
|
-
loading=${creating}
|
|
1041
|
-
tone="secondary"
|
|
1042
|
-
size="lg"
|
|
1043
|
-
idleLabel="Add topic"
|
|
1044
|
-
loadingLabel="Creating..."
|
|
1045
|
-
/>
|
|
1046
|
-
</div>
|
|
1047
|
-
</div>
|
|
1048
|
-
</div>
|
|
1049
|
-
`}
|
|
1050
|
-
${error &&
|
|
1051
|
-
html`
|
|
1052
|
-
<div class="bg-red-500/10 border border-red-500/20 rounded-lg p-3">
|
|
1053
|
-
<p class="text-sm text-red-400">${error}</p>
|
|
1054
|
-
</div>
|
|
1055
|
-
`}
|
|
1056
|
-
|
|
1057
|
-
<div class="flex items-center justify-start">
|
|
1058
|
-
<button
|
|
1059
|
-
onclick=${() => setShowCreateTopic((v) => !v)}
|
|
1060
|
-
class="${showCreateTopic
|
|
1061
|
-
? "w-auto text-sm font-medium px-4 py-2 rounded-xl transition-all border border-border text-gray-300 hover:border-gray-500"
|
|
1062
|
-
: "w-auto text-sm font-medium px-4 py-2 rounded-xl transition-all ac-btn-cyan"}"
|
|
1063
|
-
>
|
|
1064
|
-
${showCreateTopic ? "Close create topic" : "Create topic"}
|
|
1065
|
-
</button>
|
|
1066
|
-
</div>
|
|
1067
|
-
|
|
1068
|
-
<div class="border-t border-white/10" />
|
|
1069
|
-
|
|
1070
|
-
<p class="text-xs text-gray-500">
|
|
1071
|
-
Concurrency is auto-scaled to support your group:
|
|
1072
|
-
<span class="text-gray-300"> agent ${maxConcurrent}</span>,
|
|
1073
|
-
<span class="text-gray-300"> subagent ${subagentMaxConcurrent}</span>
|
|
1074
|
-
<span class="text-gray-600"> (${topicCount} topics)</span>.
|
|
1075
|
-
</p>
|
|
1076
|
-
<p class="text-[11px] text-gray-500">
|
|
1077
|
-
This registry can drift if topics are created, renamed, or removed
|
|
1078
|
-
outside this page. Your agent will update the registry if it notices a
|
|
1079
|
-
discrepancy.
|
|
1080
|
-
</p>
|
|
1081
|
-
</div>
|
|
1082
|
-
`;
|
|
1083
|
-
};
|
|
1084
|
-
|
|
1085
|
-
export const TelegramWorkspace = ({ onBack }) => {
|
|
1086
|
-
const initialState = loadTelegramWorkspaceState();
|
|
1087
|
-
const cachedWorkspace = loadTelegramWorkspaceCache();
|
|
1088
|
-
const [step, setStep] = useState(() => {
|
|
1089
|
-
const value = Number.parseInt(String(initialState.step ?? 0), 10);
|
|
1090
|
-
if (!Number.isFinite(value)) return 0;
|
|
1091
|
-
return Math.min(Math.max(value, 0), kSteps.length - 1);
|
|
1092
|
-
});
|
|
1093
|
-
const [botInfo, setBotInfo] = useState(initialState.botInfo || null);
|
|
1094
|
-
const [groupId, setGroupId] = useState(initialState.groupId || "");
|
|
1095
|
-
const [groupInfo, setGroupInfo] = useState(initialState.groupInfo || null);
|
|
1096
|
-
const [verifyGroupError, setVerifyGroupError] = useState(
|
|
1097
|
-
initialState.verifyGroupError || null,
|
|
1098
|
-
);
|
|
1099
|
-
const [allowUserId, setAllowUserId] = useState(
|
|
1100
|
-
initialState.allowUserId || "",
|
|
1101
|
-
);
|
|
1102
|
-
const [topics, setTopics] = useState(initialState.topics || {});
|
|
1103
|
-
const [workspaceConfig, setWorkspaceConfig] = useState(() => ({
|
|
1104
|
-
ready: !!cachedWorkspace,
|
|
1105
|
-
configured: !!cachedWorkspace?.configured,
|
|
1106
|
-
groupId: cachedWorkspace?.groupId || "",
|
|
1107
|
-
groupName: cachedWorkspace?.groupName || "",
|
|
1108
|
-
topics: cachedWorkspace?.topics || {},
|
|
1109
|
-
debugEnabled: !!cachedWorkspace?.debugEnabled,
|
|
1110
|
-
concurrency: cachedWorkspace?.concurrency || {
|
|
1111
|
-
agentMaxConcurrent: null,
|
|
1112
|
-
subagentMaxConcurrent: null,
|
|
1113
|
-
},
|
|
1114
|
-
}));
|
|
1115
|
-
|
|
1116
|
-
const goNext = () => setStep((s) => Math.min(kSteps.length - 1, s + 1));
|
|
1117
|
-
const goBack = () => setStep((s) => Math.max(0, s - 1));
|
|
1118
|
-
const resetOnboarding = async () => {
|
|
1119
|
-
try {
|
|
1120
|
-
const data = await api.resetWorkspace();
|
|
1121
|
-
if (!data.ok) throw new Error(data.error || "Failed to reset onboarding");
|
|
1122
|
-
try {
|
|
1123
|
-
window.localStorage.removeItem(kTelegramWorkspaceStorageKey);
|
|
1124
|
-
window.localStorage.removeItem(kTelegramWorkspaceCacheKey);
|
|
1125
|
-
} catch {}
|
|
1126
|
-
setStep(0);
|
|
1127
|
-
setBotInfo(null);
|
|
1128
|
-
setGroupId("");
|
|
1129
|
-
setGroupInfo(null);
|
|
1130
|
-
setVerifyGroupError(null);
|
|
1131
|
-
setAllowUserId("");
|
|
1132
|
-
setTopics({});
|
|
1133
|
-
setWorkspaceConfig({
|
|
1134
|
-
ready: true,
|
|
1135
|
-
configured: false,
|
|
1136
|
-
groupId: "",
|
|
1137
|
-
groupName: "",
|
|
1138
|
-
topics: {},
|
|
1139
|
-
debugEnabled: !!workspaceConfig?.debugEnabled,
|
|
1140
|
-
concurrency: { agentMaxConcurrent: null, subagentMaxConcurrent: null },
|
|
1141
|
-
});
|
|
1142
|
-
showToast("Telegram onboarding reset", "success");
|
|
1143
|
-
} catch (e) {
|
|
1144
|
-
showToast(e.message || "Failed to reset onboarding", "error");
|
|
1145
|
-
}
|
|
1146
|
-
};
|
|
1147
|
-
const handleDone = () => {
|
|
1148
|
-
try {
|
|
1149
|
-
window.localStorage.removeItem(kTelegramWorkspaceStorageKey);
|
|
1150
|
-
window.localStorage.setItem(
|
|
1151
|
-
kTelegramWorkspaceCacheKey,
|
|
1152
|
-
JSON.stringify({
|
|
1153
|
-
cachedAt: Date.now(),
|
|
1154
|
-
data: {
|
|
1155
|
-
ready: true,
|
|
1156
|
-
configured: true,
|
|
1157
|
-
groupId,
|
|
1158
|
-
groupName: groupInfo?.chat?.title || groupId,
|
|
1159
|
-
topics: topics || {},
|
|
1160
|
-
debugEnabled: !!workspaceConfig?.debugEnabled,
|
|
1161
|
-
concurrency: workspaceConfig?.concurrency || {
|
|
1162
|
-
agentMaxConcurrent: null,
|
|
1163
|
-
subagentMaxConcurrent: null,
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
}),
|
|
1167
|
-
);
|
|
1168
|
-
} catch {}
|
|
1169
|
-
window.location.reload();
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
useEffect(() => {
|
|
1173
|
-
try {
|
|
1174
|
-
window.localStorage.setItem(
|
|
1175
|
-
kTelegramWorkspaceStorageKey,
|
|
1176
|
-
JSON.stringify({
|
|
1177
|
-
step,
|
|
1178
|
-
botInfo,
|
|
1179
|
-
groupId,
|
|
1180
|
-
groupInfo,
|
|
1181
|
-
verifyGroupError,
|
|
1182
|
-
allowUserId,
|
|
1183
|
-
topics,
|
|
1184
|
-
}),
|
|
1185
|
-
);
|
|
1186
|
-
} catch {}
|
|
1187
|
-
}, [
|
|
1188
|
-
step,
|
|
1189
|
-
botInfo,
|
|
1190
|
-
groupId,
|
|
1191
|
-
groupInfo,
|
|
1192
|
-
verifyGroupError,
|
|
1193
|
-
allowUserId,
|
|
1194
|
-
topics,
|
|
1195
|
-
]);
|
|
1196
|
-
|
|
1197
|
-
useEffect(() => {
|
|
1198
|
-
let active = true;
|
|
1199
|
-
const bootstrapWorkspace = async () => {
|
|
1200
|
-
try {
|
|
1201
|
-
const data = await api.workspace();
|
|
1202
|
-
if (!active || !data?.ok) return;
|
|
1203
|
-
if (!data.configured || !data.groupId) {
|
|
1204
|
-
const nextConfig = {
|
|
1205
|
-
ready: true,
|
|
1206
|
-
configured: false,
|
|
1207
|
-
groupId: "",
|
|
1208
|
-
groupName: "",
|
|
1209
|
-
topics: {},
|
|
1210
|
-
debugEnabled: !!data?.debugEnabled,
|
|
1211
|
-
concurrency: {
|
|
1212
|
-
agentMaxConcurrent: null,
|
|
1213
|
-
subagentMaxConcurrent: null,
|
|
1214
|
-
},
|
|
1215
|
-
};
|
|
1216
|
-
setWorkspaceConfig(nextConfig);
|
|
1217
|
-
saveTelegramWorkspaceCache(nextConfig);
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
const nextConfig = {
|
|
1221
|
-
ready: true,
|
|
1222
|
-
configured: true,
|
|
1223
|
-
groupId: data.groupId,
|
|
1224
|
-
groupName: data.groupName || data.groupId,
|
|
1225
|
-
topics: data.topics || {},
|
|
1226
|
-
debugEnabled: !!data.debugEnabled,
|
|
1227
|
-
concurrency: data.concurrency || {
|
|
1228
|
-
agentMaxConcurrent: null,
|
|
1229
|
-
subagentMaxConcurrent: null,
|
|
1230
|
-
},
|
|
1231
|
-
};
|
|
1232
|
-
setWorkspaceConfig(nextConfig);
|
|
1233
|
-
saveTelegramWorkspaceCache(nextConfig);
|
|
1234
|
-
setGroupId(data.groupId);
|
|
1235
|
-
setTopics(data.topics || {});
|
|
1236
|
-
setGroupInfo({
|
|
1237
|
-
chat: {
|
|
1238
|
-
id: data.groupId,
|
|
1239
|
-
title: data.groupName || data.groupId,
|
|
1240
|
-
isForum: true,
|
|
1241
|
-
},
|
|
1242
|
-
bot: {
|
|
1243
|
-
status: "administrator",
|
|
1244
|
-
isAdmin: true,
|
|
1245
|
-
canManageTopics: true,
|
|
1246
|
-
},
|
|
1247
|
-
});
|
|
1248
|
-
setVerifyGroupError(null);
|
|
1249
|
-
setAllowUserId("");
|
|
1250
|
-
setStep((currentStep) => (currentStep < 3 ? 3 : currentStep));
|
|
1251
|
-
} catch {}
|
|
1252
|
-
};
|
|
1253
|
-
bootstrapWorkspace();
|
|
1254
|
-
return () => {
|
|
1255
|
-
active = false;
|
|
1256
|
-
};
|
|
1257
|
-
}, []);
|
|
1258
|
-
|
|
1259
|
-
return html`
|
|
1260
|
-
<div class="space-y-4">
|
|
1261
|
-
<${BackButton} onBack=${onBack} />
|
|
1262
|
-
<div class="bg-surface border border-border rounded-xl p-4">
|
|
1263
|
-
${!workspaceConfig.ready
|
|
1264
|
-
? html`
|
|
1265
|
-
<div class="min-h-[220px] flex items-center justify-center">
|
|
1266
|
-
<p class="text-sm text-gray-500">Loading workspace...</p>
|
|
1267
|
-
</div>
|
|
1268
|
-
`
|
|
1269
|
-
: workspaceConfig.configured
|
|
1270
|
-
? html`
|
|
1271
|
-
<div class="flex items-center justify-between mb-4">
|
|
1272
|
-
<div class="flex items-center gap-2">
|
|
1273
|
-
<img
|
|
1274
|
-
src="/assets/icons/telegram.svg"
|
|
1275
|
-
alt=""
|
|
1276
|
-
class="w-5 h-5"
|
|
1277
|
-
/>
|
|
1278
|
-
<h2 class="font-semibold text-sm">
|
|
1279
|
-
Manage Telegram Workspace
|
|
1280
|
-
</h2>
|
|
1281
|
-
</div>
|
|
1282
|
-
</div>
|
|
1283
|
-
<${ManageTelegramWorkspace}
|
|
1284
|
-
groupId=${workspaceConfig.groupId}
|
|
1285
|
-
groupName=${workspaceConfig.groupName}
|
|
1286
|
-
initialTopics=${workspaceConfig.topics}
|
|
1287
|
-
configAgentMaxConcurrent=${workspaceConfig.concurrency
|
|
1288
|
-
?.agentMaxConcurrent}
|
|
1289
|
-
configSubagentMaxConcurrent=${workspaceConfig.concurrency
|
|
1290
|
-
?.subagentMaxConcurrent}
|
|
1291
|
-
debugEnabled=${workspaceConfig.debugEnabled}
|
|
1292
|
-
onResetOnboarding=${resetOnboarding}
|
|
1293
|
-
/>
|
|
1294
|
-
`
|
|
1295
|
-
: html`
|
|
1296
|
-
<div class="flex items-center justify-between mb-4">
|
|
1297
|
-
<div class="flex items-center gap-2">
|
|
1298
|
-
<img
|
|
1299
|
-
src="/assets/icons/telegram.svg"
|
|
1300
|
-
alt=""
|
|
1301
|
-
class="w-5 h-5"
|
|
1302
|
-
/>
|
|
1303
|
-
<h2 class="font-semibold text-sm">
|
|
1304
|
-
Set Up Telegram Workspace
|
|
1305
|
-
</h2>
|
|
1306
|
-
</div>
|
|
1307
|
-
<span class="text-xs text-gray-500"
|
|
1308
|
-
>Step ${step + 1} of ${kSteps.length}</span
|
|
1309
|
-
>
|
|
1310
|
-
</div>
|
|
1311
|
-
|
|
1312
|
-
<${StepIndicator} currentStep=${step} />
|
|
1313
|
-
|
|
1314
|
-
${step === 0 &&
|
|
1315
|
-
html`
|
|
1316
|
-
<${VerifyBotStep}
|
|
1317
|
-
botInfo=${botInfo}
|
|
1318
|
-
setBotInfo=${setBotInfo}
|
|
1319
|
-
onNext=${goNext}
|
|
1320
|
-
/>
|
|
1321
|
-
`}
|
|
1322
|
-
${step === 1 &&
|
|
1323
|
-
html`
|
|
1324
|
-
<${CreateGroupStep} onNext=${goNext} onBack=${goBack} />
|
|
1325
|
-
`}
|
|
1326
|
-
${step === 2 &&
|
|
1327
|
-
html`
|
|
1328
|
-
<${AddBotStep}
|
|
1329
|
-
groupId=${groupId}
|
|
1330
|
-
setGroupId=${setGroupId}
|
|
1331
|
-
groupInfo=${groupInfo}
|
|
1332
|
-
setGroupInfo=${setGroupInfo}
|
|
1333
|
-
userId=${allowUserId}
|
|
1334
|
-
setUserId=${setAllowUserId}
|
|
1335
|
-
verifyGroupError=${verifyGroupError}
|
|
1336
|
-
setVerifyGroupError=${setVerifyGroupError}
|
|
1337
|
-
onNext=${goNext}
|
|
1338
|
-
onBack=${goBack}
|
|
1339
|
-
/>
|
|
1340
|
-
`}
|
|
1341
|
-
${step === 3 &&
|
|
1342
|
-
html`
|
|
1343
|
-
<${TopicsStep}
|
|
1344
|
-
groupId=${groupId}
|
|
1345
|
-
topics=${topics}
|
|
1346
|
-
setTopics=${setTopics}
|
|
1347
|
-
onNext=${goNext}
|
|
1348
|
-
onBack=${goBack}
|
|
1349
|
-
/>
|
|
1350
|
-
`}
|
|
1351
|
-
${step === 4 &&
|
|
1352
|
-
html`
|
|
1353
|
-
<${SummaryStep}
|
|
1354
|
-
groupId=${groupId}
|
|
1355
|
-
groupInfo=${groupInfo}
|
|
1356
|
-
topics=${topics}
|
|
1357
|
-
onBack=${goBack}
|
|
1358
|
-
onDone=${handleDone}
|
|
1359
|
-
/>
|
|
1360
|
-
`}
|
|
1361
|
-
`}
|
|
1362
|
-
</div>
|
|
1363
|
-
</div>
|
|
1364
|
-
`;
|
|
1365
|
-
};
|