# Run as: iex --dot-iex path/to/notebook.exs
# Title: Build a chat app with Kino
Mix.install([
{:kino, "~> 0.18.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`](https://github.com/livebook-dev/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!
# ── Running as a Livebook app ──
# Our chat application is ready, therefore it means we are
# ready to run it!
# Click on the <i class="ri-livebook-deploy"></i>
# 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
# "Save".
# Click on the "Launch preview" button to run your notebook as an app.
# Now you can click the URL and interact with the chat app, as you did inside the notebook.
# When you run a notebook as an app, 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 running app and
# choose to debug the running notebook session.
# From here onwards, feel free to adjust the application,
# by removing unused outputs from earlier sections or by adding
# new features.
# Congratulations on shipping!
# ── Docker deployment ──
# Now that you have run your notebook as an application
# locally, you may be wondering: can I actually ship this production?
# The answer is yes!
# Click on the <i class="ri-livebook-deploy"></i> icon on the sidebar
# and you will find a "Manual Docker deployment" button. Clicking on it
# will open up a modal with instructions on deploying a single notebook
# or a folder with several entries through Docker.
# We also offer a service called [Livebook Teams](https://livebook.dev/teams),
# which comes with the following features for Livebook apps:
# * one-click deployment
# * authentication
# * authorization
# ── 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](https://www.youtube.com/watch?v=dSjryA1iFng),
# 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:
# * [The announcement of Livebook apps with livecoding of
# the application built in this guide](https://www.youtube.com/watch?v=q7T6ue7cw1Q)
# * [Livecoding of an audio-based chat application where
# a Neural Network is used to convert speech to text](https://www.youtube.com/watch?v=uyVRPEXOqzw)
# * [The announcement of multi-session apps with livecoding
# of a sample application](https://www.youtube.com/watch?v=dSjryA1iFng)