UNTUK APA MEREKA
Contoh aplikasi
Sebuah layar pemesanan, sebuah pencarian, sebuah reservasi, sebuah tarif. Setiap kartu di bawah dibangun dari simpul-simpul yang dibagikan halaman ini, dan semuanya hidup — tekan saja. Ketika sebuah layar butuh sesuatu yang tidak ada dalam kosakata, itu berarti kosakatanya kurang satu simpul: place, time, item dan total adalah empat yang lahir dari merakit kartu-kartu ini.
Dua baris dari sebuah menu, ukurannya, apa yang di atasnya, dan jadi berapa. Seluruh pesanan itu satu pesan yang terus disunting.
itemitemchoicechecklisttotalbutton
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+GAGASANNYA
Apa itu widget
Papan tombol adalah barisan tombol, jadi setiap pertanyaan yang bukan tombol yang mana berubah menjadi percakapan: satu pesan per nilai, dan sama sekali tidak ada cara mengumpulkan satu himpunan, satu jumlah atau satu hari di tempat. Widget adalah jawabannya: sebuah kartu yang digambar aplikasi dari JSON yang dikirim bot, di dalam pesan yang sama, dengan kanal callback yang sama di bawahnya.
Model keadaannya adalah model yang sudah dimiliki papan tombol, hanya dipertajam: keadaan itu milik bot, dan klien tidak memegang apa pun yang tidak diberikan kepadanya. Setiap ketukan adalah string peristiwa biasa; bot memperbarui salinannya sendiri lalu mendorong spesifikasi berikutnya dengan editMessageWidget, dan kartunya tergambar ulang di tempat. sendMessage menerima parameter widget, jadi spesifikasinya ikut menumpang pengiriman, dan text pesan itu adalah cadangan bagi klien yang tak bisa menggambarnya.
POST /bot$KEY/sendMessage { "chat_id": -1001, "text": "Order #4821 — packed", // the fallback "widget": { "type": "card", "gap": "xs", "children": [ { "type": "row", "gap": "sm", "children": [ { "type": "icon", "icon": "cart", "tone": "accent" }, { "type": "text", "text": "Order #4821", "weight": "bold" }, { "type": "badge", "text": "PACKED", "tone": "muted" } ] }, { "type": "progress", "value": 0.66 }, { "type": "row", "gap": "sm", "children": [ { "type": "button", "label": "Advance", "primary": true, "tap": "card:step" }, { "type": "button", "label": "Reset", "tap": "card:reset" } ] } ] } } # the tap arrives as an ordinary callback_query with data "card:step", # and the bot pushes the next state into the same message: POST /bot$KEY/editMessageWidget { "chat_id": -1001, "message_id": 19, "widget": { …the next spec… } }
PERAKITNYA
Rakit satu
Pilih sebuah kendali, putar tombol-tombolnya, lalu tekan kartunya. JSON itulah yang dikirim botmu ke sendMessage; string di bawahnya adalah keseluruhan yang kembali ketika seseorang menekan apa yang kamu gambar.
stepper menanyakan berapa banyak, dengan batas.
Jawaban Telegram adalah dua tombol dan sebuah pencacah yang dituliskan ke dalam teksnya.
curl -X POST "$BASE/bot$KEY/sendMessage" \ -H 'content-type: application/json' \ -d '{ "chat_id": -1001, "text": "People — open the app to answer", "widget": { "type": "card", "gap": "sm", "children": [ { "type": "stepper", "label": "People", "min": 1, "max": 8, "value": 2, "tap": "seats:" } ] } }'
Tekan kartunya di telepon. Apa pun yang dilakukan kendalinya, yang sampai ke bot adalah satu string di cbq — tidak ada lagi yang muat di kanal itu.
BEBERAPA SEKALIGUS
Susun satu kartu
Satu kendali menjawab satu pertanyaan, dan hampir tidak ada yang ditanyakan bot yang hanya satu pertanyaan. Sebuah reservasi adalah sebuah hitungan, sebuah hari, sebuah pengaturan dan sebuah tombol. card adalah simpul yang memegang mereka, dan anak-anaknya adalah sebuah daftar — jadi daftar itulah tata letaknya. Mulailah dari salah satu ini, atau tambahkan baris padanya.
Menyunting stepper — ia menanyakan berapa banyak, dengan batas.
curl -X POST "$BASE/bot$KEY/sendMessage" \ -H 'content-type: application/json' \ -d '{ "chat_id": -1001, "text": "Open the app to answer", "widget": { "type": "card", "gap": "sm", "children": [ { "type": "stepper", "label": "People", "min": 1, "max": 8, "value": 2, "tap": "seats:" }, { "type": "date", "label": "Day", "placeholder": "Pick a day", "tap": "day:" }, { "type": "switch", "label": "Window seat", "value": false, "tap": "window" }, { "type": "button", "label": "Book the table", "tap": "book" } ] } }'
Tekan baris mana pun dari kartunya di telepon. Berapa pun banyak simpul yang dipegang sebuah kartu, yang sampai ke bot adalah satu string — dan itulah sebabnya tiap simpul yang bisa dialamati membawa tap-nya sendiri.
KOSAKATANYA
Kosakatanya adalah daftar putih
- tata letak —
column,row,wrap,box,card,space,divider - tampilan —
text,heading,note,banner,icon,badge,progress,markdown,code,spinner,glossary,total - masukan —
field,check,switch,choice,select,checklist,stepper,slider,rating,date,time,place,button,item,gallery,pagination,tree - gabungan —
checkbox_group,payment_request,swap_panel,ticket_offer: kartu-kartu utuh yang digambar aplikasi dari keadaan yang dipasok bot - jendela —
mini_app: satu-satunya simpul yang menggambar sesuatu yang tidak ditulis aplikasi ini, dan sengaja ditaruh terakhir di tabel. Ia terbuka di atas percakapan, bukan menggantikannya.
Nama yang tidak ada di daftar akan menggambar sebuah kotak yang menyebutkan namanya, alih-alih tidak menggambar apa pun — formulir yang diam-diam kehilangan satu baris adalah formulir yang dikirim orang sambil mengira sudah lengkap. Penampilan itu sebuah token ("tone": "accent", "size": "lg"), yang diselesaikan terhadap palet aplikasi: tidak ada properti warna, tidak ada properti piksel, dan tidak ada ikon di luar daftar putih — dan itulah yang mencegah sebuah bot menggambar sesuatu berbentuk lembar konfirmasi milik aplikasi ini sendiri di dalam percakapan yang sudah dipercayai seseorang. "color" mentah diabaikan, dan ada tes yang menyatakan itu.
Bagaimana sebuah nilai kembali lewat kanal yang hanya membawa satu string
- Kendali yang nilainya adalah simpul itu sendiri —
button,check,switch— mengirim stringtap-nya tanpa perubahan. Bot memegang keadaan yang lama dan tahu keadaan barunya harus apa. - Kendali yang membawa sebuah nilai —
select,choice,checklist,stepper,slider,rating,date,field— mengirimtapdengan nilainya ditempel di belakang: dengan"tap": "form:seats", memilih empat tiba sebagaiform:seats:4. Teks bebas adalah segmen terakhir (todo:rename:2:Buy oat milk), jadi potong dari belakang, jangan pecah di setiap titik dua. - Simpul tanpa
tapdigambar diam. Begitulah cara sebuah spesifikasi berkata “ini sedang menunjukkan, bukan bertanya” — dan formulir yang sudah selesai mengirim spesifikasi berikutnya tanpa satu pun stringtap, dan begitulah ia berhenti bisa ditekan. callback_databerukuran 64 bita di Bot API, jadi nilai yang dipilih harus pendek: daftar kode aset boleh, daftar kalimat tidak.
Setiap kendali, dan apa yang kembali darinya
Satu tabel, karena seluruh protokolnya ada di dalamnya. Kirim simpul di sebelah kiri; ketukan di sebelah kanan adalah yang tiba sebagai callback_query.data. Setiap satu di antaranya diangkat dari examples/widgetbot.rs, sebuah bot yang bisa kamu jalankan.
| Simpul yang kamu kirim | Yang kembali saat ditekan |
|---|---|
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"} | form:placesimpulnya adalah nilainya |
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"} | form:remindtelanjang — bit lamanya kamu yang pegang dan kamu tahu yang baru |
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"} | form:seats:4nilainya ditempel di belakang |
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"} | form:sitting:earlyvalue pilihan itu, tidak pernah labelnya |
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"} | form:extras:cakeitem yang disentuh, bukan himpunan barunya — kamu yang membaliknya |
{"type": "slider", "id": "tip", "value": 10, "min": 0, "max": 30, "step": 5, "suffix": "%", "tap": "form:tip"} | form:tip:15 |
{"type": "rating", "id": "stars", "value": 4, "max": 5, "tap": "form:stars"} | form:stars:5 |
{"type": "date", "id": "when", "value": "", "tap": "form:when"} | form:when:2026-09-14 |
{"type": "field", "id": "email", "label": "E‑mail", "placeholder": "you@example.com", "tap": "signup:email"} | signup:email:ada@example.comteks bebas adalah segmen terakhir |
{"type": "text", "text": "Order #4821"} — tanpa tap | tidak ada. Digambar diam: ini sedang menunjukkan, bukan bertanya |
Jadi sebuah penangan memecah dua kali dan tidak lebih: sekali untuk kata kerjanya, sekali untuk bidangnya, dan sisanya adalah nilainya.
# data = "form:seats:4" let (verb, arg) = data.split_once(':').unwrap_or((data, "")); # form | seats:4 let (field, value) = arg.split_once(':').unwrap_or((arg, "")); # seats | 4 # Free text can hold colons, so it is the remainder — never a third split. # "todo:rename:2:Buy oat milk, 2 tins" is rename, item 2, and the rest.
Putarannya, dari ujung ke ujung
Tiga panggilan, dan yang di tengah milik aplikasi. Ini seluruhnya — sebuah kartu yang menanyakan sesuatu, sebuah ketukan, dan kartu itu tergambar ulang dengan jawabannya di dalam.
Tiga panggilan yang sama dengan blok di bawah, dimainkan. Tekan tanda tambah dan lihat hitungannya tetap di tempatnya: aplikasi sudah mengirim seats:3 dan tidak punya apa-apa lagi untuk dijadikan pegangan. Ia berubah ketika spesifikasi berikutnya tiba — dan itulah seluruh model keadaan dalam satu kendali, sekaligus alasan mengapa tidak ada delta di mana pun dalam protokol ini.
# 1. the bot asks. `text` is the fallback for a client that cannot draw widgets. POST /bot$KEY/sendMessage { "chat_id": -1001, "text": "Table for two — 2 seats, no day yet", "widget": { "type": "card", "gap": "xs", "children": [ { "type": "row", "gap": "sm", "children": [ { "type": "icon", "icon": "cart", "tone": "accent" }, { "type": "text", "text": "Table for two", "weight": "bold" } ] }, { "type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats" }, { "type": "date", "id": "when", "label": "Day", "value": "", "tap": "form:when" }, { "type": "note", "text": "2 seats, no day yet" }, { "type": "button", "label": "Book it", "primary": true, "tap": "form:place" } ] } } # 2. somebody moves the stepper to four — this arrives from getUpdates { "update_id": 43, "callback_query": { "id": "cb-9", "data": "form:seats:4", "from": { "id": 5501, "first_name": "Ada" }, "message": { "message_id": 19, "chat": { "id": -1001 } } } } # 3. answer first — the control is waiting on it — then redraw with the new state POST /bot$KEY/answerCallbackQuery { "callback_query_id": "cb-9", "text": "4 at the table." } POST /bot$KEY/editMessageWidget { "chat_id": -1001, "message_id": 19, "widget": { …the same card, "value": 4… } }
Menolak adalah tiga panggilan yang sama dengan yang ketiga dihilangkan: jawab "One to eight." dan jangan kirim spesifikasi baru, maka kartunya tetap seperti semula. Tidak ada cara gagal diam-diam, karena kendalinya digambar dalam keadaan menunggu sampai jawabannya mendarat.
Meminta sesuatu yang diketik
field adalah satu-satunya kendali yang membawa kata. Yang kembali adalah apa yang diketik, sebagai segmen terakhir — dan yang menjadikannya sebuah formulir alih-alih empat pesan adalah bahwa bentuknya juga milikmu: sebuah spesifikasi bisa menumbuhkan baris yang sesaat lalu belum ada.
# the bot asks
{ "type": "field", "id": "email-1", "label": "E-mail",
"placeholder": "you@example.com", "value": "", "tap": "signup:email" }
# what arrives
"data": "signup:email:ada@exmaple"
# the bot judges — nothing on the device knows what an address looks like —
# and sends the same field back wearing the verdict
{ "type": "field", "id": "email-2", "label": "E-mail",
"value": "ada@exmaple", "error": "That address has no dot after the @.",
"tap": "signup:email" }
# …with a banner above it, so the card says how many things need fixing
{ "type": "banner", "tone": "danger", "text": "One thing needs fixing." }Ada dua hal tentang bidang yang layak diketahui sebelum menulis satu. Ia melapor ketika ditinggalkan, bukan pada tiap ketukan tombol — jadi ketukannya tiba sekali saja, membawa apa yang diketik. Dan value-nya hanyalah apa yang menjadi awal isi kotaknya: untuk mengosongkan sebuah bidang, atau membawanya kembali kosong untuk percobaan kedua, berikan id baru. Karena itulah id di atas membawa nomor putaran (email‑1, email‑2) — identitas baru adalah cara sebuah kotak kembali kosong.
Tiga kartu utuh, seharga satu mesin keadaan
todo_list, swap_panel dan ticket_offer adalah gabungan: aplikasi menggambar kartunya, bot memasok keadaannya. Kosakata ketukannya milik kartu itu.
"widget": {
"type": "todo_list", "title": "Shopping",
// the two the bot decides. A full list shows no Add at all,
// rather than an Add that answers no.
"can_add": true, "can_edit": true,
"items": [
{ "id": 0, "text": "Oat milk", "done": true },
{ "id": 1, "text": "Coffee", "done": false }
]
}
# There is no "tap" on a composite: the card's vocabulary is the card's,
# and the app sends these of its own accord.
# ticking the second line → "todo:toggle:1"
# renaming it → "todo:rename:1:Coffee, the good one"
# the Add button → "todo:add" (bare — you name the new item)
# retitling the card → "todo:title:This week"Validasi itu urusanmu, dan bukan aturan soal kerapian
Setiap simpul masukan menerima hint dan error, dan keduanya adalah kata-kata bot. Tidak ada apa pun di penggambarnya yang tahu seperti apa rupa alamat surel atau bahwa sebuah daftar centang punya batas atas: nilainya naik, botnya menimbang, dan spesifikasi berikutnya membawa putusannya. Klien yang menimbang sendiri akan menjadi pendapat kedua tentang aturan yang tinggal di tempat lain, dan keduanya akan berselisih pada hari kamu mengubah aturanmu — diam-diam, ke arah meloloskan sesuatu. Periksa ulang juga di jalan masuk: menyembunyikan tombol tidak menghentikan peristiwa yang diputar ulang.
MELIHATNYA TANPA MENULIS BOT
Pengaturan → Widget dinamis di dalam aplikasi menjalankan seluruh putaran dengan tiruan di ujung sana: spesifikasi yang sama, string peristiwa yang sama, dan sebuah panel di bawah tiap kartu yang menunjukkan kedua paruh kabelnya apa adanya. Setelah itu cargo run -p botapi --example widgetbot adalah enam kartu yang sama digerakkan oleh bot sungguhan — versi yang juga bisa memperlihatkan sebuah ketukan ditolak, datang dua kali, atau kedaluwarsa.