Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Deploy a chat app with Kino

deploy_apps.livemd

Deploy a chat app with Kino

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

Introduction

In this notebook, we will build and deploy 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 stream of events in this case. Let’s define another button:

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

And now let’s consume those events. Because the stream is infinite, we will consume them inside a separate process, in order to not block our notebook:

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

Now, as you submit the button, you should see a new event printed. It happens this pattern of consuming events without blocking the notebook is so common that Kino even has a convenience functions for it, such as Kino.animate/2 and Kino.listen/2. Let’s keep on learning.

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 we want to append the message to a frame every time the form is submitted. 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, the call to listen 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.

The append function also accepts two options worth discussing. The first one, used in the example above, is the to: origin option. This means the particular message will be sent only to the user who submitted the form, instead of everyone.

Another option frequently used is :temporary. All messages are stored in the frame by default. This means that, if you reload the page, or join late, you can see all history. If you set :temporary to true, that will no longer be the case. Note all messages sent with the :to option are temporary.

You can also open up this notebook on different tabs and emulate how different users can chat with each other. Give it a try!

Deploying

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

Now, define a slug for your deployment, such as “chat-app”, set a password (or disable password protection), and click “Deploy”. Now you can click the URL and interact with the chat app, as you did inside the notebook.

When you deploy a notebook, Livebook will execute all of the code in the notebook from beginning to end. This sets up our whole application, including frames and forms, for users to interact with. In case something goes wrong, you can always click the Livebook icon on the deployed app and choose to debug the deployed notebook session.

From here onwards, feel free to adjust the deployed application, by removing unused outputs from earlier sections or by adding new features.

Congratulations on shipping!

Docker deployment

Now that you have deployed your first notebook as an application locally, you may be wondering: can I actually ship this production?

The answer is yes!

Click on the icon on the sidebar and you will find a “Deploy with Docker” link. Clicking on the link will open up a modal with instructions on deploying a single notebook or a folder with several entries through Docker.

If you want to develop and deploy notebooks as a team, check out Livebook Teams.

Where to go next

There are many types of applications you can build with notebooks. For example, we can use the foundation we learned here to develop any type of form-driven application. The structure is always the same:

  1. Define a inputs and forms for the user to interact with
  2. Hook into the form events to receive and validate data
  3. Render updates directly into Kino.Frame

The frame plays an essential role here. If you render to the frame without the to: origin option, the updates are sent to all users. With the to: origin option, the changes are visible only to a given user. This means you get full control if the application is collaborative or not.

Livebook also supports multi-session applications, where each user starts their own Livebook session on demand. By using Kino.Input and Kino.interrupt/2, it is common to build multi-session applications that execute step-by-step, similar to regular notebooks, without a need to setup form controls and events handlers as done in this guide. Furthermore, each session in a multi-session app has their own Elixir runtime, which provides isolation but also leads to higher memory usage per session.

To learn more about apps, here are some resources to dig deeper: