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:
- Define a inputs and forms for the user to interact with
- Hook into the form events to receive and validate data
-
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: