COMPAX

BOTS

WIDGETS
IN EEN GESPREK

Een toetsenbord is rijen knoppen, dus elke vraag die niet welke van deze knoppen is, wordt een gesprek. Een widget is het antwoord daarop: een kaart die de applicatie tekent uit de JSON die de bot stuurt, in hetzelfde bericht, over hetzelfde callback-kanaal. Hieronder staat elk bedieningselement dat er is, een bouwer die voor elk de JSON schrijft, en de kaart die eruit komt.

WAAR ZE VOOR ZIJN

Voorbeeldapplicaties

Een bestelscherm, een zoekopdracht, een reservering, een tarief. Elke kaart hieronder is gebouwd uit de knopen die deze pagina uitdeelt, en ze zijn allemaal levend — druk erop. Waar een scherm iets nodig had dat het vocabulaire niet had, was het het vocabulaire dat een knoop tekortkwam: place, time, item en total zijn de vier die uit het bouwen hiervan zijn gekomen.

Twee regels van een menukaart, de maat, wat erop gaat, en waar het op uitkomt. De hele bestelling is één bericht dat steeds opnieuw bewerkt wordt.

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

HET IDEE

Wat een widget is

Een toetsenbord is rijen knoppen, dus elke vraag die niet welke van deze knoppen is, wordt een gesprek: één bericht per waarde, en geen enkele manier om ter plekke een verzameling, een aantal of een dag te verzamelen. Een widget is het antwoord daarop: een kaart die de app tekent uit de JSON die de bot stuurt, in hetzelfde bericht, met hetzelfde callback-kanaal eronder.

Het toestandsmodel is dat wat een toetsenbord al heeft, aangescherpt: de bot bezit de toestand, de client houdt niets vast wat hem niet gegeven is. Elke druk is een gewone gebeurtenistekenreeks; de bot werkt zijn eigen kopie bij en duwt de volgende specificatie na met editMessageWidget, en de kaart tekent zich ter plekke opnieuw. sendMessage neemt een widget-parameter, dus de specificatie reist met het verzenden mee, en de text van het bericht is de terugval voor een client die haar niet kan tekenen.

EEN KAART UIT ALGEMENE KNOPEN
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… } }

DE BOUWER

Bouw er een

Kies een bedieningselement, draai aan de knoppen en druk op de kaart. De JSON is wat jouw bot naar sendMessage stuurt; de tekenreeks eronder is het geheel van wat terugkomt als iemand drukt op wat je getekend hebt.

stepper vraagt om hoeveel, met grenzen.

Het antwoord van Telegram zijn twee knoppen en een teller die in de tekst geschreven staat.

WAT DE BOT STUURT
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:"
           }
         ]
       }
     }'
Wat er terugkomt

Druk op de kaart in de telefoon. Wat het bedieningselement ook doet, wat de bot bereikt is één tekenreeks op cbq — meer past er niet op dat kanaal.

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

MEERDERE TEGELIJK

Stel een kaart samen

Eén bedieningselement beantwoordt één vraag, en bijna niets wat een bot vraagt is één vraag. Een reservering is een aantal, een dag, een instelling en een knop. card is de knoop die ze bij elkaar houdt, en zijn kinderen zijn een lijst — dus de lijst is de indeling. Begin met een van deze, of voeg er rijen aan toe.

Bezig met stepper — het vraagt om hoeveel, met grenzen.

WAT DE BOT STUURT
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"
           }
         ]
       }
     }'
Wat er terugkomt

Druk op een willekeurige rij van de kaart in de telefoon. Hoeveel knopen een kaart ook bevat, wat de bot bereikt is één tekenreeks — en daarom draagt elke adresseerbare knoop zijn eigen tap.

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

HET VOCABULAIRE

Het vocabulaire is een witte lijst

  • indeling — column, row, wrap, box, card, space, divider
  • weergave — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • invoer — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • samengestelde — checkbox_group, payment_request, swap_panel, ticket_offer: hele kaarten die de app tekent uit de toestand die de bot levert
  • het venster — mini_app: de ene knoop die iets tekent wat deze applicatie niet geschreven heeft, en met opzet de laatste in de tabel. Het opent boven het gesprek in plaats van in de plaats ervan.

Een naam die niet op de lijst staat tekent een vakje dat hem noemt, in plaats van niets — een formulier waar stilzwijgend een rij aan ontbreekt is er een dat iemand verstuurt in de veronderstelling dat het compleet is. Uiterlijk is een token ("tone": "accent", "size": "lg"), opgelost tegen het palet van de app: er is geen kleureigenschap, geen pixeleigenschap en geen pictogram buiten de witte lijst — en dat is wat een bot ervan weerhoudt iets in de vorm van het eigen bevestigingsblad van deze app te tekenen binnen een gesprek dat iemand al vertrouwt. Een rauwe "color" wordt genegeerd, en er is een test die dat vastlegt.

Hoe een waarde terugkomt over een kanaal dat één tekenreeks draagt

  • Een bedieningselement waarvan de waarde de knoop is — button, check, switch — stuurt zijn tap-tekenreeks onveranderd. De bot houdt de oude toestand vast en weet wat de nieuwe moet zijn.
  • Een bedieningselement dat een waarde draagt — select, choice, checklist, stepper, slider, rating, date, field — stuurt tap met de waarde eraan geplakt: bij "tap": "form:seats" komt vier kiezen binnen als form:seats:4. Vrije tekst is het laatste segment (todo:rename:2:Buy oat milk), dus haal het eraf in plaats van op elke dubbele punt te splitsen.
  • Een knoop zonder tap wordt roerloos getekend. Zo zegt een specificatie “dit toont, dit vraagt niet” — en een afgerond formulier stuurt zijn volgende specificatie zonder ook maar één tap-tekenreeks, en zo houdt het op indrukbaar te zijn.
  • callback_data is 64 bytes in de Bot API, dus een gekozen waarde moet kort zijn: een lijst met assetcodes kan, een lijst met zinnen niet.

Elk bedieningselement, en wat er terugkomt

Eén tabel, want het hele protocol staat erin. Stuur de knoop links; de druk rechts is wat binnenkomt als callback_query.data. Elk hiervan is overgenomen uit examples/widgetbot.rs, een bot die je kunt draaien.

De knoop die je stuurtWat er bij een druk terugkomt
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
de knoop is de waarde
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
kaal — jij houdt de oude bit vast en kent de nieuwe
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
de waarde wordt eraan geplakt
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
de value van de optie, nooit het label ervan
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
het aangeraakte item, niet de nieuwe verzameling — jij kantelt hem
{"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
vrije tekst is het laatste segment
{"type": "text", "text": "Order #4821"} — geen tapniets. Roerloos getekend: dit toont, dit vraagt niet

Een afhandelaar splitst dus twee keer en niet vaker: één keer voor het werkwoord, één keer voor het veld, en wat overblijft is de waarde.

EEN DRUK LEZEN
# 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.

De lus, van begin tot eind

Drie aanroepen, en die in het midden is van de app. Dit is het hele verhaal — een kaart die iets vraagt, een druk, en de kaart opnieuw getekend met het antwoord erin.

Het getal beweegt niet tot de bot het beweegt
9:41
‹tablebotbot
tablebot
Personen−23+
DagVr, 20:00
Boeken
bewerkt
seats:3 → tablebot
+Schrijf een bericht…☺

Dezelfde drie aanroepen als het blok hieronder, afgespeeld. Druk op de plus en zie de teller staan waar hij staat: de app heeft seats:3 gestuurd en heeft verder niets om op af te gaan. Hij verandert wanneer de volgende specificatie binnenkomt — en dat is het hele toestandsmodel in één bedieningselement, en de reden dat er nergens in dit protocol delta’s zijn.

VRAGEN → DRUKKEN → OPNIEUW TEKENEN
# 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… } }

Weigeren zijn dezelfde drie aanroepen zonder de derde: antwoord "One to eight." en stuur geen nieuwe specificatie, dan blijft de kaart zoals hij was. Stilletjes mislukken kan niet, want het bedieningselement wordt wachtend getekend tot het antwoord binnen is.

Vragen om iets getypts

Een field is het enige bedieningselement dat woorden draagt. Wat terugkomt is wat er getypt is, als laatste segment — en wat er een formulier van maakt in plaats van vier berichten, is dat ook de vorm van jou is: een specificatie kan een rij laten groeien die er een moment eerder nog niet was.

EEN VELD, EN EEN OORDEEL EROVER
# 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." }

Twee dingen over een veld zijn het waard om te weten voor je er een schrijft. Het meldt zich wanneer je het verlaat, niet bij elke toetsaanslag — de druk komt dus één keer binnen, met wat er getypt is. En zijn value is alleen datgene waarmee het vakje begint: om een veld leeg te maken, of het voor een tweede poging leeg terug te halen, geef het een nieuwe id. Daarom dragen de ids hierboven het rondenummer (email‑1, email‑2) — een nieuwe identiteit is hoe een vakje leeg terugkomt.

Drie hele kaarten, voor de prijs van een toestandsmachine

todo_list, swap_panel en ticket_offer zijn samengesteld: de app tekent de kaart, de bot levert de toestand. Het drukvocabulaire is dat van de kaart.

EEN LIJST DIE DE APP TEKENT
"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"

De controle is van jou, en het is geen regel over netheid

Elke invoerknoop neemt hint en error, en beide zijn de woorden van de bot. Niets in de tekenaar weet hoe een e-mailadres eruitziet of dat een aankruislijst een maximum heeft: de waarde gaat omhoog, de bot oordeelt, en de volgende specificatie draagt het oordeel. Een client die zelf oordeelde zou een tweede mening zijn over regels die ergens anders wonen, en de twee zouden het oneens zijn op de dag dat jij die van jou verandert — geruisloos, en in de richting van iets doorlaten. Controleer ook bij binnenkomst opnieuw: een knop verbergen houdt een herhaalde gebeurtenis niet tegen.

HET ZIEN ZONDER EEN BOT TE SCHRIJVEN

Instellingen → Dynamische widgets in de app doorloopt de hele lus met een nabootsing aan de andere kant: dezelfde specificaties, dezelfde gebeurtenistekenreeksen, en onder elke kaart een paneel dat beide helften van de draad letterlijk toont. En cargo run -p botapi --example widgetbot zijn diezelfde zes kaarten aangedreven door een echte bot — dat is de versie die ook een geweigerde, dubbel binnengekomen of verlopen druk kan laten zien.