# Automating with Smart cells ```elixir Mix.install([ {:kino, "~> 0.18.0"}, {:jason, "~> 1.4"} ]) ``` ## Introduction So far we discussed how Livebook supports interactive outputs and covered creating custom outputs with Kino. Livebook v0.6 opens up the door to a whole new category of extensions that we call Smart cells. Just like the Code and Markdown cells, Smart cells are building blocks for our notebooks. A Smart cell provides a user interface specifically designed for a particular task. While `Kino.JS` and `Kino.JS.Live` are about customizing the output, without changing the runtime, Smart cells run just like a regular piece of code, however the code is generated automatically based on the UI interactions. Smart cells allow for accomplishing high-level tasks faster, without writing any code, yet, without sacrificing code! This characteristic makes them a productivity boost and also a great learning tool. Go ahead, place your cursor between cells and click on the + Smart button, you should see a list of suggested Smart cells. Feel free to try them out! As with outputs, we can define custom Smart cells through Kino, and that's what we're going to explore in this notebook! ## Basic concepts Smart cells consist of UI and state, which we implement using `Kino.JS` and `Kino.JS.Live` that we talked about in the previous guide. The additional element is code generation. Let's have a look at a simple Smart cell that lets the user type text and generates code for printing that text. ```elixir defmodule KinoGuide.PrintCell do use Kino.JS use Kino.JS.Live use Kino.SmartCell, name: "Print" @impl true def init(attrs, ctx) do ctx = assign(ctx, text: attrs["text"] || "") {:ok, ctx} end @impl true def handle_connect(ctx) do {:ok, %{text: ctx.assigns.text}, ctx} end @impl true def handle_event("update_text", text, ctx) do broadcast_event(ctx, "update_text", text) {:noreply, assign(ctx, text: text)} end @impl true def to_attrs(ctx) do %{"text" => ctx.assigns.text} end @impl true def to_source(attrs) do quote do IO.puts(unquote(attrs["text"])) end |> Kino.SmartCell.quoted_to_string() end asset "main.js" do """ export function init(ctx, payload) { root.innerHTML = `
Say what?
`; const textEl = document.getElementById("text"); textEl.value = payload.text; ctx.handleEvent("update_text", (text) => { textEl.value = text; }); textEl.addEventListener("change", (event) => { ctx.pushEvent("update_text", event.target.value); }); ctx.handleSync(() => { // Synchronously invokes change listeners document.activeElement && document.activeElement.dispatchEvent(new Event("change")); }); } """ end end Kino.SmartCell.register(KinoGuide.PrintCell) ``` Most of the implementation includes regular `Kino.JS.Live` bits that should feel familiar, specifically `init/2`, `handle_connect/1`, `handle_event/3` and the JS module. Let's go through the new parts. Firstly, we add `use Kino.SmartCell` and specify the name of our Smart cell, that's the name that will show up in Livebook. Next, we define the `to_attrs/1` callback responsible for serializing the Smart cell state. The attributes are stored in the notebook source as JSON. When opening an existing notebook, a Smart cell is started and receives the attributes as the first argument to the `init/2` callback to restore the relevant state. On initial start an empty map is given. The other new callback is `to_source/1`. It is used to generate source code based on the Smart cell attributes. Elixir has built in support for source code manipulation, that's why in our example we use the `quote` construct, instead of a less robust string interpolation. Finally, we register our new smart cell using `Kino.SmartCell.register/1`, so that Livebook picks it up. Note that in practice we would put the Smart cell in a package and we would register it in `application.ex` when starting the application. Note that we register a synchronization handler on the client with `ctx.handleSync(() => ...)`. This optional handler is invoked before evaluation and it should flush any deferred UI changes to the server. In our example we listen to input's "change" event, which is only triggered on blur, so on synchronization we trigger it programmatically. Now let's try out the new cell! We already inserted one below, but you can add more with the + Smart button. ```elixir IO.puts("Hello!") ``` Focus the Smart cell and click the "Source" icon. You should see the generated source code, however the editor is in read-only mode. Now switch back to the Smart cell UI, modify the input and see how the source code changes. You can evaluate the cell, as with a regular Code cell. ## Collaborative editor Livebook puts a strong emphasis on collaboration and Smart cells are no exception. The above cell works fine with multiple users, however the changes to the input are atomic, so if users edit it simultaneously, one would override the other. This behaviour is alright for small parameter inputs, however some cells may require editing a larger chunk of text, such as an SQL query or JSON data. Livebook already provides a collaborative editor for the Code and Markdown cells. Fortunately, a Smart cell can opt-in for an editor as well! To showcase this feature, let's build a cell on top of `System.shell/2`. ```elixir defmodule KinoGuide.ShellCell do use Kino.JS use Kino.JS.Live use Kino.SmartCell, name: "Shell script" @impl true def init(attrs, ctx) do source = attrs["source"] || "" {:ok, assign(ctx, source: source), editor: [source: source]} end @impl true def handle_connect(ctx) do {:ok, %{}, ctx} end @impl true def handle_editor_change(source, ctx) do {:ok, assign(ctx, source: source)} end @impl true def to_attrs(ctx) do %{"source" => ctx.assigns.source} end @impl true def to_source(attrs) do quote do System.shell( unquote(quoted_multiline(attrs["source"])), into: IO.stream(), stderr_to_stdout: true ) |> elem(1) end |> Kino.SmartCell.quoted_to_string() end defp quoted_multiline(string) do {:<<>>, [delimiter: ~s["""]], [string <> "\n"]} end asset "main.js" do """ export function init(ctx, payload) { ctx.importCSS("main.css"); root.innerHTML = `
Shell script
`; } """ end asset "main.css" do """ .app { padding: 8px 16px; border: solid 1px #cad5e0; border-radius: 0.5rem 0.5rem 0 0; border-bottom: none; } """ end end Kino.SmartCell.register(KinoGuide.ShellCell) ``` The tuple returned from `init/2` has an optional third element that we use to configure the Smart cell. To enable the editor, all we need is a configuration option! The editor is fully managed by Livebook, separately from the Smart cell UI and the editor content is placed in `attrs` under the name specified with `:attribute`. This way we can access it in `to_source/1`. In this example we don't need any other attributes, so in the UI we only show the cell name. ```elixir System.shell( """ echo "There you are:" ls -dlh $(pwd) exit 1 """, into: IO.stream(), stderr_to_stdout: true ) |> elem(1) ``` ## Stepping it up Now that we discussed both regular inputs and the collaborative editor, it's time to put it all together. For our final example we will build a Smart cell that takes JSON data and generates an Elixir code with a matching data structure assigned to a user-defined variable. ```elixir defmodule KinoGuide.JSONConverterCell do use Kino.JS use Kino.JS.Live use Kino.SmartCell, name: "JSON converter" @impl true def init(attrs, ctx) do json = attrs["json"] || "" ctx = assign(ctx, variable: Kino.SmartCell.prefixed_var_name("data", attrs["variable"]), json: json ) {:ok, ctx, editor: [source: json, language: "json"]} end @impl true def handle_connect(ctx) do {:ok, %{variable: ctx.assigns.variable}, ctx} end @impl true def handle_event("update_variable", variable, ctx) do ctx = if Kino.SmartCell.valid_variable_name?(variable) do assign(ctx, variable: variable) else ctx end broadcast_event(ctx, "update_variable", ctx.assigns.variable) {:noreply, ctx} end @impl true def handle_editor_change(json, ctx) do {:ok, assign(ctx, json: json)} end @impl true def to_attrs(ctx) do %{ "variable" => ctx.assigns.variable, "json" => ctx.assigns.json } end @impl true def to_source(attrs) do case Jason.decode(attrs["json"]) do {:ok, data} -> quote do unquote(quoted_var(attrs["variable"])) = unquote(Macro.escape(data)) :ok end |> Kino.SmartCell.quoted_to_string() _ -> "" end end defp quoted_var(nil), do: nil defp quoted_var(string), do: {String.to_atom(string), [], nil} asset "main.js" do """ export function init(ctx, payload) { ctx.importCSS("main.css"); ctx.importCSS("https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap"); root.innerHTML = `
`; const variableEl = ctx.root.querySelector(`[name="variable"]`); variableEl.value = payload.variable; variableEl.addEventListener("change", (event) => { ctx.pushEvent("update_variable", event.target.value); }); ctx.handleEvent("update_variable", (variable) => { variableEl.value = variable; }); ctx.handleSync(() => { // Synchronously invokes change listeners document.activeElement && document.activeElement.dispatchEvent(new Event("change")); }); } """ end asset "main.css" do """ .app { font-family: "Inter"; display: flex; align-items: center; gap: 16px; background-color: #ecf0ff; padding: 8px 16px; border: solid 1px #cad5e0; border-radius: 0.5rem 0.5rem 0 0; } .label { font-size: 0.875rem; font-weight: 500; color: #445668; text-transform: uppercase; } .input { padding: 8px 12px; background-color: #f8fafc; font-size: 0.875rem; border: 1px solid #e1e8f0; border-radius: 0.5rem; color: #445668; min-width: 150px; } .input:focus { outline: none; } """ end end Kino.SmartCell.register(KinoGuide.JSONConverterCell) ``` In this case, one of the attributes is a variable name, so we use `Kino.SmartCell.prefixed_var_name/2`. For a fresh cell it will generate a default variable name that isn't already taken by another Smart cell. You can test this by inserting another JSON cell, where the variable name should default to `data2`. This time we also added some proper styling to show a Smart cell in its full glory! ```elixir data = [ %{ "id" => 1, "name" => "livebook", "topics" => ["elixir", "visualization", "realtime", "collaborative", "notebooks"], "url" => "https://github.com/livebook-dev/livebook" }, %{ "id" => 2, "name" => "kino", "topics" => ["charts", "elixir", "livebook"], "url" => "https://github.com/livebook-dev/kino" } ] :ok ``` This cell accomplishes a coding task that would otherwise be tedious without parsing the JSON from a string. We could further extend the cell with options to convert keys to snake case or use atoms! We went through a couple examples, however there is even more power to Smart cells than that! One feature we haven't discussed is access to notebook variables and evaluation results. Those allow for making the UI data-driven! Hopefully this notebook gives you a good overview of the Smart cells' potential, now it's your turn to unlock it. ⚡ Once your Smart cell is complete, you can publish it as a package to [Hex.pm](https://hex.pm/) and allow anyone in the community to reuse them, or even find one that fits your needs. To learn more from examples, check out the several examples under the [Livebook Organization](https://github.com/livebook-dev). ## Final words Congratulations, you finished our "course" on Kino! Throughout those guides, you first mastered Kino's API and learned how to use its building blocks to build a chat app and a multiplayer pong game. Then, you learned how you can take Kino anywhere you want by implementing your own kinos and cells with Elixir and JavaScript. We are looking forward to see what you can build with it! 🚀