COMPAX

BOTY

WIDGETY
W ROZMOWIE

Klawiatura to rzędy przycisków, więc każde pytanie inne niż który z tych przycisków zamienia się w rozmowę. Widget jest odpowiedzią na to: karta, którą aplikacja rysuje z JSON-a przysłanego przez bota, w tej samej wiadomości, tym samym kanałem callbacku. Niżej jest każda kontrolka, jaka istnieje, konstruktor, który pisze JSON dla każdej z nich, i karta, jaka z tego wychodzi.

PO CO ONE SĄ

Przykłady aplikacji

Ekran zamówienia, wyszukiwarka, rezerwacja, taryfa. Każda karta niżej jest zbudowana z węzłów, które rozdaje ta strona, i każda z nich żyje — naciśnij ją. Tam, gdzie ekran potrzebował czegoś, czego słownik nie miał, to słownikowi brakowało węzła: place, time, item i total to te cztery, które wyszły z budowania tych kart.

Dwie pozycje z karty, rozmiar, co na wierzchu i ile to razem wychodzi. Całe zamówienie to jedna wiadomość, którą wciąż się edytuje.

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

POMYSŁ

Czym jest widget

Klawiatura to rzędy przycisków, więc każde pytanie inne niż który z tych przycisków zamienia się w rozmowę: jedna wiadomość na wartość i żadnego sposobu, by zebrać na miejscu zbiór, liczbę albo dzień. Widget jest odpowiedzią na to: karta, którą aplikacja rysuje z JSON-a przysłanego przez bota, w tej samej wiadomości, z tym samym kanałem callbacku pod spodem.

Model stanu jest ten, który klawiatura już ma, tylko wyostrzony: stan należy do bota, a klient nie trzyma niczego, czego mu nie dano. Każde naciśnięcie to zwykły ciąg zdarzenia; bot aktualizuje własną kopię i dosyła następną specyfikację przez editMessageWidget, a karta przerysowuje się w miejscu. sendMessage przyjmuje parametr widget, więc specyfikacja jedzie razem z wysyłką, a text wiadomości jest zapasem dla klienta, który nie umie jej narysować.

KARTA ZBUDOWANA Z OGÓLNYCH WĘZŁÓW
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… } }

KONSTRUKTOR

Zbuduj jeden

Wybierz kontrolkę, pokręć jej gałkami i naciśnij kartę. JSON to jest to, co twój bot wysyła do sendMessage; ciąg pod spodem to całość tego, co wraca, gdy ktoś naciśnie to, co narysowałeś.

stepper pyta o ile, w granicach.

Odpowiedź Telegrama to dwa przyciski i licznik wpisany w tekst.

CO WYSYŁA 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:"
           }
         ]
       }
     }'
Co wraca

Naciśnij kartę w telefonie. Cokolwiek kontrolka robi, do bota dociera jeden ciąg na cbq — nic więcej się na tym kanale nie mieści.

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

KILKA NARAZ

Skomponuj kartę

Jedna kontrolka odpowiada na jedno pytanie, a prawie nic, o co bot pyta, nie jest jednym pytaniem. Rezerwacja to liczba, dzień, ustawienie i przycisk. card to węzeł, który je trzyma, a jego dzieci są listą — więc lista jest układem. Zacznij od jednej z tych albo dołóż do niej wiersze.

Edytujesz stepper — pyta o ile, w granicach.

CO WYSYŁA 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"
           }
         ]
       }
     }'
Co wraca

Naciśnij dowolny wiersz karty w telefonie. Ile by węzłów karta nie trzymała, do bota dociera jeden ciąg — i dlatego każdy adresowalny węzeł niesie własny tap.

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

SŁOWNIK

Słownik jest białą listą

  • układ — column, row, wrap, box, card, space, divider
  • wyświetlanie — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • wejście — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • złożone — checkbox_group, payment_request, swap_panel, ticket_offer: całe karty, które aplikacja rysuje ze stanu dostarczonego przez bota
  • okno — mini_app: jedyny węzeł, który rysuje coś, czego ta aplikacja nie napisała, i celowo ostatni w tabeli. Otwiera się nad rozmową, a nie zamiast niej.

Nazwa nieobecna na liście rysuje pudełko, które ją nazywa, a nie nic — formularz z po cichu brakującym wierszem to formularz, który ktoś wysyła w przekonaniu, że jest kompletny. Wygląd to token ("tone": "accent", "size": "lg"), rozstrzygany względem palety aplikacji: nie ma właściwości koloru, nie ma właściwości w pikselach i nie ma ikony spoza białej listy — i właśnie to powstrzymuje bota przed narysowaniem czegoś o kształcie własnej karty potwierdzenia tej aplikacji wewnątrz rozmowy, której ktoś już ufa. Surowy "color" jest ignorowany i jest test, który to mówi.

Jak wraca wartość kanałem, który niesie jeden ciąg

  • Kontrolka, której wartością jest sam węzeł — button, check, switch — wysyła swój ciąg tap bez zmian. Bot trzyma stary stan i wie, jaki musi być nowy.
  • Kontrolka, która niesie wartość — select, choice, checklist, stepper, slider, rating, date, field — wysyła tap z wartością doklejoną na końcu: przy "tap": "form:seats" wybór czwórki przychodzi jako form:seats:4. Wolny tekst jest ostatnim segmentem (todo:rename:2:Buy oat milk), więc odetnij go, zamiast dzielić na każdym dwukropku.
  • Węzeł bez tap jest rysowany bezwładnie. Tak specyfikacja mówi „to pokazuje, a nie pyta” — a skończony formularz wysyła następną specyfikację zupełnie bez ciągów tap, i tak przestaje dać się naciskać.
  • callback_data to 64 bajty w Bot API, więc wybrana wartość musi być krótka: lista kodów aktywów przejdzie, lista zdań nie.

Każda kontrolka i to, co z niej wraca

Jedna tabela, bo mieści się w niej cały protokół. Wyślij węzeł z lewej; naciśnięcie z prawej to to, co przychodzi jako callback_query.data. Każdy z nich jest wzięty z examples/widgetbot.rs, czyli z bota, którego możesz uruchomić.

Węzeł, który wysyłaszCo wraca po naciśnięciu
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
węzeł jest wartością
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
goły — stary bit masz ty i wiesz, jaki jest nowy
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
wartość jest doklejona na końcu
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
value opcji, nigdy jej etykieta
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
dotknięty element, a nie nowy zbiór — przestawiasz go ty
{"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
wolny tekst jest ostatnim segmentem
{"type": "text", "text": "Order #4821"} — bez tapnic. Narysowany bezwładnie: to pokazuje, a nie pyta

Więc obsługa dzieli dwa razy i nie więcej: raz na czasownik, raz na pole, a co zostanie, jest wartością.

CZYTANIE NACIŚNIĘCIA
# 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.

Pętla, od początku do końca

Trzy wywołania, a środkowe należy do aplikacji. To wszystko — karta, która o coś pyta, naciśnięcie i karta przerysowana z odpowiedzią w środku.

Liczba nie ruszy się, dopóki nie ruszy jej bot
9:41
‹tablebotbot
tablebot
Osoby−23+
DzieńPt, 20:00
Rezerwuj
edytowano
seats:3 → tablebot
+Napisz wiadomość…☺

Te same trzy wywołania co w bloku niżej, odtworzone. Naciśnij plus i patrz, jak licznik stoi w miejscu: aplikacja wysłała seats:3 i nie ma się na czym oprzeć. Zmieni się, gdy przyjdzie następna specyfikacja — a to jest cały model stanu w jednej kontrolce i powód, dla którego w tym protokole nigdzie nie ma delt.

PYTANIE → NACIŚNIĘCIE → PRZERYSOWANIE
# 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… } }

Odmowa to te same trzy wywołania bez trzeciego: odpowiedz "One to eight." i nie wysyłaj nowej specyfikacji, a karta zostanie taka, jaka była. Nie da się zawieść po cichu, bo kontrolka jest rysowana jako czekająca, dopóki odpowiedź nie dojdzie.

Prosić o coś wpisanego

field to jedyna kontrolka, która niesie słowa. Wraca to, co wpisano, jako ostatni segment — a tym, co czyni z tego formularz zamiast czterech wiadomości, jest to, że kształt też należy do ciebie: specyfikacja może wyhodować wiersz, którego chwilę wcześniej nie było.

POLE I WYROK NA NIE
# 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." }

Dwie rzeczy o polu warto wiedzieć, zanim się je napisze. Zgłasza się, gdy się z niego wychodzi, a nie przy każdym klawiszu — więc naciśnięcie przychodzi raz, z tym, co wpisano. A jego value to tylko to, od czego pole zaczyna: żeby je wyczyścić albo przywrócić puste do drugiej próby, daj mu nowe id. Dlatego identyfikatory wyżej niosą numer rundy (email‑1, email‑2) — nowa tożsamość to sposób, w jaki pole wraca puste.

Trzy całe karty, w cenie automatu stanów

todo_list, swap_panel i ticket_offer są złożone: kartę rysuje aplikacja, stan dostarcza bot. Słownik naciśnięć należy do karty.

LISTA, KTÓRĄ RYSUJE APLIKACJA
"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"

Walidacja należy do ciebie i nie jest zasadą o schludności

Każdy węzeł wejściowy przyjmuje hint i error, i oba to słowa bota. Nic w rysowniku nie wie, jak wygląda adres e-mail ani że lista zaznaczeń ma maksimum: wartość idzie w górę, bot ocenia, a następna specyfikacja niesie wyrok. Klient, który oceniałby sam, byłby drugą opinią o regułach mieszkających gdzie indziej, a obie rozeszłyby się tego dnia, w którym zmienisz swoje — po cichu i w stronę przepuszczenia czegoś. Sprawdzaj ponownie także na wejściu: schowanie przycisku nie zatrzymuje powtórzonego zdarzenia.

ZOBACZYĆ TO BEZ PISANIA BOTA

Ustawienia → Dynamiczne widgety w aplikacji przechodzi całą pętlę z atrapą po drugiej stronie: te same specyfikacje, te same ciągi zdarzeń i panel pod każdą kartą pokazujący obie połówki drutu dosłownie. Potem cargo run -p botapi --example widgetbot to te same sześć kart napędzanych przez prawdziwego bota — czyli wersja, która potrafi też pokazać naciśnięcie odrzucone, przychodzące dwa razy albo przeterminowane.