I came across the lack of documentation on how to pass/trigger the modal dialog used by the Kino.DB smart cell when setting up an interactive chat tree widget for working with my GenAI multi provider client wrapper https://github.com/noizu-labs-ml/genai
It turns out to be pretty straight forward, the javscript ctx module exposes a function for launching the modal which can then be passed on down to your smart cell for use in code generation. The livebook the below was generated from is here: Google Gist
SmartCell Secret Input Modal Example
Mix.install([
{:kino, "~> 0.12.0"}
])
Setup Frame
Quick hack to expose frame to smart cell demo.
frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, true)
Break Down
Now here is a quick break down of our of the steps needed to hook up to the LiveBook secret and secret selector modal pop up in our own smart cell provider.
First: we add a secret_key field to our context
@impl true
def init(attrs, ctx) do
ctx =
assign(ctx,
secret_key: attrs["secret_key"]
)
{:ok, ctx, []}
end
Second: Event Handler for secret update
We listen for incoming updates to this key and push back a field updated message to inform the frontend of success.
@impl true
def handle_event("set_secret_key" = event, variable, ctx) do
ctx = assign(ctx, secret_key: variable["value"])
broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
{:noreply, ctx}
end
Third: Evaluate output code gen
Once our smart cell is evaluated we fetch and return the provided key from livebooks secret storage system or return a not set tuple. You could alternatively in this section check if all required inputs are set and return a blank string response wehn unavailable as the Kino DB connection_cell does.
@impl true
def to_source(attrs) do
secret = attrs["secret_key"]
if is_bitstring(secret) and secret != "" do
quote do
{:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
end
else
quote do
{:oh, :you_havent_set_the_secret_yet}
end
end
|> Kino.SmartCell.quoted_to_string()
end
Front End
Finally we define a basic html form to accept the required params.
We wrap the label and input for the secret input field with a on click event that invokes the javascript’s ctx.selectSecret method. It pops the input modal and once selected the provided secret hanlde is pushed back to our live cell’s set_secret_key event handler.
ctx.selectSecret(
(secretName) => {
ctx.pushEvent("set_secret_key", {
field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
value: secretName,
});
},
"YOUR_SECRET_KEY_DEFAULT",
{ title: "TELL ME YOUR SECRET" }
);
const el = ctx.root.querySelector(`.secret-modal`);
const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);
ctx.handleEvent("update_secret_key", (text) => {
alert("hey")
el_input.value = text;
});
Logging
To assist with inspecting the internal state of our smart cell I’ve added jury rigged (qiocl amd dirty) persistent_term backed toggler for logs and log output statements at the top of our smart cell methods.
method for passing our output frame and log on/off toggle to our smart cell.
An obvious improvement here for a longer term solution would be to pass in the frame as an input and, and track the log on/flag in the per instance ctx context state. And add the frame as the output of a cell on our live book.
frame = Kino.Frame.new()
:persistent_term.put({:demo, :frame}, frame)
:persistent_term.put({:demo, :log}, false)
To indicate the value has been assigned we add a listener for the handle_event update_secret_key broadcast event.
Here it is all together:
defmodule TheRobotLives.AddingSecretsToKinoSmartCell do
use Kino.JS
use Kino.JS.Live
use Kino.SmartCell, name: "Smart Cell Secret Selection Demo"
@impl true
def init(attrs, ctx) do
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
:persistent_term.get({:demo, :log}) &&
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
ctx =
assign(ctx,
secret_key: attrs["secret_key"]
)
{:ok, ctx, []}
end
@impl true
def handle_connect(ctx) do
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([actx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
:persistent_term.get({:demo, :log}) &&
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
{:ok, %{secret_key: ctx.assigns.secret_key}, ctx}
end
@impl true
def handle_event("set_secret_key" = event, variable, ctx) do
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([event: event, variable: variable, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
:persistent_term.get({:demo, :log}) &&
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
ctx = assign(ctx, secret_key: variable["value"])
broadcast_event(ctx, "update_secret_key", ctx.assigns.secret_key)
{:noreply, ctx}
end
@impl true
def handle_event("toggle_log" = event, args, ctx) do
s = :persistent_term.get({:demo, :log})
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)} - Set To #{inspect(!s)}\n\n```elixir\n#{inspect([event: event, args: args, ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
:persistent_term.put({:demo, :log}, !s)
{:noreply, ctx}
end
@impl true
def to_attrs(ctx) do
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([ctx: ctx], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
:persistent_term.get({:demo, :log}) &&
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
%{"secret_key" => ctx.assigns.secret_key}
end
@impl true
def to_source(attrs) do
content =
Kino.Markdown.new(
"- [x] #{inspect(__ENV__.function)}\n\n```elixir\n#{inspect([attrs: attrs], pretty: true, limit: :infinity, printable_limit: :infinity)}\n\n```"
)
:persistent_term.get({:demo, :log}) &&
Kino.Frame.append(:persistent_term.get({:demo, :frame}), content)
secret = attrs["secret_key"]
if is_bitstring(secret) and secret != "" do
quote do
{:all_your_scrests_belong_to_me, System.fetch_env!(unquote("LB_#{secret}"))}
end
else
quote do
{:oh, :you_havent_set_the_secret_yet}
end
end
|> Kino.SmartCell.quoted_to_string()
end
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 = `
<div class="app">
<div class="secret-modal">
<label class="label">Secret</label>
<input class="input" type="password" name="secret" />
</div>
<label class="label">Enable Frame Logs</label>
<input class="input" type="checkbox" name="log" />
</div>
`;
const el = ctx.root.querySelector(`.secret-modal`);
const el_input = ctx.root.querySelector(`.secret-modal [name="secret"]`);
ctx.handleEvent("update_secret_key", (text) => {
el_input.value = text;
});
el.addEventListener("click", (event) => {
const preselectName = "DEFAULT_ENV_VAR";
ctx.selectSecret(
(secretName) => {
ctx.pushEvent("set_secret_key", {
field: "if_you_wanted_to_impelment_a_handler_method_supporting_multiple_inputs",
value: secretName,
});
},
preselectName,
{ title: "TELL ME YOUR SECRET" }
);
});
const el2 = ctx.root.querySelector(`[name="log"]`);
el2.addEventListener("click", (event) => {
ctx.pushEvent("toggle_log", {});
});
ctx.handleSync(() => {
// Synchronously invokes change listeners
document.activeElement &&
document.activeElement.dispatchEvent(new Event("change"));
});
}
"""
end
asset "main.css" do
"""
// make it pretty
"""
end
end
Kino.SmartCell.register(TheRobotLives.AddingSecretsToKinoSmartCell)
And here it is in action LiveBook file