# 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 # 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 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)