@chrysb/alphaclaw 0.1.25 → 0.2.0

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