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!