main
html 680 lines 23.6 KB
Raw
1 <html>
2
3 <head>
4 <title>Telegram Integration</title>
5 <script type="module">
6 import { store } from "/plugins/_telegram_integration/webui/telegram-config-store.js";
7 </script>
8 </head>
9
10 <body>
11 <div x-data x-init="$store.telegramConfig.init(config, context)" x-destroy="$store.telegramConfig.cleanup()">
12 <template x-if="config">
13 <div class="tg-page">
14 <div class="section-title">Telegram</div>
15
16 <template x-if="$store.telegramConfig.didInit && $store.telegramConfig.bots.length === 0">
17 <div class="tg-empty">
18 <div class="tg-empty-title">Start with one bot</div>
19 <div class="tg-empty-copy">
20 Most setups only need a bot token, one allowed user, and the default polling mode.
21 </div>
22 <button class="btn btn-field" @click="$store.telegramConfig.addBot()">
23 Connect a bot
24 </button>
25 </div>
26 </template>
27
28 <template x-for="(bot, idx) in $store.telegramConfig.bots" :key="idx">
29 <div class="tg-card">
30 <div class="tg-card-header" @click="$store.telegramConfig.toggleEditing(idx)">
31 <div class="tg-card-heading">
32 <div class="tg-card-title" x-text="$store.telegramConfig.botTitle(bot, idx)"></div>
33 <div class="tg-card-subtitle" x-text="$store.telegramConfig.botSubtitle(bot)"></div>
34 </div>
35 <div class="tg-card-actions">
36 <span class="tg-status-pill" :class="'tone-' + $store.telegramConfig.botStatusTone(bot)"
37 x-text="$store.telegramConfig.botStatusLabel(bot)"></span>
38 <button class="btn btn-action delete"
39 @click.stop="$confirmClick($event, () => $store.telegramConfig.removeBot(idx))" title="Remove bot">
40 <x-icon name="delete"></x-icon>
41 </button>
42 <x-icon class="tg-card-chevron"
43 :class="{ 'is-open': $store.telegramConfig.editing === idx }" name="expand_more"></x-icon>
44 </div>
45 </div>
46
47 <template x-if="$store.telegramConfig.editing === idx">
48 <div class="tg-card-body">
49 <div class="tg-step-header">
50 <div class="tg-step-copy">
51 <div class="tg-step-title" x-text="$store.telegramConfig.currentStepMeta().title"></div>
52 <div class="tg-step-description" x-text="$store.telegramConfig.currentStepMeta().description"></div>
53 </div>
54 <div class="tg-step-dots" aria-hidden="true">
55 <template x-for="(step, stepIdx) in $store.telegramConfig.steps" :key="step.title">
56 <button type="button" class="tg-step-dot"
57 :class="{ 'is-active': $store.telegramConfig.currentStep === stepIdx }"
58 @click="$store.telegramConfig.setStep(stepIdx)"></button>
59 </template>
60 </div>
61 </div>
62
63 <template x-if="$store.telegramConfig.currentStep === 0">
64 <div class="tg-step-panel">
65 <div class="tg-token-row">
66 <div class="tg-qr-card">
67 <div class="tg-qr-title">Scan to open @BotFather</div>
68 <img class="tg-qr-image" :src="$store.telegramConfig.botFatherQr"
69 alt="Scan to open @BotFather" />
70 </div>
71
72 <div class="tg-token-card">
73 <div class="tg-token-steps">
74 <div class="tg-token-steps-title">Create your bot</div>
75 <ol class="tg-token-steps-list">
76 <li>Open <a href="https://t.me/BotFather" target="_blank" rel="noopener">@BotFather</a></li>
77 <li>Send <strong>/newbot</strong> and follow the prompts</li>
78 <li>Paste the token here</li>
79 </ol>
80 </div>
81
82 <div class="field">
83 <div class="field-label">
84 <div class="field-title">Bot token</div>
85 <div class="field-description">Paste the token you got from @BotFather.</div>
86 </div>
87 <div class="field-control">
88 <input type="text" class="masked-text-input" autocomplete="off" x-model="bot.token"
89 placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" />
90 </div>
91 </div>
92 </div>
93 </div>
94 </div>
95 </template>
96
97 <template x-if="$store.telegramConfig.currentStep === 1">
98 <div class="tg-step-panel">
99 <div class="field">
100 <div class="field-label">
101 <div class="field-title">Turn on this bot</div>
102 <div class="field-description">Enable message polling when you are ready for the bot to go live.
103 </div>
104 </div>
105 <div class="field-control">
106 <label class="toggle">
107 <input type="checkbox" x-model="bot.enabled" />
108 <span class="toggler"></span>
109 </label>
110 </div>
111 </div>
112
113 <div class="field">
114 <div class="field-label">
115 <div class="field-title">Bot name</div>
116 <div class="field-description">A friendly internal label so you can tell bots apart.</div>
117 </div>
118 <div class="field-control">
119 <input type="text" x-model="bot.name" placeholder="support_bot" />
120 </div>
121 </div>
122
123 <div class="field">
124 <div class="field-label">
125 <div class="field-title">Delivery mode</div>
126 <div class="field-description">Polling is the simplest option. Webhook is for public HTTPS
127 setups.</div>
128 </div>
129 <div class="field-control">
130 <select x-model="bot.mode">
131 <option value="polling">Polling</option>
132 <option value="webhook">Webhook</option>
133 </select>
134 </div>
135 </div>
136
137 <div class="field" x-show="bot.mode === 'webhook'">
138 <div class="field-label">
139 <div class="field-title">Webhook URL</div>
140 <div class="field-description">Your public Agent Zero base URL, for example
141 https://yourdomain.com.</div>
142 </div>
143 <div class="field-control">
144 <input type="text" x-model="bot.webhook_url" placeholder="https://yourdomain.com" />
145 </div>
146 </div>
147
148 <div class="field">
149 <div class="field-label">
150 <div class="field-title">Allowed users</div>
151 <div class="field-description">Comma-separated @usernames or numeric IDs. Leave empty only if
152 you truly want open access.</div>
153 </div>
154 <div class="field-control">
155 <input type="text" :value="$store.telegramConfig.whitelistText(bot)"
156 @input="$store.telegramConfig.setWhitelist(bot, $event.target.value)"
157 placeholder="@yourname, 123456789" />
158 </div>
159 </div>
160
161 <div class="tg-warning" x-show="$store.telegramConfig.accessWarning(bot)">
162 <span x-text="$store.telegramConfig.accessWarning(bot)"></span>
163 </div>
164
165 <div class="field">
166 <div class="field-label">
167 <div class="field-title">Default project</div>
168 <div class="field-description">Optional fallback project for conversations from this bot.</div>
169 </div>
170 <div class="field-control">
171 <select :value="bot.default_project" @change="bot.default_project = $event.target.value">
172 <option value="">No project</option>
173 <template x-for="proj in $store.telegramConfig.projects" :key="proj.name">
174 <option :value="proj.name" x-text="proj.title || proj.name"
175 :selected="bot.default_project === proj.name"></option>
176 </template>
177 </select>
178 </div>
179 </div>
180 </div>
181 </template>
182
183 <template x-if="$store.telegramConfig.currentStep === 2">
184 <div class="tg-step-panel">
185 <div class="field">
186 <div class="field-label">
187 <div class="field-title">Group behavior</div>
188 <div class="field-description">Choose whether the bot replies only when invited into the
189 conversation or to everything in a group.</div>
190 </div>
191 <div class="field-control">
192 <select x-model="bot.group_mode">
193 <option value="mention">Reply when @mentioned or replied to</option>
194 <option value="all">Reply to every group message</option>
195 <option value="off">Ignore group messages</option>
196 </select>
197 </div>
198 </div>
199
200 <div class="field" x-show="bot.group_mode !== 'off'">
201 <div class="field-label">
202 <div class="field-title">Welcome new members</div>
203 <div class="field-description">Send a greeting when someone joins the group.</div>
204 </div>
205 <div class="field-control">
206 <label class="toggle">
207 <input type="checkbox" x-model="bot.welcome_enabled" />
208 <span class="toggler"></span>
209 </label>
210 </div>
211 </div>
212
213 <div class="field" x-show="bot.group_mode !== 'off' && bot.welcome_enabled">
214 <div class="field-label">
215 <div class="field-title">Welcome message</div>
216 <div class="field-description">Use {name} for the new member's name.</div>
217 </div>
218 <div class="field-control">
219 <input type="text" x-model="bot.welcome_message" placeholder="Welcome, {name}!" />
220 </div>
221 </div>
222
223 <div class="field">
224 <div class="field-label">
225 <div class="field-title">Agent instructions</div>
226 <div class="field-description">Extra guidance for how the agent should reply in Telegram chats.
227 </div>
228 </div>
229 <div class="field-control">
230 <textarea x-model="bot.agent_instructions" rows="3"
231 placeholder="Reply in a concise, mobile-friendly way."></textarea>
232 </div>
233 </div>
234 </div>
235 </template>
236
237 <div class="tg-test-panel">
238 <div class="tg-test-copy">
239 <div class="tg-test-title">Check the connection</div>
240 <div class="tg-test-description" x-text="$store.telegramConfig.testIntro()"></div>
241 </div>
242 <button class="btn btn-field" @click.stop="$store.telegramConfig.testConnection(idx)"
243 :disabled="$store.telegramConfig.testing === idx || !$store.telegramConfig.canTest(bot)">
244 <span x-text="$store.telegramConfig.testButtonLabel(bot, idx)"></span>
245 </button>
246 </div>
247
248 <template x-if="$store.telegramConfig.testResults && $store.telegramConfig.testResultsFor === idx">
249 <div class="tg-results" :class="{ 'is-error': !$store.telegramConfig.testResults.success }">
250 <template x-for="result in $store.telegramConfig.testResults.results"
251 :key="result.test + result.message">
252 <div class="tg-result-row">
253 <span class="tg-result-icon" x-text="result.ok ? '✓' : '✗'"></span>
254 <div class="tg-result-copy">
255 <div class="tg-result-title" x-text="$store.telegramConfig.resultTitle(result)"></div>
256 <div class="tg-result-message" x-text="$store.telegramConfig.resultMessage(result)"></div>
257 </div>
258 </div>
259 </template>
260 </div>
261 </template>
262
263 <template x-if="$store.telegramConfig.currentStep > 0">
264 <details class="tg-advanced">
265 <summary>
266 <span>Advanced</span>
267 <x-icon class="tg-advanced-chevron"
268 aria-hidden="true" name="keyboard_arrow_down"></x-icon>
269 </summary>
270 <div class="tg-advanced-body">
271 <div class="field" x-show="bot.mode === 'webhook'">
272 <div class="field-label">
273 <div class="field-title">Webhook secret</div>
274 <div class="field-description">Optional shared secret for webhook verification.</div>
275 </div>
276 <div class="field-control">
277 <input type="text" class="masked-text-input" autocomplete="off" x-model="bot.webhook_secret" />
278 </div>
279 </div>
280
281 <div class="field">
282 <div class="field-label">
283 <div class="field-title">Message notifications</div>
284 <div class="field-description">Show a WebUI notification for each incoming Telegram message.
285 </div>
286 </div>
287 <div class="field-control">
288 <label class="toggle">
289 <input type="checkbox" x-model="bot.notify_messages" />
290 <span class="toggler"></span>
291 </label>
292 </div>
293 </div>
294
295 <div class="field">
296 <div class="field-label">
297 <div class="field-title">User project mapping</div>
298 <div class="field-description">Map specific Telegram user IDs to projects with user_id=project
299 entries.</div>
300 </div>
301 <div class="field-control">
302 <input type="text" :value="$store.telegramConfig.userProjectsText(bot)"
303 @input="$store.telegramConfig.setUserProjects(bot, $event.target.value)"
304 placeholder="123456=my_project, 789012=other_project" />
305 </div>
306 </div>
307
308 <div class="field">
309 <div class="field-label">
310 <div class="field-title">Attachment max age</div>
311 <div class="field-description">How long to keep downloaded attachments. Use 0 to keep them
312 forever.</div>
313 </div>
314 <div class="field-control">
315 <input type="number" x-model.number="bot.attachment_max_age_hours" min="0" step="1"
316 placeholder="0" />
317 </div>
318 </div>
319
320 </div>
321 </details>
322 </template>
323 </div>
324 </template>
325 </div>
326 </template>
327
328 <template x-if="$store.telegramConfig.bots.length > 0">
329 <button class="btn btn-field tg-add-btn" @click="$store.telegramConfig.addBot()">
330 Connect another bot
331 </button>
332 </template>
333 </div>
334 </template>
335 </div>
336
337 <style>
338 .tg-page {
339 display: flex;
340 flex-direction: column;
341 gap: 0.9rem;
342 }
343
344 .tg-advanced summary {
345 cursor: pointer;
346 list-style: none;
347 font-weight: 700;
348 display: flex;
349 align-items: center;
350 gap: 0.75rem;
351 }
352
353 .tg-advanced summary::-webkit-details-marker {
354 display: none;
355 }
356
357 .tg-guide-card,
358 .tg-empty,
359 .tg-card {
360 border-radius: 0.5rem;
361 }
362
363 .tg-empty-title,
364 .tg-step-title,
365 .tg-test-title,
366 .tg-qr-title {
367 font-weight: 700;
368 }
369
370 .tg-empty-copy,
371 .tg-step-description,
372 .tg-info-box,
373 .tg-test-description,
374 .tg-result-message,
375 .tg-card-subtitle {
376 color: var(--color-text-secondary);
377 line-height: 1.5;
378 }
379
380 .tg-empty {
381 padding: 1rem;
382 }
383
384 .tg-empty-copy {
385 margin-top: 0.35rem;
386 margin-bottom: 0.9rem;
387 }
388
389 .tg-card-header {
390 display: flex;
391 justify-content: space-between;
392 gap: 1rem;
393 align-items: flex-start;
394 padding: var(--spacing-sm) 0;
395 border-bottom: 1px solid var(--color-border);
396 cursor: pointer;
397 }
398
399 .tg-card-heading {
400 min-width: 0;
401 flex: 1 1 auto;
402 }
403
404 .tg-card-title {
405 font-weight: 700;
406 line-height: 1.35;
407 overflow-wrap: anywhere;
408 }
409
410 .tg-card-subtitle {
411 margin-top: 0.3rem;
412 font-size: var(--font-size-small);
413 }
414
415 .tg-card-actions {
416 display: flex;
417 align-items: center;
418 gap: 0.45rem;
419 flex: 0 0 auto;
420 }
421
422 .tg-card-chevron {
423 transition: transform 0.18s ease;
424 opacity: 0.7;
425 }
426
427 .tg-card-chevron.is-open {
428 transform: rotate(180deg);
429 }
430
431 .tg-card-body {
432 border-top: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
433 }
434
435 .tg-step-header {
436 display: flex;
437 justify-content: space-between;
438 gap: 1rem;
439 align-items: flex-start;
440 margin: 1rem 0;
441 }
442
443 .tg-step-dots {
444 display: flex;
445 gap: 0.5rem;
446 flex-wrap: wrap;
447 justify-content: flex-end;
448 align-items: center;
449 }
450
451 .tg-step-dot {
452 width: 0.85rem;
453 height: 0.85rem;
454 border-radius: 999px;
455 border: 1px solid var(--color-border);
456 background: transparent;
457 cursor: pointer;
458 padding: 0;
459 }
460
461 .tg-step-dot.is-active {
462 background: rgba(59, 130, 246, 0.9);
463 border-color: rgba(59, 130, 246, 0.9);
464 }
465
466 .tg-step-panel {
467 display: flex;
468 flex-direction: column;
469 gap: 0.2rem;
470 }
471
472 .tg-token-row {
473 display: grid;
474 grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
475 gap: 1rem;
476 align-items: stretch;
477 }
478
479 .tg-qr-card,
480 .tg-token-card {
481 border-radius: 0.5rem;
482 background: color-mix(in srgb, var(--color-background) 90%, white 10%);
483 padding: 1rem;
484 }
485
486 .tg-qr-card {
487 display: flex;
488 flex-direction: column;
489 align-items: center;
490 text-align: center;
491 gap: 0.8rem;
492 }
493
494 .tg-qr-image {
495 width: 150px;
496 height: 150px;
497 border-radius: 0.5rem;
498 image-rendering: pixelated;
499 }
500
501 .tg-token-steps {
502 margin-bottom: 0.9rem;
503 }
504
505 .tg-token-steps-title {
506 font-weight: 700;
507 margin-bottom: 0.45rem;
508 }
509
510 .tg-token-steps-list {
511 margin: 0;
512 padding-left: 1.15rem;
513 color: var(--color-text-secondary);
514 line-height: 1.55;
515 }
516
517 .tg-token-steps-list a {
518 color: var(--color-highlight);
519 }
520
521 .tg-info-box,
522 .tg-warning {
523 margin-bottom: 0.9rem;
524 padding: 0.8rem 0.9rem;
525 border-radius: 12px;
526 font-size: var(--font-size-small);
527 }
528
529 .tg-info-box {
530 background: color-mix(in srgb, #3b82f6 12%, var(--color-background) 88%);
531 }
532
533 .tg-warning {
534 background: color-mix(in srgb, #f59e0b 14%, var(--color-background) 86%);
535 }
536
537 .tg-advanced {
538 margin-top: 0.95rem;
539 border: 1px solid color-mix(in srgb, var(--color-border) 88%, white 12%);
540 border-radius: 14px;
541 overflow: hidden;
542 }
543
544 .tg-advanced summary {
545 padding: 0.9rem 1rem;
546 background: color-mix(in srgb, var(--color-background) 88%, white 12%);
547 }
548
549 .tg-advanced-chevron {
550 margin-left: auto;
551 flex: 0 0 auto;
552 transition: transform 0.18s ease, opacity 0.18s ease;
553 opacity: 0.72;
554 }
555
556 .tg-advanced[open] .tg-advanced-chevron {
557 transform: rotate(180deg);
558 opacity: 1;
559 }
560
561 .tg-advanced-body {
562 padding: 1rem;
563 display: flex;
564 flex-direction: column;
565 gap: 0.2rem;
566 }
567
568 .tg-test-panel {
569 margin-top: 1rem;
570 padding: var(--spacing-xs) 0;
571 display: flex;
572 justify-content: space-between;
573 align-items: center;
574 gap: 1rem;
575 }
576
577 .tg-test-copy,
578 .tg-result-copy {
579 min-width: 0;
580 }
581
582 .tg-results {
583 margin-top: 0.9rem;
584 border-radius: 14px;
585 border: 1px solid color-mix(in srgb, #22c55e 28%, var(--color-border) 72%);
586 background: color-mix(in srgb, #22c55e 8%, var(--color-background) 92%);
587 padding: 0.35rem 0.9rem;
588 }
589
590 .tg-results.is-error {
591 border-color: color-mix(in srgb, #ef4444 28%, var(--color-border) 72%);
592 background: color-mix(in srgb, #ef4444 8%, var(--color-background) 92%);
593 }
594
595 .tg-result-row {
596 display: flex;
597 align-items: flex-start;
598 gap: 0.8rem;
599 padding: 0.7rem 0;
600 }
601
602 .tg-result-row+.tg-result-row {
603 border-top: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
604 }
605
606 .tg-result-icon {
607 width: 1.25rem;
608 font-weight: 800;
609 line-height: 1.3;
610 }
611
612 .tg-result-title {
613 font-weight: 700;
614 margin-bottom: 0.18rem;
615 }
616
617 .tg-status-pill {
618 display: inline-flex;
619 align-items: center;
620 justify-content: center;
621 min-width: 5.25rem;
622 padding: 0.35rem 0.7rem;
623 border-radius: 999px;
624 font-size: 0.8rem;
625 font-weight: 700;
626 }
627
628 .tg-status-pill.tone-success {
629 background: rgba(34, 197, 94, 0.14);
630 color: #7ee7a4;
631 }
632
633 .tg-status-pill.tone-ready {
634 background: rgba(59, 130, 246, 0.16);
635 color: #93c5fd;
636 }
637
638 .tg-status-pill.tone-warning {
639 background: rgba(245, 158, 11, 0.16);
640 color: #fcd34d;
641 }
642
643 .tg-status-pill.tone-muted {
644 background: rgba(148, 163, 184, 0.14);
645 color: #cbd5e1;
646 }
647
648 .tg-add-btn {
649 align-self: flex-start;
650 }
651
652 @media (max-width: 900px) {
653 .tg-token-row {
654 grid-template-columns: minmax(0, 1fr);
655 }
656 }
657
658 @media (max-width: 640px) {
659
660 .tg-card-header,
661 .tg-step-header,
662 .tg-test-panel {
663 flex-direction: column;
664 align-items: stretch;
665 }
666
667 .tg-card-actions,
668 .tg-step-dots {
669 width: 100%;
670 justify-content: space-between;
671 }
672
673 .tg-status-pill {
674 min-width: 0;
675 }
676 }
677 </style>
678 </body>
679
680 </html>