COMPAX

BOTI

WIDGETY
V KONVERZACI

Klávesnice jsou řady tlačítek, takže každá otázka, která není které z těchto tlačítek, se změní v rozhovor. Widget je odpověď na to: karta, kterou aplikace nakreslí z JSONu, jejž pošle bot, ve stejné zprávě, po stejném callbackovém kanálu. Níže je každý ovládací prvek, který existuje, stavebnice, která pro každý napíše JSON, a karta, která z toho vyjde.

K ČEMU JSOU

Příklady aplikací

Objednávková obrazovka, vyhledávání, rezervace, jízdné. Každá karta níže je postavená z uzlů, které tato stránka rozdává, a všechny jsou živé — zmáčkněte je. Kde obrazovka potřebovala něco, co slovník neměl, chyběl slovníku uzel: place, time, item a total jsou ty čtyři, které vzešly ze stavby těchto karet.

Dva řádky jídelního lístku, velikost, co na to přijde, a kolik to dělá. Celá objednávka je jedna zpráva, která se pořád upravuje.

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

TA MYŠLENKA

Co je widget

Klávesnice jsou řady tlačítek, takže každá otázka, která není které z těchto tlačítek, se změní v rozhovor: jedna zpráva na hodnotu a vůbec žádný způsob, jak na místě sebrat množinu, počet nebo den. Widget je odpověď na to: karta, kterou aplikace nakreslí z JSONu, jejž pošle bot, ve stejné zprávě a se stejným callbackovým kanálem pod tím.

Model stavu je ten, který klávesnice už má, jen zaostřený: stav patří botovi a klient nedrží nic, co mu nebylo dáno. Každé zmáčknutí je obyčejný řetězec události; bot aktualizuje vlastní kopii a pošle další specifikaci přes editMessageWidget, a karta se překreslí na místě. sendMessage přijímá parametr widget, takže specifikace jede spolu s odesláním a text zprávy je záložní varianta pro klienta, který ji neumí nakreslit.

KARTA POSTAVENÁ Z OBECNÝCH UZLŮ
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… } }

STAVEBNICE

Postavte si jeden

Vyberte ovládací prvek, otočte jeho knoflíky a zmáčkněte kartu. JSON je to, co váš bot posílá na sendMessage; řetězec pod ním je všechno, co se vrátí, když někdo zmáčkne to, co jste nakreslili.

stepper se ptá na kolik, v mezích.

Odpověď Telegramu jsou dvě tlačítka a počítadlo napsané do textu.

CO BOT POSÍLÁ
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 se vrací

Zmáčkněte kartu v telefonu. Ať prvek dělá cokoli, k botovi dorazí jeden řetězec na cbq — víc se na ten kanál nevejde.

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

VÍC NAJEDNOU

Slož kartu

Jeden ovládací prvek odpovídá na jednu otázku a skoro nic, na co se bot ptá, není jedna otázka. Rezervace je počet, den, nastavení a tlačítko. card je uzel, který je drží, a jeho děti jsou seznam — takže ten seznam je rozvržení. Začněte od některé z těchto, nebo k ní přidejte řádky.

Upravujete stepper — ptá se na kolik, v mezích.

CO BOT POSÍLÁ
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 se vrací

Zmáčkněte libovolný řádek karty v telefonu. Ať karta drží kolik chce uzlů, k botovi dorazí jeden řetězec — a proto každý adresovatelný uzel nese vlastní tap.

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

SLOVNÍK

Slovník je bílá listina

  • rozvržení — column, row, wrap, box, card, space, divider
  • zobrazení — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • vstup — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • složené — checkbox_group, payment_request, swap_panel, ticket_offer: celé karty, které aplikace kreslí ze stavu dodaného botem
  • okno — mini_app: jediný uzel, který kreslí něco, co tato aplikace nenapsala, a záměrně poslední v tabulce. Otvírá se nad konverzací, ne místo ní.

Jméno, které v seznamu není, nakreslí rámeček, který ho pojmenuje, místo aby nenakreslilo nic — formulář s tiše chybějícím řádkem je ten, který někdo odešle v domnění, že je úplný. Vzhled je token ("tone": "accent", "size": "lg"), rozhodnutý proti paletě aplikace: není tu vlastnost barvy, není tu vlastnost v pixelech a není tu ikona mimo bílou listinu — a právě to brání botovi nakreslit něco ve tvaru vlastního potvrzovacího listu této aplikace uvnitř konverzace, které už někdo věří. Syrový "color" se ignoruje a je na to test.

Jak se hodnota vrací kanálem, který nese jediný řetězec

  • Ovládací prvek, jehož hodnotou je uzel sám — button, check, switch — posílá svůj řetězec tap beze změny. Bot drží starý stav a ví, jaký musí být nový.
  • Ovládací prvek, který hodnotu nese — select, choice, checklist, stepper, slider, rating, date, field — posílá tap s hodnotou připojenou na konec: u "tap": "form:seats" dorazí volba čtyřky jako form:seats:4. Volný text je poslední segment (todo:rename:2:Buy oat milk), takže ho odřízněte, místo abyste dělili na každé dvojtečce.
  • Uzel bez tap se kreslí nehybný. Tak specifikace říká „tohle ukazuje, neptá se“ — a hotový formulář posílá další specifikaci úplně bez řetězců tap, a právě tak přestane jít zmáčknout.
  • callback_data má v Bot API 64 bajtů, takže vybraná hodnota musí být krátká: seznam kódů aktiv projde, seznam vět ne.

Každý ovládací prvek a co se z něj vrací

Jedna tabulka, protože se do ní vejde celý protokol. Pošlete uzel vlevo; zmáčknutí vpravo je to, co dorazí jako callback_query.data. Každý z nich je převzatý z examples/widgetbot.rs, což je bot, kterého si můžete spustit.

Uzel, který posíláteCo se vrátí při zmáčknutí
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
uzel je ta hodnota
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
holé — starý bit držíte vy a víte, jaký je nový
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
hodnota je připojená na konec
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
value té volby, nikdy její popisek
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
dotčená položka, ne nová množina — překlápíte ji vy
{"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
volný text je poslední segment
{"type": "text", "text": "Order #4821"} — bez tapnic. Nakreslené nehybně: tohle ukazuje, neptá se

Takže obsluha dělí dvakrát a víckrát ne: jednou na sloveso, jednou na pole, a co zbude, je hodnota.

ČTENÍ ZMÁČKNUTÍ
# 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.

Smyčka, od začátku do konce

Tři volání, a to prostřední patří aplikaci. To je celé — karta, která se na něco ptá, zmáčknutí, a karta překreslená s odpovědí uvnitř.

Číslo se nepohne, dokud jím nepohne bot
9:41
‹tablebotbot
tablebot
Osoby−23+
DenPá, 20:00
Rezervovat
upraveno
seats:3 → tablebot
+Napište zprávu…☺

Táž tři volání jako v bloku níže, přehraná. Zmáčkněte plus a dívejte se, jak počítadlo zůstává, kde je: aplikace odeslala seats:3 a nemá se čeho jiného chytit. Změní se, až dorazí další specifikace — a to je celý model stavu v jednom ovládacím prvku a důvod, proč v tomto protokolu nikde nejsou delty.

OTÁZKA → ZMÁČKNUTÍ → PŘEKRESLENÍ
# 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… } }

Odmítnutí jsou táž tři volání bez toho třetího: odpovězte "One to eight." a nové specifikace neposílejte, a karta zůstane, jak byla. Selhat potichu nejde, protože ovládací prvek se kreslí čekající, dokud odpověď nedorazí.

Ptát se na něco napsaného

field je jediný ovládací prvek, který nese slova. Vrací se to, co bylo napsáno, jako poslední segment — a co z toho dělá formulář místo čtyř zpráv, je to, že i tvar patří vám: specifikace může vypěstovat řádek, který tam před chvílí nebyl.

POLE A ROZSUDEK NAD NÍ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." }

Dvě věci o poli stojí za to vědět, než nějaké napíšete. Hlásí se, když se z něj odejde, ne při každé klávese — zmáčknutí tedy dorazí jednou, s tím, co bylo napsáno. A jeho value je jen to, s čím políčko začíná: abyste pole vyprázdnili nebo ho vrátili prázdné pro druhý pokus, dejte mu nové id. Proto identifikátory výše nesou číslo kola (email‑1, email‑2) — nová identita je způsob, jak se políčko vrátí prázdné.

Tři celé karty, za cenu stavového automatu

todo_list, swap_panel a ticket_offer jsou složené: kartu kreslí aplikace, stav dodává bot. Slovník zmáčknutí patří kartě.

SEZNAM, KTERÝ KRESLÍ APLIKACE
"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"

Kontrola je na vás a není to pravidlo o pořádku

Každý vstupní uzel přijímá hint a error, a obojí jsou slova bota. Nic v kreslítku neví, jak vypadá e-mailová adresa ani že zaškrtávací seznam má maximum: hodnota jde nahoru, bot soudí a další specifikace nese rozsudek. Klient, který by soudil sám, by byl druhým názorem na pravidla bydlící jinde, a ty dva by se rozešly ten den, kdy ta vaše změníte — potichu a směrem k tomu něco propustit. Kontrolujte znovu i na vstupu: schovat tlačítko nezastaví přehranou událost.

VIDĚT TO, ANIŽ BYSTE PSALI BOTA

Nastavení → Dynamické widgety v aplikaci projde celou smyčku s atrapou na druhém konci: tytéž specifikace, tytéž řetězce událostí a pod každou kartou panel, který doslova ukazuje obě poloviny drátu. Potom cargo run -p botapi --example widgetbot je těch samých šest karet poháněných opravdovým botem — a to je verze, která umí ukázat i zmáčknutí odmítnuté, přišlé dvakrát nebo propadlé.