COMPAX

BOTLAR

SOHBETİN İÇİNDE
WIDGET’LAR

Bir klavye, sıra sıra düğmedir; dolayısıyla bu düğmelerden hangisi olmayan her soru bir konuşmaya dönüşür. Widget bunun cevabıdır: uygulamanın, botun gönderdiği JSON’dan çizdiği bir kart — aynı mesajın içinde, aynı callback kanalında. Aşağıda var olan her denetim, her biri için JSON’u yazan bir kurucu ve çizilen kart var.

NE İŞE YARARLAR

Örnek uygulamalar

Bir sipariş ekranı, bir arama, bir rezervasyon, bir ücret. Aşağıdaki her kart bu sayfanın dağıttığı düğümlerden kurulmuştur ve hepsi canlıdır — bas onlara. Bir ekranın, sözlükte olmayan bir şeye ihtiyacı olduğunda, eksik olan sözlüğün bir düğümüydü: place, time, item ve total, bunları kurarken ortaya çıkan dört düğüm.

Bir menünün iki satırı, boyu, üstüne ne geldiği ve ne tuttuğu. Bütün sipariş, sürekli düzenlenen tek bir mesaj.

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…☺🎤

FİKİR

Widget nedir

Bir klavye, sıra sıra düğmedir; dolayısıyla bu düğmelerden hangisi olmayan her soru bir konuşmaya dönüşür: her değer için bir mesaj, ve bir kümeyi, bir adedi ya da bir günü yerinde toplamanın hiçbir yolu yok. Widget bunun cevabıdır: uygulamanın, botun gönderdiği JSON’dan çizdiği bir kart — aynı mesajın içinde, altında aynı callback kanalıyla.

Durum modeli, bir klavyenin zaten sahip olduğu modeldir, keskinleştirilmiş hâli: durum botundur, istemci kendisine verilmemiş hiçbir şeyi tutmaz. Her dokunuş sıradan bir olay dizgisidir; bot kendi kopyasını günceller ve bir sonraki tanımı editMessageWidget ile iter, kart da yerinde yeniden çizilir. sendMessage bir widget parametresi alır, yani tanım gönderimle birlikte yolculuk eder ve mesajın text’i onu çizemeyen bir istemci için geri düşüş noktasıdır.

GENEL DÜĞÜMLERDEN KURULMUŞ BİR KART
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… } }

KURUCU

Bir tane kur

Bir denetim seç, düğmelerini çevir ve karta bas. JSON, botunun sendMessage’a gönderdiği şeydir; altındaki dizgi ise birisi senin çizdiğine bastığında geri gelen şeyin tamamıdır.

stepper kaç tane, sınırlar içinde ister.

Telegram’ın cevabı iki düğme ve metnin içine yazılmış bir sayaçtır.

BOTUN GÖNDERDİĞİ
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:"
           }
         ]
       }
     }'
Geri gelen

Telefondaki karta bas. Denetim ne yaparsa yapsın, bota ulaşan şey cbq üzerinde tek bir dizgidir — o kanala başka bir şey sığmaz.

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

AYNI ANDA BİRKAÇI

Bir kart oluştur

Bir denetim bir soruya cevap verir, ve bir botun sorduğu şeylerin neredeyse hiçbiri tek bir soru değildir. Bir rezervasyon bir sayı, bir gün, bir ayar ve bir düğmedir. card onları tutan düğümdür, ve çocukları bir listedir — yani liste, yerleşimin ta kendisidir. Bunlardan biriyle başla, ya da ona satır ekle.

stepper düzenleniyor — kaç tane, sınırlar içinde ister.

BOTUN GÖNDERDİĞİ
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"
           }
         ]
       }
     }'
Geri gelen

Telefondaki kartın herhangi bir satırına bas. Bir kart kaç düğüm tutarsa tutsun, bota ulaşan şey tek bir dizgidir — adreslenebilir her düğümün kendi tap’ini taşımasının sebebi de bu.

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

SÖZLÜK

Sözlük bir beyaz listedir

  • yerleşim — column, row, wrap, box, card, space, divider
  • gösterim — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • giriş — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • bileşikler — checkbox_group, payment_request, swap_panel, ticket_offer: uygulamanın, botun verdiği durumdan çizdiği bütün kartlar
  • pencere — mini_app: bu uygulamanın yazmadığı bir şeyi çizen tek düğüm, ve bilerek tablonun sonuncusu. Konuşmanın yerine değil, üstüne açılır.

Listede olmayan bir ad, hiçbir şey değil, onu adlandıran bir kutu çizer — sessizce bir satırı eksik olan bir form, birinin tamam sanarak gönderdiği formdur. Görünüm bir token’dır ("tone": "accent", "size": "lg") ve uygulamanın paletine karşı çözülür: renk özelliği yok, piksel özelliği yok, beyaz listenin dışında simge yok — ve bir botun, birinin zaten güvendiği bir konuşmanın içine bu uygulamanın kendi onay sayfasının biçiminde bir şey çizmesini engelleyen tam da budur. Ham bir "color" yok sayılır, ve bunu söyleyen bir test var.

Tek bir dizgi taşıyan bir kanaldan bir değer nasıl geri döner

  • Değeri düğümün kendisi olan bir denetim — button, check, switch — tap dizgisini olduğu gibi gönderir. Eski durumu bot tutar ve yenisinin ne olması gerektiğini bilir.
  • Bir değer taşıyan bir denetim — select, choice, checklist, stepper, slider, rating, date, field — tap’i değer sonuna eklenmiş hâlde gönderir: "tap": "form:seats" ile dördü seçmek form:seats:4 olarak gelir. Serbest metin son parçadır (todo:rename:2:Buy oat milk), yani her iki noktadan bölmek yerine onu sondan ayır.
  • tap’i olmayan bir düğüm hareketsiz çizilir. Bir tanım “bu gösteriyor, sormuyor” demeyi böyle yapar — ve bitmiş bir form bir sonraki tanımını hiç tap dizgisi olmadan gönderir, basılabilir olmaktan da böyle çıkar.
  • callback_data Bot API’de 64 bayttır, yani seçilen bir değer kısa olmak zorundadır: varlık kodlarından bir liste olur, cümlelerden bir liste olmaz.

Her denetim ve her birinden ne döner

Tek bir tablo, çünkü protokolün tamamı onun içinde. Soldaki düğümü gönder; sağdaki dokunuş callback_query.data olarak gelen şeydir. Bunların her biri, çalıştırabileceğin bir bot olan examples/widgetbot.rs dosyasından alınmıştır.

Gönderdiğin düğümDokunuşta geri gelen
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
değer, düğümün kendisi
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
çıplak — eski biti sen tutuyorsun ve yenisini biliyorsun
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
değer sona eklenir
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
seçeneğin value’su, asla etiketi değil
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
dokunulan öğe, yeni küme değil — onu sen çevirirsin
{"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
serbest metin son parçadır
{"type": "text", "text": "Order #4821"} — tap yokhiçbir şey. Hareketsiz çizilmiş: bu gösteriyor, sormuyor

Yani bir işleyici iki kez böler, daha fazla değil: bir kez fiil için, bir kez alan için, ve geriye kalan da değerdir.

BİR DOKUNUŞU OKUMAK
# 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.

Döngü, baştan sona

Üç çağrı, ve ortadaki uygulamanın. Hepsi bu — bir şey soran bir kart, bir dokunuş, ve cevabı içinde olacak şekilde yeniden çizilmiş kart.

Bot oynatmadıkça sayı yerinden kıpırdamaz
9:41
‹tablebotbot
tablebot
Kişi−23+
GünCum, 20.00
Ayırt
düzenlendi
seats:3 → tablebot
+Bir mesaj yaz…☺

Aşağıdaki bloktaki aynı üç çağrı, oynatılmış hâli. Artıya bas ve sayacın yerinde kaldığını izle: uygulama seats:3’ü göndermiş ve dayanacak başka bir şeyi yok. Bir sonraki tanım geldiğinde değişir — ki bu, tek bir denetimde bütün durum modelidir, ve bu protokolde hiçbir yerde delta olmamasının sebebidir.

SOR → DOKUN → YENİDEN ÇİZ
# 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… } }

Reddetmek, üçüncüsü olmadan aynı üç çağrıdır: "One to eight." diye cevap ver ve yeni tanım gönderme, kart olduğu gibi kalır. Sessizce başarısız olmanın yolu yok, çünkü denetim cevap gelene kadar bekliyor olarak çizilir.

Yazılmış bir şey istemek

field, kelime taşıyan tek denetimdir. Geri gelen, yazılan şeydir, son parça olarak — ve bunu dört mesaj yerine bir form yapan şey, biçimin de sana ait olmasıdır: bir tanım, az önce orada olmayan bir satırı büyütebilir.

BİR ALAN VE ONA DAİR BİR HÜKÜM
# 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." }

Bir alan hakkında, birini yazmadan önce bilmeye değer iki şey var. Her tuşta değil, alandan çıkıldığında haber verir — yani dokunuş bir kez gelir, yazılanla birlikte. Ve value’su yalnızca kutunun başladığı şeydir: bir alanı temizlemek ya da ikinci bir deneme için boş geri getirmek için ona yeni bir id ver. Yukarıdaki id’lerin tur numarasını taşımasının sebebi bu (email‑1, email‑2) — bir kutunun boş dönmesinin yolu yeni bir kimliktir.

Üç bütün kart, bir durum makinesi fiyatına

todo_list, swap_panel ve ticket_offer bileşiktir: kartı uygulama çizer, durumu bot verir. Dokunuş sözlüğü kartındır.

UYGULAMANIN ÇİZDİĞİ BİR LİSTE
"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"

Doğrulama senin işin, ve düzenlilikle ilgili bir kural değil

Her giriş düğümü hint ve error alır, ve ikisi de botun sözleridir. Çizicideki hiçbir şey bir e-posta adresinin neye benzediğini ya da bir işaret listesinin bir üst sınırı olduğunu bilmez: değer yukarı gider, bot hükmeder, ve bir sonraki tanım hükmü taşır. Kendi başına hükmeden bir istemci, başka bir yerde yaşayan kurallar hakkında ikinci bir görüş olurdu, ve seninkileri değiştirdiğin gün ikisi ayrışırdı — sessizce, bir şeyi geçirme yönünde. Girişte de yeniden denetle: bir düğmeyi saklamak, yeniden oynatılan bir olayı durdurmaz.

BOT YAZMADAN GÖRMEK

Uygulamadaki Ayarlar → Dinamik widget’lar, öteki uçta bir taklitle bütün döngüyü çalıştırır: aynı tanımlar, aynı olay dizgileri, ve her kartın altında telin iki yarısını olduğu gibi gösteren bir panel. Sonra cargo run -p botapi --example widgetbot, aynı altı kartın gerçek bir botla sürülmüş hâlidir — reddedilmiş, iki kez gelmiş ya da zaman aşımına uğramış bir dokunuşu da gösterebilen sürüm budur.