JetBrains Space Help

(Kotlin) How to Add Interactive UI to Messages


We assume that:

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.

Space chatbot with buttons

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 control group elements. So, we will extend the MessageControlGroupBuilder class with our own custom button function.

Let's create a new type of message that will suggest the user three reminder time intervals with three buttons.

  1. Open the CommandRemind.kt file and append the following code:

    fun MessageControlGroupBuilder.remindButton(delayMs: Long) { val text = "${delayMs / 1000} seconds" val style = MessageButtonStyle.PRIMARY val action = PostMessageAction("remind", delayMs.toString()) 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 of PostMessageAction with the remind action ID and the timer delay value.

    This is how such a button will look:

    Chatbot message button
  2. 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:

    fun suggestRemindMessage(): ChatMessage { return message { section { header = "Remind me in ..." controls { // buttons for 5, 60, and 300 seconds remindButton(5 * 1000) remindButton(60 * 1000) remindButton(300 * 1000) } } } }

    The final message will look like follows:

    Chatbot interactive message
  3. 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 the remind command but didn't specify the time interval.

    In the CommandRemind.kt, find the runTimer() function and update it as shown below:

    private suspend fun runTimer(context: CallContext, delayMs: Long?) { if (delayMs != null) { sendMessage(context, acceptRemindMessage(delayMs)) coroutineScope { delay(delayMs) sendMessage(context, remindMessage(delayMs)) } } else { // This is the line to edit! // if user doesn't specify interval, show buttons sendMessage(context, suggestRemindMessage()) } }
  4. Done! Now we have a message with buttons that will be returned to a user when the user sends remind without arguments. 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.

  1. First, let's create an overload for the commandRemind(context: CallContext, payload: MessagePayload) function. The existing one accepts the MessagePayload while we need it to also accept MessageActionPayload.

    Open the CommandRemind.kt and append the code:

    suspend fun commandRemind(context: CallContext, payload: MessageActionPayload) { val args = payload.actionValue val delayMs = args.toLongOrNull() runTimer(context, delayMs) }

    Here we take an action argument from the payload using payload.actionValue and run the timer with this argument.

  2. Now let's teach our chatbot's endpoint to process the MessageActionPayload.

    Open the Routes.kt file and update the Routing.api() function:

    fun Routing.api() { post("api/myapp"){ // 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 } // read payload and get context (user id) val payload = readPayload(body) val context = getCallContext(payload) // JSON serializer val jackson = ObjectMapper() // analyze the message payload // MessageActionPayload = user clicks a button // MessagePayload = user sends a command // ListCommandsPayload = user types a slash or a char when (payload) { is MessageActionPayload -> { when (payload.actionId) { "remind" -> { // The reminder can be set on any time // As this could be a long time interval, // we run commandRemind in a separate thread launch { commandRemind(context, payload) } } else -> error("Unknown command ${payload.actionId}") } // After sending a command, Space will wait for HTTP OK confirmation call.respond(HttpStatusCode.OK, "") } is ListCommandsPayload -> { call.respondText(jackson.writeValueAsString(commandListAllCommands(context)), ContentType.Application.Json) } is MessagePayload -> { val command = commands.find { == payload.command() } if (command == null) { commandHelp(context) } else { launch {, payload) } } call.respond(HttpStatusCode.OK, "") } } } }

    Here we add the is MessageActionPayload condition that checks the actionId and if it's remind, runs our just-added overload of commandRemind.

  3. Nice! Let's check out how it works!

Step 3. Run the bot

  1. Start our application by clicking Run in the gutter next to the main function in Application.kt:

    Run Ktor server
  2. Open your Space instance and find the bot: press Ctrl+K and type its name.

    Find the bot
  3. Send the remind message.

  4. In the response, click the 5 seconds button.

    Run the chatbot

Great job! We have successfully added a message with buttons to our chatbot.

Last modified: 30 June 2022