COMPAX

BOT

WIDGET
IN UNA CHAT

Una tastiera è file di pulsanti, quindi ogni domanda che non sia quale di questi pulsanti diventa una conversazione. Un widget è la risposta a questo: una scheda che l’applicazione disegna a partire dal JSON che manda il bot, nello stesso messaggio, sullo stesso canale di callback. Qui sotto c’è ogni controllo che esiste, un costruttore che scrive il JSON di ciascuno, e la scheda che ne esce.

A COSA SERVONO

Esempi di applicazioni

Una schermata d’ordine, una ricerca, una prenotazione, una tariffa. Ogni scheda qui sotto è costruita con i nodi che questa pagina distribuisce, e sono tutte vive — premile. Dove a una schermata serviva qualcosa che il vocabolario non aveva, era il vocabolario a cui mancava un nodo: place, time, item e total sono i quattro usciti dal costruire queste.

Due righe di un menu, la misura, cosa ci va sopra, e quanto viene. L’ordine intero è un messaggio che continua a essere modificato.

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

L’IDEA

Che cos’è un widget

Una tastiera è file di pulsanti, quindi ogni domanda che non sia quale di questi pulsanti diventa una conversazione: un messaggio per valore, e nessun modo di raccogliere sul posto un insieme, una quantità o un giorno. Un widget è la risposta a questo: una scheda che l’app disegna a partire dal JSON che manda il bot, nello stesso messaggio, con lo stesso canale di callback sotto.

Il modello di stato è quello che una tastiera ha già, affilato: lo stato è del bot, e il client non tiene niente che non gli sia stato dato. Ogni pressione è una comune stringa di evento; il bot aggiorna la propria copia e spinge la specifica successiva con editMessageWidget, e la scheda si ridisegna sul posto. sendMessage accetta un parametro widget, così la specifica viaggia con l’invio e il text del messaggio è il ripiego per un client che non sappia disegnarla.

UNA SCHEDA FATTA DI NODI GENERICI
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… } }

IL COSTRUTTORE

Costruiscine uno

Scegli un controllo, gira le sue manopole e premi la scheda. Il JSON è quello che il tuo bot manda a sendMessage; la stringa sotto è tutto quello che torna indietro quando qualcuno preme ciò che hai disegnato.

stepper chiede quanti, entro dei limiti.

La risposta di Telegram sono due pulsanti e un contatore scritto dentro il testo.

COSA MANDA IL 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:"
           }
         ]
       }
     }'
Cosa torna indietro

Premi la scheda nel telefono. Qualunque cosa faccia il controllo, quello che arriva al bot è una stringa su cbq — su quel canale non ci sta altro.

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

PIÙ D’UNO ALLA VOLTA

Componi una scheda

Un controllo risponde a una domanda, e quasi niente di ciò che un bot chiede è una domanda sola. Una prenotazione è un conteggio, un giorno, un’impostazione e un pulsante. card è il nodo che li tiene insieme, e i suoi figli sono una lista — quindi la lista è la disposizione. Parti da una di queste, o aggiungici righe.

Stai modificando stepper — chiede quanti, entro dei limiti.

COSA MANDA IL 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"
           }
         ]
       }
     }'
Cosa torna indietro

Premi una riga qualsiasi della scheda nel telefono. Per quanti nodi una scheda contenga, quello che arriva al bot è una stringa — ed è per questo che ogni nodo indirizzabile porta il proprio tap.

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

IL VOCABOLARIO

Il vocabolario è una lista bianca

  • disposizione — column, row, wrap, box, card, space, divider
  • visualizzazione — text, heading, note, banner, icon, badge, progress, markdown, code, spinner, glossary, total
  • immissione — field, check, switch, choice, select, checklist, stepper, slider, rating, date, time, place, button, item, gallery, pagination, tree
  • compositi — checkbox_group, payment_request, swap_panel, ticket_offer: schede intere che l’app disegna a partire dallo stato che fornisce il bot
  • la finestra — mini_app: l’unico nodo che disegna qualcosa che questa applicazione non ha scritto, e di proposito l’ultimo della tabella. Si apre sopra la conversazione anziché al suo posto.

Un nome assente dalla lista disegna una casella che lo nomina, invece di niente — un modulo a cui manca in silenzio una riga è un modulo che qualcuno invia credendolo completo. L’aspetto è un token ("tone": "accent", "size": "lg"), risolto contro la tavolozza dell’app: non c’è una proprietà di colore, né una proprietà in pixel, né un’icona fuori dalla lista bianca — ed è questo che impedisce a un bot di disegnare qualcosa con la forma del foglio di conferma dell’app stessa dentro una conversazione di cui qualcuno si fida già. Un "color" grezzo viene ignorato, e c’è un test che lo dice.

Come torna indietro un valore su un canale che porta una sola stringa

  • Un controllo il cui valore è il nodo — button, check, switch — manda la sua stringa tap immutata. Il bot ha lo stato vecchio e sa quale deve essere quello nuovo.
  • Un controllo che porta un valore — select, choice, checklist, stepper, slider, rating, date, field — manda tap con il valore in coda: con "tap": "form:seats", scegliere quattro arriva come form:seats:4. Il testo libero è l’ultimo segmento (todo:rename:2:Buy oat milk), quindi staccalo invece di spezzare a ogni due punti.
  • Un nodo senza tap è disegnato inerte. È così che una specifica dice “questo mostra, non chiede” — e un modulo finito manda la specifica successiva senza nessuna stringa tap, ed è così che smette di essere premibile.
  • callback_data è di 64 byte nella Bot API, quindi un valore scelto deve essere corto: una lista di sigle di asset va bene, una lista di frasi no.

Ogni controllo, e cosa torna indietro

Una tabella, perché ci sta dentro tutto il protocollo. Manda il nodo di sinistra; la pressione a destra è ciò che arriva come callback_query.data. Ognuno di questi è preso da examples/widgetbot.rs, che è un bot che puoi far girare.

Il nodo che mandiCosa torna a una pressione
{"type": "button", "label": "Book it", "primary": true, "tap": "form:place"}form:place
il nodo è il valore
{"type": "switch", "id": "remind", "label": "Remind me", "on": false, "tap": "form:remind"}form:remind
nudo — il bit vecchio ce l’hai tu e sai qual è quello nuovo
{"type": "stepper", "id": "seats", "label": "Seats", "value": 2, "min": 1, "max": 8, "tap": "form:seats"}form:seats:4
il valore è messo in coda
{"type": "select", "id": "sitting", "value": "main", "options": [{"value": "early", "label": "Early — 18:00"}, …], "tap": "form:sitting"}form:sitting:early
il value dell’opzione, mai la sua etichetta
{"type": "checklist", "id": "extras", "values": ["window"], "options": […], "tap": "form:extras"}form:extras:cake
l’elemento toccato, non l’insieme nuovo — a ribaltarlo sei tu
{"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
il testo libero è l’ultimo segmento
{"type": "text", "text": "Order #4821"} — senza tapniente. Disegnato inerte: questo mostra, non chiede

Quindi un gestore spezza due volte e non di più: una per il verbo, una per il campo, e quello che resta è il valore.

LEGGERE UNA PRESSIONE
# 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.

Il ciclo, da un capo all’altro

Tre chiamate, e quella di mezzo è dell’app. È tutto qui — una scheda che chiede qualcosa, una pressione, e la scheda ridisegnata con dentro la risposta.

Il numero non si muove finché non lo muove il bot
9:41
‹tablebotbot
tablebot
Persone−23+
GiornoVen, 20
Prenota
modificato
seats:3 → tablebot
+Scrivi un messaggio…☺

Le stesse tre chiamate del blocco qui sotto, riprodotte. Premi il più e guarda il conteggio restare dov’è: l’app ha mandato seats:3 e non ha nient’altro su cui basarsi. Cambia quando arriva la specifica successiva — che è tutto il modello di stato in un controllo solo, e la ragione per cui in questo protocollo non ci sono delta da nessuna parte.

CHIEDI → PREMI → RIDISEGNA
# 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… } }

Rifiutare sono le stesse tre chiamate senza la terza: rispondi "One to eight." e non mandare una specifica nuova, e la scheda resta com’era. Non c’è modo di fallire in silenzio, perché il controllo è disegnato in attesa finché la risposta non arriva.

Chiedere qualcosa di scritto

Un field è l’unico controllo che porta parole. Quello che torna è ciò che è stato scritto, come ultimo segmento — e ciò che ne fa un modulo invece di quattro messaggi è che anche la forma è tua: una specifica può far crescere una riga che un momento prima non c’era.

UN CAMPO, E UN VERDETTO SU DI ESSO
# 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." }

Due cose su un campo vale la pena saperle prima di scriverne uno. Si fa vivo quando lo si lascia, non a ogni tasto — quindi la pressione arriva una volta sola, con quello che è stato scritto. E il suo value è soltanto ciò con cui la casella comincia: per svuotare un campo, o per riportarlo vuoto a un secondo tentativo, dagli un id nuovo. Per questo gli id qui sopra portano il numero del giro (email‑1, email‑2) — una nuova identità è il modo in cui una casella torna vuota.

Tre schede intere, al prezzo di una macchina a stati

todo_list, swap_panel e ticket_offer sono compositi: la scheda la disegna l’app, lo stato lo fornisce il bot. Il vocabolario delle pressioni è quello della scheda.

UNA LISTA CHE DISEGNA L’APP
"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"

La validazione è tua, e non è una regola sull’ordine

Ogni nodo d’immissione accetta hint ed error, e sono entrambe parole del bot. Niente nel disegnatore sa che aspetto abbia un indirizzo e-mail o che una lista di spunte abbia un massimo: il valore sale, il bot giudica, e la specifica successiva porta il verdetto. Un client che giudicasse da sé sarebbe un secondo parere su regole che vivono altrove, e i due sarebbero in disaccordo il giorno in cui cambi le tue — in silenzio, e nella direzione di lasciar passare qualcosa. Ricontrolla anche in entrata: nascondere un pulsante non ferma un evento ripetuto.

VEDERLO SENZA SCRIVERE UN BOT

Impostazioni → Widget dinamici nell’app percorre l’intero ciclo con un finto interlocutore dall’altra parte: le stesse specifiche, le stesse stringhe di evento, e sotto ogni scheda un pannello che mostra entrambe le metà del filo alla lettera. Poi cargo run -p botapi --example widgetbot sono le stesse sei schede mosse da un bot vero — la versione che può mostrare anche una pressione rifiutata, arrivata due volte, o scaduta.