Notesclub

Deploy a chat app with Kino

deploy_apps.livemd

Deploy a chat app with Kino

Mix.install([
  {:kino, "~> 0.9.0"}
])

Introduction

In this notebook, we will build and deplay a chat application. To do so, we will use Livebook’s companion library called kino.

In a nutshell, Kino is a library that you install as part of your notebooks to make your notebooks interactive. Kino comes from the Greek prefix “kino-“ and it stands for “motion”. As you learn the library, it will become clear that this is precisely what it brings to our notebooks.

Kino can render Markdown, animate frames, display tables, manage inputs, and more. It also provides the building blocks for extending Livebook with charts, smart cells, and much more. For building notebook applications, we rely on two main building blocks: Kino.Control and Kino.Frame.

You can see kino listed as a dependency above, so let’s run the setup cell and get started.

Kino.Control

The simplest control is Kino.Control.button/1. Let’s give it a try:

click_me = Kino.Control.button("Click me!")

Execute the cell above and the button will be rendered. You can click it, but nothing will happen. Luckily, we can subscribe to the button events:

Kino.Control.subscribe(click_me, :click_me)

Now that we have subscribed, every time the button is clicked, we will receive a message tagged with :click_me. Let’s print all messages in our inbox:

Process.info(self(), :messages)

Now execute the cell above, click the button a couple times, and re-execute the cell above. For each click, there is a new message in our inbox. There are several ways we can consume this message. Let’s see a different one in the next example.

Enumerating controls

All Kino controls are enumerable. This means we can treat them as a collection, an infinite collection in this case, and consume their events. Let’s define another button:

click_me_again = Kino.Control.button("Click me again!")

And now let’s consume it:

for event <- click_me_again do
  IO.inspect(event)
end

Now, as you submit the button, you should see a new event printed. However, there is a downside: we are now stuck inside this infinite loop of events. Luckily, we started this particular section as a branched section, which means the main execution flow will not be interrupted. But it is something you should keep in mind in the future. You can also stop it by pressing the “Stop” button above the Code cell.

Kino.Frame and animations

Kino.Frame allows us to render an empty frame and update it as we progress. Let’s render an empty frame:

frame = Kino.Frame.new()

Now, let’s render a random number between 1 and 100 directly in the frame:

Kino.Frame.render(frame, "Got: #{Enum.random(1..100)}")

Notice how every time you reevaluate the cell above it updates the frame. You can also use Kino.Frame.append/2 to append to the frame:

Kino.Frame.append(frame, "Got: #{Enum.random(1..100)}")

Appending multiple times will always add new contents. The content can be reset by calling Kino.Frame.render/2 or Kino.Frame.clear/1.

One important thing about frames is that they are shared across all users. If you open up this same notebook in another tab and execute the cell above, it will append the new result on all tabs. This means we can use frames for building collaborative applications within Livebook itself!

You can combine this with loops to dynamically add contents or animate your notebooks. In fact, there is a convenience function called Kino.animate/2 to be used exactly for this purpose:

Kino.animate(100, fn i ->
  Kino.Markdown.new("**Iteration: `#{i}`**")
end)

The above example creates a new frame behind the scenes and renders new Markdown output every 100ms. You can use the same approach to render regular output or images too!

There’s also Kino.animate/3, in case you need to accumulate state or halt the animation at certain point. Both animate functions allow an enumerable to be given, which means we can animate a frame based on the events of a control:

button = Kino.Control.button("Click") |> Kino.render()

Kino.animate(button, 0, fn _event, counter ->
  new_counter = counter + 1
  md = Kino.Markdown.new("**Clicks: `#{new_counter}`**")
  {:cont, md, new_counter}
end)

One of the benefits of using animate to consume events is that it does not block the notebook execution and we can proceed as usual.

Putting it all together

We have learned about controls and frames, which means now we are ready to build our chat application.

The first step is to define the frame we want to render our chat messages:

frame = Kino.Frame.new()

Now we will use a new control, called forms, to render and submit multiple inputs at once:

inputs = [
  name: Kino.Input.text("Name"),
  message: Kino.Input.text("Message")
]

form = Kino.Control.form(inputs, submit: "Send", reset_on_submit: [:message])

Now, every time the form is submitted, we want to append the message to a frame. We have learned about Kino.animate/3, that receives control events, but unfortunately it only updates frames in place, while we want to always append content. We could accumulate the content ourselves and always re-render it all on the frame, but that sounds a bit wasteful.

Luckily, Kino also provides a function called listen. listen also consumes events from controls and enumerables, but it does not assume we want to render a frame, ultimately giving us more control. Let’s give it a try:

Kino.listen(form, fn %{data: %{name: name, message: message}, origin: origin} ->
  if name != "" and message != "" do
    content = Kino.Markdown.new("**#{name}**: #{message}")
    Kino.Frame.append(frame, content)
  else
    content = Kino.Markdown.new("_ERROR! You need a name and message to submit..._")
    Kino.Frame.append(frame, content, to: origin)
  end
end)

Execute the cell above and your chat app should be fully operational. Scroll up, submit messages via the form, and see them appear in the frame.

Implementation-wise, our listen above receives the form events, which includes the value of each input. If a name and message have been given, we append it to the frame. If one of them is missing, we append an error message to the frame with the to: origin option. This means that particular message will be sent only to the user who submitted the form, instead of everyone.

You can also open up this notebook on different tabs and emulate how different users can chat with each other.

Deploying

Our chat application is ready, therefore it means we are ready to deploy! Click on the icon on the sidebar.

Now, define a slug for your deployment, such as “chat-app”, set a password (or disable password protection), and click “Deploy”.

Once you do so, you will see that… the deployed application will be in a “Booting” state forever? Well, clearly something has gone wrong.

To understand what went wrong, we need to talk about how the Deploy feature works. When you deploy a notebook, Livebook will execute all of the code cells in the notebook. Your application will effectively be all frames, controls, etc. that you rendered on the page. Once all cells successfully evaluate, the application finishes booting.

However, if you remember the “Enumerating controls” section, we did create an infinite loop there! And because that cell never terminates, the booting process never completes.

The solution is easy, delete the whole “Enumerating controls” section (or just that code cell), and try again. Now booting should finish rather quickly and you will be able to interact with your newly deployed app. Feel free to further improve it, by removing frames from previous sections that do not belong to the chat app or by adding new features.

Congratulations on shipping!