COMPAX

BOT

WIDGET
DI DALAM OBROLAN

Papan tombol adalah barisan tombol, jadi setiap pertanyaan yang bukan tombol yang mana berubah menjadi percakapan. Widget adalah jawabannya: sebuah kartu yang digambar aplikasi dari JSON yang dikirim bot, di dalam pesan yang sama, lewat kanal callback yang sama. Di bawah ini ada setiap kendali yang tersedia, sebuah perakit yang menuliskan JSON masing-masing, dan kartu yang dihasilkannya.

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

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
MargheritaSan Marzano, fior di latte, basil£11.50−1+
Diavola'Nduja, chilli, honey£13.00−1+
Size
Regular+£3.00
Large
On top
Extra mozzarella+£1.50
Olives+£1.00
Anchovy+£1.50
Totaldelivery included£24.50
Send to the kitchen
+Write a message…☺🎤

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.

SEBUAH KARTU DARI SIMPUL-SIMPUL UMUM
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.

YANG DIKIRIM BOT
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:"
           }
         ]
       }
     }'
Yang kembali

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.

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
People−2+
+Write a message…☺🎤

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.

YANG DIKIRIM BOT
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"
           }
         ]
       }
     }'
Yang kembali

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.

9:41
‹tablebotbot
/book10:58 ✓✓
tablebot
People−2+
DayPick a day
Window seat
Book the table
+Write a message…☺🎤

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 string tap-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 — mengirim tap dengan nilainya ditempel di belakang: dengan "tap": "form:seats", memilih empat tiba sebagai form: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 tap digambar diam. Begitulah cara sebuah spesifikasi berkata “ini sedang menunjukkan, bukan bertanya” — dan formulir yang sudah selesai mengirim spesifikasi berikutnya tanpa satu pun string tap, dan begitulah ia berhenti bisa ditekan.
  • callback_data berukuran 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 kirimYang kembali saat ditekan
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
simpulnya adalah nilainya
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
telanjang — 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:4
nilainya ditempel di belakang
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
value pilihan itu, tidak pernah labelnya
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
item 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.com
teks bebas adalah segmen terakhir
{"type": "text", "text": "Order #4821"} — tanpa taptidak 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.

MEMBACA SEBUAH KETUKAN
# 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.

Angkanya tidak bergerak sampai botnya yang menggerakkan
9:41
‹tablebotbot
tablebot
Orang−23+
HariJum, 20.00
Pesan
disunting
seats:3 → tablebot
+Tulis pesan…☺

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.

TANYA → KETUK → GAMBAR ULANG
# 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.

SEBUAH BIDANG, DAN PUTUSAN ATASNYA
# 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.

SEBUAH DAFTAR YANG DIGAMBAR APLIKASI
"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.