(Kotlin) How to Add Interactive UI to Messages
Prerequisites
We assume that:
You have the source code of the "Remind me" bot.
You completed the How to Create a Chatbot tutorial. More specifically, you completed the steps that set up the environment:
What will we do
In this tutorial we will extend the functionality of the 'Remind me' bot that we created in the (Kotlin) How to Create a Chatbot tutorial. More specifically, we'll add UI elements to one of the bot messages: When the user sends remind
without specifying the exact time, the bot will send the user a message containing three buttons with the predefined time.
If you don't want to pass this tutorial step by step and want just look at the resulting code, it's totally OK – here's the source code.
Step 1. Create a message containing interactive buttons
As you might remember from the (Kotlin) How to Create a Chatbot tutorial, Space SDK offers the separate DSL for creating chat messages. We call it Message Builder. Previously, we used it only to add fancy look to our messages. Now, let's use it to create a message with clickable buttons inside.
Currently, buttons are the only interactive elements in messages. A button
must have an assigned action
: When a user clicks the button, Space will send a special MessageActionPayload
type of payload. Such a payload contains action ID and action arguments that we can parse in our chatbot.
MessageControlGroupBuilder
, MessageFieldBuilder
, MessageSectionBuilder
: These are the classes that let us extend the message builder DSL. The button
belongs to control group elements. So, we will extend the MessageControlGroupBuilder
class with our own custom button function.
Let's create a new message type that will suggest the user three reminder time intervals with three buttons.
Open the
CommandRemind.kt
file and append the following code:private fun MessageControlGroupBuilder.remindButton(delayMs: Long, reminderText: String) { val text = "${delayMs / 1000} seconds" val style = MessageButtonStyle.PRIMARY val action = PostMessageAction("remind", "$delayMs $reminderText") button(text, action, style) }Here we predefine a button with our custom style and text. The most important variable here is
action
: It returns an instance ofPostMessageAction
with theremind
action ID, a timer delay value, and a reminder text.This is how such a button will look:
Now, let's create a message that contains the
remindButton
. For example, let's add three buttons to the message: each for a certain time delay.Append the following code to
CommandRemind.kt
:private fun suggestRemindMessage(reminderText: String): ChatMessage { return message { section { text("Remind me in ...") controls { // buttons for 5, 60, and 300 seconds remindButton(5 * 1000, reminderText) remindButton(60 * 1000, reminderText) remindButton(300 * 1000, reminderText) } } } }The final message will look like follows:
Now, let's decide when we will show the user our newly created
suggestRemindMessage()
. The most obvious decision is to show it when the user sent theremind
command and a reminder text but didn't specify the time interval.In the
CommandRemind.kt
, find therunRemindCommand()
function and update it as shown below:suspend fun runRemindCommand(payload: MessagePayload) { val remindMeArgs = getArgs(payload) when { remindMeArgs == null -> { sendMessage(payload.userId, helpMessage()) } remindMeArgs.delayMs == null && remindMeArgs.reminderText.isNotEmpty() -> { sendMessage(payload.userId, suggestRemindMessage(remindMeArgs.reminderText)) } remindMeArgs.delayMs == null -> { sendMessage(payload.userId, helpMessage()) } else -> { remindAfterDelay(payload.userId, remindMeArgs.delayMs, remindMeArgs.reminderText) } } }Done! Now we have a message with buttons that will be returned to a user when the user sends
remind
without a time interval. But what happens when the user actually clicks one of the buttons?
Step 2. Process the MessageActionPayload
When a user clicks the button, Space will send MessageActionPayload
payload to the bot. The payload contains an ID of the action specified in the button and action arguments. Our task is to process the payload.
First, let's create an overload for the
runRemindCommand(payload: MessagePayload)
function. The existing one accepts theMessagePayload
while we need it to also acceptMessageActionPayload
.Open the
CommandRemind.kt
and append the code:suspend fun runRemindCommand(payload: MessageActionPayload) { val remindMeArgs = getArgs(payload) ?: return val delayMs = remindMeArgs.delayMs ?: return val reminderText = remindMeArgs.reminderText remindAfterDelay(payload.userId, delayMs, reminderText) } // overload of getArgs that accepts MessageActionPayload private fun getArgs(payload: MessageActionPayload): RemindMeArgs? { val args = payload.actionValue val delayMs = args.substringBefore(" ").toLongOrNull() ?: return null val reminderText = args.substringAfter(" ").trimStart().takeIf { it.isNotEmpty() } ?: return null return RemindMeArgs(delayMs, reminderText) }Here we take an action argument from the payload using
payload.actionValue
and run the timer with this argument.Now let's teach our chatbot's endpoint to process the
MessageActionPayload
.Open the
Routes.kt
file and update theApplication.configureRouting()
function:fun Application.configureRouting() { routing { post("api/space") { // read request body val body = call.receiveText() // verify if the request comes from a trusted Space instance val signature = call.request.header("X-Space-Public-Key-Signature") val timestamp = call.request.header("X-Space-Timestamp")?.toLongOrNull() // verifyWithPublicKey gets a key from Space, uses it to generate message hash // and compares the generated hash to the hash in a message if (signature.isNullOrBlank() || timestamp == null || !spaceClient.verifyWithPublicKey(body, timestamp, signature) ) { call.respond(HttpStatusCode.Unauthorized) return@post } when (val payload = readPayload(body)) { is ListCommandsPayload -> { // Space requests the list of supported commands call.respondText( ObjectMapper().writeValueAsString(getSupportedCommands()), ContentType.Application.Json ) } is MessagePayload -> { // user sent a message to the application val command = supportedCommands.find { it.name == payload.command() } if (command == null) { runHelpCommand(payload) } else { launch { command.run(payload) } } call.respond(HttpStatusCode.OK, "") } is MessageActionPayload -> { when (payload.actionId) { "remind" -> { launch { runRemindCommand(payload) } } else -> error("Unknown command ${payload.actionId}") } // After sending a command, Space will wait for HTTP OK confirmation call.respond(HttpStatusCode.OK, "") } } } } }Here we add the
is MessageActionPayload
condition that checks theactionId
and if it'sremind
, runs our just-added overload ofrunRemindCommand
.Nice! Let's check out how it works!
Step 3. Run the bot
Start our application by clicking
Run
in the gutter next to themain
function inApplication.kt
:Open your Space instance and find the bot: press Ctrl+K and type its name.
Send the
remind
message.In the response, click the 5 seconds button.
Great job! We have successfully added a message with buttons to our chatbot.