feat: Telegram config panel - Alpine store
keyboardstaff committed
Mar 20, 2026 at 10:23 UTC
0504a90a355b813914419676f2f5680056669a23
2 files changed
+418
plugins/_telegram_integration/webui/config.html
new
+311
@@ -0,0 +1,311 @@
1
+<html>
2
+<head>
3
+ <title>Telegram Integration</title>
4
+ <script type="module">
5
+ import { store } from "/plugins/_telegram_integration/webui/telegram-config-store.js";
6
+ </script>
7
+</head>
8
+
9
+<body>
10
+ <div x-data x-init="$store.telegramConfig.init()">
11
+ <template x-if="config">
12
+ <div class="tg-page">
13
+ <div class="section-title">Telegram Integration</div>
14
+ <div class="section-description">
15
+ Configure Telegram bots to communicate with Agent Zero via Telegram.
16
+ Each bot connects to a Telegram Bot API token and handles messages from allowed users.
17
+ </div>
18
+
19
+ <!-- Bot cards -->
20
+ <template x-for="(bot, idx) in (config.bots || [])" :key="idx">
21
+ <div class="bot-card">
22
+ <div class="bot-card-header" @click="$store.telegramConfig.toggle(idx)">
23
+ <span class="material-symbols-outlined bot-expand-icon"
24
+ :class="{ 'expanded': $store.telegramConfig.expandedIdx === idx }">chevron_right</span>
25
+ <span class="bot-card-name" x-text="bot.name || '(unnamed)'"></span>
26
+ <span class="bot-card-summary" x-show="$store.telegramConfig.expandedIdx !== idx"
27
+ x-text="bot.enabled ? 'Enabled' : 'Disabled'"></span>
28
+ <button class="text-button bot-delete-btn"
29
+ @click.stop="$confirmClick($event, () => $store.telegramConfig.removeBot(config, idx))"
30
+ title="Remove bot">
31
+ <span class="material-symbols-outlined">close</span>
32
+ </button>
33
+ </div>
34
+
35
+ <div class="bot-card-body" x-show="$store.telegramConfig.expandedIdx === idx" x-transition.opacity>
36
+
37
+ <div class="field">
38
+ <div class="field-label">
39
+ <div class="field-title">Enabled</div>
40
+ <div class="field-description">Enable or disable this Telegram bot</div>
41
+ </div>
42
+ <div class="field-control">
43
+ <label class="toggle">
44
+ <input type="checkbox" x-model="bot.enabled" />
45
+ <span class="toggler"></span>
46
+ </label>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="field">
51
+ <div class="field-label">
52
+ <div class="field-title">Bot Name</div>
53
+ <div class="field-description">Unique identifier for this bot configuration</div>
54
+ </div>
55
+ <div class="field-control">
56
+ <input type="text" x-model="bot.name" placeholder="e.g. my_bot" />
57
+ </div>
58
+ </div>
59
+
60
+ <div class="field">
61
+ <div class="field-label">
62
+ <div class="field-title">Bot Token</div>
63
+ <div class="field-description">Token from @BotFather on Telegram</div>
64
+ </div>
65
+ <div class="field-control">
66
+ <input type="password" x-model="bot.token" placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
67
+ </div>
68
+ </div>
69
+
70
+ <div class="field">
71
+ <div class="field-label">
72
+ <div class="field-title">Mode</div>
73
+ <div class="field-description">Polling: no public IP needed. Webhook: requires HTTPS endpoint</div>
74
+ </div>
75
+ <div class="field-control">
76
+ <select x-model="bot.mode">
77
+ <option value="polling">Polling</option>
78
+ <option value="webhook">Webhook</option>
79
+ </select>
80
+ </div>
81
+ </div>
82
+
83
+ <div class="field" x-show="bot.mode === 'webhook'">
84
+ <div class="field-label">
85
+ <div class="field-title">Webhook URL</div>
86
+ <div class="field-description">Public HTTPS URL for Telegram to send updates to</div>
87
+ </div>
88
+ <div class="field-control">
89
+ <input type="text" x-model="bot.webhook_url" placeholder="https://yourdomain.com/telegram/my_bot" />
90
+ </div>
91
+ </div>
92
+
93
+ <div class="field" x-show="bot.mode === 'webhook'">
94
+ <div class="field-label">
95
+ <div class="field-title">Webhook Secret</div>
96
+ <div class="field-description">Optional shared secret for webhook verification</div>
97
+ </div>
98
+ <div class="field-control">
99
+ <input type="password" x-model="bot.webhook_secret" />
100
+ </div>
101
+ </div>
102
+
103
+ <div class="field">
104
+ <div class="field-label">
105
+ <div class="field-title">Allowed Users</div>
106
+ <div class="field-description">Comma-separated Telegram user IDs or @usernames. Empty = allow all</div>
107
+ </div>
108
+ <div class="field-control">
109
+ <input type="text"
110
+ :value="$store.telegramConfig.whitelistText(bot)"
111
+ @input="$store.telegramConfig.setWhitelist(bot, $event.target.value)"
112
+ placeholder="123456789, @username" />
113
+ </div>
114
+ </div>
115
+
116
+ <div class="field">
117
+ <div class="field-label">
118
+ <div class="field-title">Group Mode</div>
119
+ <div class="field-description">How the bot responds in group chats</div>
120
+ </div>
121
+ <div class="field-control">
122
+ <select x-model="bot.group_mode">
123
+ <option value="mention">Respond when @mentioned or replied to</option>
124
+ <option value="all">Respond to every message</option>
125
+ <option value="off">Ignore group messages</option>
126
+ </select>
127
+ </div>
128
+ </div>
129
+
130
+ <div class="field">
131
+ <div class="field-label">
132
+ <div class="field-title">User → Project Mapping</div>
133
+ <div class="field-description">Map user IDs to projects. Format: user_id=project_name, comma-separated</div>
134
+ </div>
135
+ <div class="field-control">
136
+ <input type="text"
137
+ :value="$store.telegramConfig.userProjectsText(bot)"
138
+ @input="$store.telegramConfig.setUserProjects(bot, $event.target.value)"
139
+ placeholder="123456=my_project, 789012=other_project" />
140
+ </div>
141
+ </div>
142
+
143
+ <div class="field">
144
+ <div class="field-label">
145
+ <div class="field-title">Default Project</div>
146
+ <div class="field-description">Fallback project if user not in the mapping above</div>
147
+ </div>
148
+ <div class="field-control">
149
+ <select x-model="bot.default_project">
150
+ <option value="">No project</option>
151
+ <template x-for="proj in $store.telegramConfig.projects" :key="proj.name">
152
+ <option :value="proj.name" x-text="proj.title || proj.name"></option>
153
+ </template>
154
+ </select>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="field">
159
+ <div class="field-label">
160
+ <div class="field-title">Attachment Max Age</div>
161
+ <div class="field-description">Auto-delete downloaded attachments older than this many hours. 0 = keep forever</div>
162
+ </div>
163
+ <div class="field-control">
164
+ <input type="number" x-model.number="bot.attachment_max_age_hours" min="0" step="1" placeholder="0" />
165
+ </div>
166
+ </div>
167
+
168
+ <div class="field">
169
+ <div class="field-label">
170
+ <div class="field-title">Agent Instructions</div>
171
+ <div class="field-description">Extra instructions for the agent in Telegram chats</div>
172
+ </div>
173
+ <div class="field-control">
174
+ <textarea x-model="bot.agent_instructions" rows="3" placeholder="e.g. Always respond concisely for mobile..."></textarea>
175
+ </div>
176
+ </div>
177
+
178
+ <!-- Test connection + results -->
179
+ <div class="bot-test-row">
180
+ <button class="btn btn-field"
181
+ @click.stop="$store.telegramConfig.testConnection(config, idx)"
182
+ :disabled="$store.telegramConfig.testing === idx">
183
+ <span x-show="$store.telegramConfig.testing !== idx">Test Connection</span>
184
+ <span x-show="$store.telegramConfig.testing === idx">Testing...</span>
185
+ </button>
186
+ <template x-if="$store.telegramConfig.testResults && $store.telegramConfig.expandedIdx === idx">
187
+ <div class="bot-test-results">
188
+ <template x-for="r in $store.telegramConfig.testResults.results" :key="r.test">
189
+ <div class="bot-test-result-row">
190
+ <span class="bot-test-icon"
191
+ x-text="r.ok ? '✓' : '✗'"
192
+ :class="r.ok ? 'ok' : 'fail'"></span>
193
+ <span class="bot-test-label" x-text="r.test"></span>
194
+ <span class="bot-test-msg" x-text="r.message"></span>
195
+ </div>
196
+ </template>
197
+ </div>
198
+ </template>
199
+ </div>
200
+
201
+ </div>
202
+ </div>
203
+ </template>
204
+
205
+ <button class="text-button bot-add-btn" @click="$store.telegramConfig.addBot(config)">
206
+ <span class="material-symbols-outlined">add</span>
207
+ <span>Add Bot</span>
208
+ </button>
209
+ </div>
210
+ </template>
211
+ </div>
212
+
213
+ <style>
214
+ .tg-page {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 8px;
218
+ }
219
+ .bot-card {
220
+ border: 1px solid var(--color-border);
221
+ border-radius: 6px;
222
+ overflow: hidden;
223
+ }
224
+ .bot-card-header {
225
+ display: flex;
226
+ align-items: center;
227
+ gap: 6px;
228
+ padding: 8px 10px;
229
+ cursor: pointer;
230
+ font-size: 0.85rem;
231
+ }
232
+ .bot-card-header:hover {
233
+ background: var(--color-background-hover, rgba(255,255,255,0.04));
234
+ }
235
+ .bot-expand-icon {
236
+ font-size: 16px;
237
+ transition: transform 0.15s ease;
238
+ }
239
+ .bot-expand-icon.expanded {
240
+ transform: rotate(90deg);
241
+ }
242
+ .bot-card-name {
243
+ font-weight: 500;
244
+ }
245
+ .bot-card-summary {
246
+ flex: 1;
247
+ text-align: right;
248
+ opacity: 0.5;
249
+ font-size: 0.75rem;
250
+ overflow: hidden;
251
+ text-overflow: ellipsis;
252
+ white-space: nowrap;
253
+ }
254
+ .bot-delete-btn {
255
+ margin-left: auto;
256
+ opacity: 0.5;
257
+ padding: 2px !important;
258
+ }
259
+ .bot-delete-btn:hover {
260
+ opacity: 1;
261
+ color: var(--color-error, #f44) !important;
262
+ }
263
+ .bot-delete-btn .material-symbols-outlined {
264
+ font-size: 16px;
265
+ }
266
+ .bot-card-body {
267
+ padding: 4px 12px 12px;
268
+ border-top: 1px solid var(--color-border);
269
+ }
270
+ .bot-add-btn {
271
+ margin-top: 4px;
272
+ }
273
+ .bot-test-row {
274
+ margin-top: 12px;
275
+ display: flex;
276
+ align-items: flex-start;
277
+ gap: 12px;
278
+ flex-wrap: wrap;
279
+ }
280
+ .bot-test-results {
281
+ padding: 4px 0;
282
+ font-size: 0.85rem;
283
+ flex: 1;
284
+ min-width: 200px;
285
+ }
286
+ .bot-test-result-row {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 8px;
290
+ padding: 4px 0;
291
+ }
292
+ .bot-test-icon {
293
+ font-weight: bold;
294
+ }
295
+ .bot-test-icon.ok {
296
+ color: #4caf50;
297
+ }
298
+ .bot-test-icon.fail {
299
+ color: #f44336;
300
+ }
301
+ .bot-test-label {
302
+ font-weight: 500;
303
+ min-width: 80px;
304
+ }
305
+ .bot-test-msg {
306
+ opacity: 0.8;
307
+ }
308
+ </style>
309
+</body>
310
+
311
+</html>
plugins/_telegram_integration/webui/telegram-config-store.js
new
+107
@@ -0,0 +1,107 @@
1
+import { createStore } from "/js/AlpineStore.js";
2
+
3
+const API_BASE = "/plugins/_telegram_integration";
4
+
5
+export const store = createStore("telegramConfig", {
6
+ projects: [],
7
+ expandedIdx: null,
8
+ testing: null,
9
+ testResults: null,
10
+ _loaded: false,
11
+
12
+ async init() {
13
+ if (this._loaded) return;
14
+ try {
15
+ const { callJsonApi } = await import("/js/api.js");
16
+ const res = await callJsonApi("projects", { action: "list" });
17
+ this.projects = res.data || [];
18
+ } catch (_) {
19
+ this.projects = [];
20
+ }
21
+ this._loaded = true;
22
+ },
23
+
24
+ defaultBot() {
25
+ return {
26
+ name: "",
27
+ enabled: false,
28
+ token: "",
29
+ mode: "polling",
30
+ webhook_url: "",
31
+ webhook_secret: "",
32
+ allowed_users: [],
33
+ group_mode: "mention",
34
+ user_projects: {},
35
+ default_project: "",
36
+ agent_instructions: "",
37
+ attachment_max_age_hours: 0,
38
+ };
39
+ },
40
+
41
+ addBot(config) {
42
+ if (!config.bots) config.bots = [];
43
+ const bot = this.defaultBot();
44
+ bot.name = "bot_" + (config.bots.length + 1);
45
+ config.bots.push(bot);
46
+ this.expandedIdx = config.bots.length - 1;
47
+ },
48
+
49
+ removeBot(config, idx) {
50
+ config.bots.splice(idx, 1);
51
+ this.expandedIdx = null;
52
+ },
53
+
54
+ toggle(idx) {
55
+ this.expandedIdx = this.expandedIdx === idx ? null : idx;
56
+ this.testResults = null;
57
+ },
58
+
59
+ whitelistText(bot) {
60
+ return (bot.allowed_users || []).join(", ");
61
+ },
62
+
63
+ setWhitelist(bot, val) {
64
+ bot.allowed_users = val
65
+ .split(",")
66
+ .map((s) => s.trim())
67
+ .filter((s) => s);
68
+ },
69
+
70
+ userProjectsText(bot) {
71
+ const up = bot.user_projects || {};
72
+ return Object.entries(up)
73
+ .map(([k, v]) => k + "=" + v)
74
+ .join(", ");
75
+ },
76
+
77
+ setUserProjects(bot, val) {
78
+ const obj = {};
79
+ val
80
+ .split(",")
81
+ .map((s) => s.trim())
82
+ .filter((s) => s)
83
+ .forEach((item) => {
84
+ const [k, v] = item.split("=").map((p) => p.trim());
85
+ if (k && v) obj[k] = v;
86
+ });
87
+ bot.user_projects = obj;
88
+ },
89
+
90
+ async testConnection(config, idx) {
91
+ this.testing = idx;
92
+ this.testResults = null;
93
+ try {
94
+ const { callJsonApi } = await import("/js/api.js");
95
+ const res = await callJsonApi(`${API_BASE}/test_connection`, {
96
+ bot: config.bots[idx],
97
+ });
98
+ this.testResults = res;
99
+ } catch (e) {
100
+ this.testResults = {
101
+ success: false,
102
+ results: [{ test: "Connection", ok: false, message: String(e) }],
103
+ };
104
+ }
105
+ this.testing = null;
106
+ },
107
+});