Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

<!-- livebook:{"app_settings":{"output_type":"rich","show_source":true,"slug":"cinex"}} --> # CinEx ```elixir Mix.install([ {:kino, "~> 0.12.3"}, {:instructor, github: "acalejos/instructor_ex"}, {:erlexec, "~> 2.0"}, {:exterval, "~> 0.2.0"}, {:req, "~> 0.4.0"} ]) ``` ## Upload Struct ```elixir defmodule Upload do defstruct [:filename, :path] @video_types [:mp4, :ogg, :avi, :wmv, :mov] @audio_types [:wav, :mp3, :mpeg] @image_types [:jpeg, :jpg, :png, :gif, :svg, :pixel] defguard is_audio(ext) when ext in @audio_types defguard is_video(ext) when ext in @video_types defguard is_image(ext) when ext in @image_types defguard is_valid_upload(ext) when is_audio(ext) or is_video(ext) or is_image(ext) def accepted_types, do: @audio_types ++ @video_types ++ @image_types defp to_existing_atom(str) do try do {:ok, String.to_existing_atom(str)} rescue _ in ArgumentError -> {:error, "#{inspect(str)} is not an existing atom"} _e -> {:error, "Unknown Error ocurred in `String.to_existing_atom/1`"} end end def ext_type(filename) do with <<"."::utf8, rest::binary>> <- Path.extname(filename), {:ok, ext} <- to_existing_atom(rest) do ext end end def to_kino(upload = %__MODULE__{path: path}) do content = File.read!(upload.path) case ext_type(path) do ext when is_audio(ext) -> Kino.Audio.new(content, ext) ext when is_video(ext) -> Kino.Video.new(content, ext) ext when is_image(ext) -> Kino.Image.new(content, ext) end end def new(filename, path) do %__MODULE__{filename: filename, path: path} end def generate_temp_filename(extension \\ "mp4") do random_string = :crypto.strong_rand_bytes(8) |> Base.encode16() temp_dir = System.tmp_dir!() Path.join(temp_dir, "temp_#{random_string}.#{extension}") end end ``` ## Setup State Management ```elixir defmodule Models do @oai_models Req.get!("https://api.openai.com/v1/models", auth: {:bearer, System.fetch_env!("LB_OPENAI_TOKEN")} ) |> Map.get(:body) |> Map.get("data") |> Enum.filter(fn entry -> Map.get(entry, "id") |> String.contains?("gpt") end) |> Enum.sort_by(&Map.get(&1, "created"), :desc) |> Enum.map(fn entry -> model = Map.get(entry, "id") {model, model} end) @ollama_models Req.get!("http://localhost:11434/api/tags") |> Map.get(:body) |> Map.get("models") |> Enum.sort_by(fn entry -> {:ok, datetime, _offset} = entry |> Map.get("modified_at") |> DateTime.from_iso8601() datetime end) |> Enum.map(fn entry -> name = Map.get(entry, "name") {name, name} end) # There are many different ways to authenticate. This uses API Keys # See https://cloud.google.com/docs/authentication/api-keys # We filter for only Gemini 1.5 models since those are the only to support JSON mode in the API @gemini_models Req.get!("https://generativelanguage.googleapis.com/v1beta/models", headers: %{"x-goog-api-key" => System.fetch_env!("LB_GOOGLE_GEMINI_API_KEY")} ) |> Map.get(:body) |> Map.get("models") |> Enum.filter(fn entry -> Map.get(entry, "name") |> String.contains?("gemini-1.5") end) |> Enum.map(fn entry -> {Map.get(entry, "name"), Map.get(entry, "displayName")} end) def oai_models, do: @oai_models def ollama_models, do: @ollama_models def gemini_models, do: @gemini_models end ``` ```elixir defmodule FormState do use Agent @openai_config [ http_options: [receive_timeout: 10 * 60 * 1000], api_key: System.fetch_env!("LB_OPENAI_TOKEN"), api_url: "https://api.openai.com", adapter: Instructor.Adapters.OpenAI ] @ollama_config [ http_options: [receive_timeout: 10 * 60 * 1000], api_key: "ollama", api_url: "http://localhost:11434", adapter: Instructor.Adapters.OpenAI ] @gemini_config [ api_version: :v1beta, api_url: "https://generativelanguage.googleapis.com/", http_options: [receive_timeout: 60_000], api_key: System.fetch_env!("LB_GOOGLE_GEMINI_API_KEY"), adapter: Instructor.Adapters.Gemini ] def start_link(_init) do Agent.start_link( fn -> %{ config: @openai_config, provider: :openai, prompt: "", retries: 2, debug: false, explain_outputs: true, model: Models.oai_models() |> hd() |> elem(0), adapter: Application.get_env(:instructor, :adapter, Instructor.Adapters.OpenAI) } end, name: __MODULE__ ) end def update(:provider, :openai) do Agent.update(__MODULE__, fn state -> state |> Map.put(:config, @openai_config) |> Map.put(:provider, :openai) end) end def update(:provider, :ollama) do Agent.update(__MODULE__, fn state -> state |> Map.put(:config, @ollama_config) |> Map.put(:provider, :ollama) end) end def update(:provider, :gemini) do Agent.update(__MODULE__, fn state -> state |> Map.put(:config, @gemini_config) |> Map.put(:provider, :gemini) end) end def update(key, value) do Agent.update(__MODULE__, fn state -> Map.put(state, key, value) end) end def get(key) do Agent.get(__MODULE__, fn state -> Map.get(state, key) end) end end defmodule EditHistory do use Agent def start_link(_init) do Agent.start_link(fn -> :queue.new() end, name: __MODULE__) end def push(%Upload{} = upload, prompt \\ nil) do Agent.update(__MODULE__, fn history -> :queue.snoc(history, {upload, prompt}) end) end def undo_edit do Agent.get_and_update(__MODULE__, fn history -> popped = :queue.liat(history) {:queue.last(popped), popped} end) end def current do Agent.get(__MODULE__, fn history -> :queue.last(history) end) end def original do Agent.get(__MODULE__, fn history -> :queue.head(history) end) end def previous_edit do Agent.get(__MODULE__, fn history -> popped = :queue.liat(history) unless :queue.is_empty(popped) do :queue.last(popped) else nil end end) end def reset do Agent.get_and_update(__MODULE__, fn history -> original = :queue.head(history) {original, :queue.from_list([original])} end) end end ``` ```elixir Enum.each([EditHistory, FormState], &Kino.start_child!/1) ``` ## Setup Boilerplate ```elixir defmodule Boilerplate do def placeholder, do: """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Video Preview Placeholder with Spinner</title> <style> .video-preview-placeholder { width: 100%; max-width: 640px; height: 0; padding-bottom: 56.25%; /* 16:9 aspect ratio */ border: 2px dashed #ccc; display: flex; align-items: center; justify-content: center; background-color: #f9f9f9; color: #666; font-size: 20px; text-align: center; position: relative; box-sizing: border-box; margin: auto; } .spinner-container { display: flex; flex-direction: column; align-items: center; justify-content: center; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 2s linear infinite; margin-bottom: 10px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .message { font-size: 16px; color: #666; } </style> </head> <body> <div class="video-preview-placeholder"> <div class="spinner-container"> <%= if show_spinner do %> <div class="spinner"></div> <% end %> <div class="message"><%= message %></div> </div> </div> </body> </html> """ def log_template, do: """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Log Level Message Box</title> <style> .message-box { width: 100%; border: 2px solid; padding: 20px; box-sizing: border-box; margin: 20px 0; border-radius: 5px; font-size: 18px; text-align: left; } .message-box.error { border-color: #f44336; background-color: #fdecea; color: #f44336; } .message-box.success { border-color: #4caf50; background-color: #e8f5e9; color: #4caf50; } .message-box.info { border-color: #2196f3; background-color: #e3f2fd; color: #2196f3; } </style> </head> <body> <div class="message-box <%= level %>"> <%= message %> </div> </body> </html> """ def stdout_template, do: """ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><%= device %></title> <style> body { background-color: #1e1e1e; color: #c5c8c6; font-family: "Courier New", Courier, monospace; margin: 0; padding: 20px 20px 20px 5px; } .container { border: 1px solid #444; border-radius: 5px; overflow: hidden; } .header { background-color: #444; color: #c5c8c6; padding: 10px; font-weight: bold; text-transform: uppercase; } .output { background-color: #1d1f21; border-left: 4px solid <%= border_color %>; padding: 12px 12px 12px 5px; font-size: 16px; color: #c5c8c6; white-space: pre-wrap; word-break: break-all; } </style> </head> <body> <div class="container"> <div class="header"> <%= device %> </div> <div class="output"> <%= output %> </div> </div> </body> </html> """ def make_stdout(output, device, border_color \\ "gray") do Kino.HTML.new(EEx.eval_string(stdout_template(), binding())) end def make_log(message, level) do Kino.HTML.new(EEx.eval_string(log_template(), binding())) end end ``` ## Setup Form ```elixir original = Kino.Frame.new() prompt = Kino.Input.textarea("Prompt") upload = Kino.Input.file("Upload", accept: Upload.accepted_types()) errors = Kino.Frame.new(placeholder: false) submit_button = Kino.Control.button("Run!") submit_frame = Kino.Frame.new(placeholder: false) undo_frame = Kino.Frame.new(placeholder: false) reset_frame = Kino.Frame.new(placeholder: false) undo_button = Kino.Control.button("Undo") reset_button = Kino.Control.button("Reset") output = Kino.Frame.new(placeholder: false) logs = Kino.Frame.new(placeholder: false) debug_checkbox = Kino.Input.checkbox("Verbose Mode") debug_frame = Kino.Frame.new(placeholder: false) explain_checkbox = Kino.Input.checkbox("Explain Outputs", default: true) explain_frame = Kino.Frame.new(placeholder: false) retries = Kino.Input.number("# Retries", default: 2) provider = Kino.Input.select("Provider", [{:openai, "OpenAI"}, {:gemini, "Gemini"}, {:ollama, "Ollama"}]) models_frame = Kino.Frame.new(placeholder: false) oai_select = Kino.Input.select("Model", Models.oai_models()) gemini_select = Kino.Input.select("Model", Models.gemini_models()) ollama_select = Kino.Input.select( "Model", if(Models.ollama_models() == [], do: [{nil, "Pull Local Model to Use Ollama"}], else: Models.ollama_models() ) ) Kino.Frame.render(models_frame, oai_select) Kino.Frame.render( original, Kino.HTML.new( EEx.eval_string(Boilerplate.placeholder(), message: "Upload Media to Get Started", show_spinner: false ) ) ) model_info = Kino.Layout.grid([provider, models_frame], columns: 2) inputs = Kino.Layout.grid([prompt, retries], columns: 2, gap: 10) buttons = Kino.Layout.grid([submit_frame, undo_frame, reset_frame, explain_frame, debug_frame], columns: 7, gap: 1 ) Kino.Layout.grid([original, model_info, inputs, upload, buttons, output, logs]) ``` ## FFMPEG Instructions ```elixir defmodule Alfred do use Ecto.Schema use Instructor.Validator import Ecto.Changeset import Exterval @confidence_interval ~i<[0,10]//0.5> @system_prompt """ You are the companion Agent to another Agent whose job is to product execve-styled arguments for programs given a specific prompt. Your job is to interpret and explain the output of the command after it has been run. You will be given the prompt / task that originally generated the command, then you will be given the command that was run, along with the output that was generated. You do not need to re-explain what the task was or regurgitate what the command was. You only need to explain what the output means within the context of the task. If the task / prompt was a question, you should determine whether the provided output directly answers the question and if it does not you should answer it based on the output. If the output is not relevant to the prompt this should also be noted. You will also provide a confidence score about how confident you are about the above explanation. The confidence score is separate from the explanation. """ @primary_key false @doc """ ## Field Descriptions: - explanation: Explanation of the output given the context of the task and command that was run - confidence: Rating from 0 to 10 in increments of 0.5 of how confident you are in your answer, with higher scores being more confident. """ embedded_schema do field(:explanation, :string) field(:confidence, :float) end @impl true def validate_changeset(changeset) do changeset |> validate_inclusion(:confidence, @confidence_interval) end def execute(model, config, prompt, command, retries, outputs \\ [stdout: nil, stderr: nil]) do Instructor.chat_completion( [ mode: :json, model: model, response_model: __MODULE__, max_retries: retries, messages: [ %{ role: "system", content: @system_prompt }, %{ role: "user", content: """ Here's the prompt that generated the command: #{inspect(prompt)} """ }, %{ role: "user", content: """ Here's command: #{inspect(command)} """ }, Keyword.get(outputs, :stdout) && %{ role: "user", content: """ stdout: #{inspect(Keyword.fetch!(outputs, :stdout))} """ }, Keyword.get(outputs, :stderr) && %{ role: "user", content: """ stderr: #{inspect(Keyword.fetch!(outputs, :stderr))} """ } ] |> Enum.filter(& &1) ], config ) end end ``` ```elixir defmodule AutoFfmpeg do import Ecto.Changeset use Ecto.Schema use Instructor.Validator @system_prompt """ You are a multimedia editor and your job is to receive tasks for multimedia editing and use the programs available to you (and only those) to complete the tasks. You will return arguments to be passed to the program assuming that the input file(s) has already been passed. You do not need to call the binary itself, you are only in charge of generating all subsequent arugments after inputs have been passed. Assume the output file path will be appended after the arguments you provide. You have access to the following programs: ffmpeg and ffprobe So assume the command already is composed of something like `ffmpeg -i input_file_path [..., args, ...] output_file_path` and you then pass arugments to complete the given task. You will also be provided the input file for context, but you should not include inputs in your arguments. Use the given file extension to determine how to form your arugments. You will also provide the output file extension / file type, since depending on the task it could differ from the input type. If the given task does not result in an operation that writes to a file, (eg. asking for timestamps where it is silent would result in writing to stdout), the extension would be `null`. If the command is such that it will output to stdout, you should output as JSON when possible. """ @doc """ ## Field Descriptions: - program: the executable program to call - arguments: execve-formatted arguments for the command - output_ext: The extension (filetype) of the outputted file """ @primary_key false embedded_schema do field(:program, Ecto.Enum, values: [:ffmpeg, :ffprobe]) field(:arguments, {:array, :string}) field(:output_ext, Ecto.Enum, values: [ :mp4, :ogg, :avi, :wmv, :mov, :wav, :mp3, :mpeg, :jpeg, :jpg, :png, :gif, :svg, :pixel, :null ] ) field(:output_path, :string, virtual: true) end @impl true def validate_changeset( changeset, context ) do changeset |> validate_required([:program, :arguments, :output_ext]) |> validate_exclusion(:arguments, ~w(-i)) |> validate_command(context) end defp validate_command(%Ecto.Changeset{valid?: false} = changeset, _context), do: changeset defp validate_command( changeset, %{ upload_path: upload_path, debug: debug, debug_frame: debug_frame, output_frame: output_frame, prompt: prompt, explain: explain, retries: retries, model: model, config: config } ) do program = get_field(changeset, :program) program_args = get_field(changeset, :arguments) input_args = ["-i", upload_path] output_ext = get_field(changeset, :output_ext) # Only perform command if arguments pass validation # Otherwise skip running command and return arg errors output_args = cond do program == :ffprobe -> [] output_ext == :null -> ["-f", "null", "-"] true -> [Upload.generate_temp_filename(Atom.to_string(output_ext))] end command = Enum.join([Atom.to_string(program) | input_args ++ program_args ++ output_args], " ") if debug do message = """ <strong>Command:</strong> <code>#{command}</code> """ Kino.Frame.append( debug_frame, Boilerplate.make_log( message, :info ) ) end case :exec.run(command, [ :sync, :stdout, :stderr ]) do {:ok, result} when is_list(result) -> outputs = [:stdout, :stderr] |> Enum.map(fn device -> if Keyword.has_key?(result, device) do output = Enum.join(Keyword.fetch!(result, device), "") Kino.Frame.append(output_frame, Boilerplate.make_stdout(output, device)) {device, output} else {device, nil} end end) if explain do case Alfred.execute(model, config, prompt, command, retries, outputs) do {:ok, %Alfred{explanation: explanation, confidence: confidence}} -> Kino.Frame.append( output_frame, Boilerplate.make_stdout( "<strong>Explanation:</strong> #{explanation}\n\n<strong>Confidence:</strong> #{confidence}", :alfred, "green" ) ) {:error, %Ecto.Changeset{ errors: [ explanation: {error, _extras} ], valid?: false }} -> Kino.Frame.append( debug_frame, Boilerplate.make_log("Trouble providing explanation: #{inspect(error)}", :error) ) end end if program == :ffmpeg && output_ext != :null do [output_path] = output_args put_change(changeset, :output_path, output_path) else changeset end {:error, result} when is_list(result) -> debug && Kino.Frame.append( debug_frame, Boilerplate.make_log("Something Went Wrong! Retrying...", :error) ) error = cond do Keyword.has_key?(result, :stderr) -> Keyword.fetch!(result, :stderr) |> Enum.join("") Keyword.has_key?(result, :stdout) -> Keyword.fetch!(result, :stdout) |> Enum.join("") Keyword.has_key?(result, :exit_signal) -> "Error resulted in exit code #{Keyword.fetch!(result, :exit_signal)}" true -> "Unexpected error occurred!" end add_error( changeset, :arguments, error, status: Keyword.get(result, :exit_status) ) end end def execute(model, config, prompt, %{upload_path: upload_path} = context, retries) do Instructor.chat_completion( [ mode: :json, model: model, validation_context: Map.merge(context, %{ prompt: prompt, retries: retries, model: model, config: config }), response_model: __MODULE__, max_retries: retries, messages: [ %{ role: "system", content: @system_prompt }, %{ role: "user", content: """ Here's the editing task: #{inspect(prompt)} """ }, %{ role: "user", content: """ Here's input file type: #{inspect(Upload.ext_type(upload_path))} """ } ] ], config ) end end ``` ## Listeners ```elixir import Upload [ upload: upload, submit: submit_button, reset: reset_button, undo: undo_button, prompt: prompt, debug: debug_checkbox, retries: retries, explain: explain_checkbox, provider: provider, oai_select: oai_select, ollama_select: ollama_select, gemini_select: gemini_select ] |> Kino.Control.tagged_stream() |> Kino.listen(fn {model_select, %{type: :change, value: value}} when model_select in [:oai_select, :ollama_select, :gemini_select] -> FormState.update(:model, value) {:provider, %{type: :change, value: value}} -> Kino.Frame.clear(logs) current_provider = FormState.get(:provider) unless value == current_provider do case value do :ollama -> if Models.ollama_models() == [] do Kino.Frame.append( logs, Boilerplate.make_log( "You must have local models to use Ollama. Pull a model then rerun this notebook.", :error ) ) end Kino.Frame.render(models_frame, ollama_select) FormState.update(:provider, value) FormState.update(:model, Models.ollama_models() |> hd() |> elem(0)) :openai -> Kino.Frame.render(models_frame, oai_select) FormState.update(:provider, value) FormState.update(:model, Models.oai_models() |> hd() |> elem(0)) :gemini -> Kino.Frame.render(models_frame, gemini_select) FormState.update(:provider, value) FormState.update(:model, Models.gemini_models() |> hd() |> elem(0)) _ -> Kino.Frame.append( logs, Boilerplate.make_log( "Bad provider selected. Only OpenAI and Ollama are currently supported.", :error ) ) end end {:explain, %{type: :change, value: value}} -> FormState.update(:explain_outputs, value) {:retries, %{type: :change, value: value}} -> FormState.update(:retries, value) {:debug, %{type: :change, value: value}} -> FormState.update(:debug, value) {:prompt, %{type: :change, value: prompt}} -> FormState.update(:prompt, prompt) {:upload, %{ type: :change, value: %{ file_ref: file_ref, client_name: filename } }} -> Kino.Frame.clear(logs) Kino.Frame.clear(output) ext_type = Upload.ext_type(filename) unless is_valid_upload(ext_type) do Kino.Frame.render( logs, Boilerplate.make_log( "File must be of one of the following types: #{inspect(Upload.accepted_types())}", :error ) ) else file_path = file_ref |> Kino.Input.file_path() tmp_path = Upload.generate_temp_filename(ext_type) _bytes_copied = File.copy!(file_path, tmp_path) upload = Upload.new(filename, tmp_path) Upload.to_kino(upload) |> then(&Kino.Frame.render(original, &1)) EditHistory.push(upload) Kino.Frame.render(debug_frame, debug_checkbox) Kino.Frame.render(explain_frame, explain_checkbox) Kino.Frame.render(submit_frame, submit_button) Kino.Frame.clear(undo_frame) Kino.Frame.clear(reset_frame) end {:submit, %{type: :click}} -> Kino.Frame.clear(logs) Kino.Frame.clear(output) prompt = FormState.get(:prompt) |> String.trim() if prompt == "" do Kino.Frame.append(logs, Boilerplate.make_log("Prompt cannot be empty!", :error)) else Kino.Frame.render( original, Kino.HTML.new( EEx.eval_string(Boilerplate.placeholder(), message: "Working...", show_spinner: true) ) ) {%Upload{} = current_upload, _old_prompt} = EditHistory.current() num_retries = FormState.get(:retries) model = FormState.get(:model) debug = FormState.get(:debug) if debug do message = """ <strong>Provider:</strong> <em>#{FormState.get(:provider)}</em><br><br> <strong>Adapter:</strong> <em>#{FormState.get(:config) |> Keyword.fetch!(:adapter)}</em><br><br> <strong>Model:</strong> <code>#{model}</code><br><br> <strong>Prompt:</strong> <em>#{prompt}</em> """ Kino.Frame.append( logs, Boilerplate.make_log( message, :info ) ) end case AutoFfmpeg.execute( model, FormState.get(:config), prompt, %{ upload_path: current_upload.path, debug: debug, debug_frame: logs, output_frame: output, explain: FormState.get(:explain_outputs) }, num_retries ) do {:ok, %AutoFfmpeg{output_path: output_path}} -> FormState.get(:debug) && Kino.Frame.append(logs, Boilerplate.make_log("Success!", :success)) unless is_nil(output_path) do new_upload = Upload.new(current_upload.filename, output_path) EditHistory.push(new_upload, prompt) Upload.to_kino(new_upload) |> then(&Kino.Frame.render(original, &1)) else Upload.to_kino(current_upload) |> then(&Kino.Frame.render(original, &1)) end Kino.Frame.render(undo_frame, undo_button) Kino.Frame.render(reset_frame, reset_button) {:error, %Ecto.Changeset{ errors: errors, valid?: false }} -> Upload.to_kino(current_upload) |> then(&Kino.Frame.render(original, &1)) Kino.Frame.append( logs, Boilerplate.make_log("Failed after #{num_retries} attempts!", :error) ) Enum.each(errors, fn {field, error} -> Kino.Frame.append( logs, Boilerplate.make_log("Error on field #{inspect(field)}: #{inspect(error)}", :error) ) end) {:error, <<"LLM Adapter Error: ", error::binary>>} -> Upload.to_kino(current_upload) |> then(&Kino.Frame.render(original, &1)) {error, _binding} = error |> Code.eval_string() Kino.Frame.append( logs, Boilerplate.make_log("Error! Reference the error below for details", :error) ) Kino.Frame.append(logs, Kino.Tree.new(error)) {:error, <<"Invalid JSON returned from LLM: ", error::binary>>} -> Upload.to_kino(current_upload) |> then(&Kino.Frame.render(original, &1)) Kino.Frame.append(logs, Boilerplate.make_log(error, :error)) end end {:reset, %{type: :click}} -> {%Upload{} = original_upload, nil} = EditHistory.reset() Upload.to_kino(original_upload) |> then(&Kino.Frame.render(original, &1)) Kino.Frame.clear(logs) Kino.Frame.clear(output) Kino.Frame.clear(reset_frame) Kino.Frame.clear(undo_frame) {:undo, %{type: :click}} -> Kino.Frame.clear(logs) Kino.Frame.clear(output) case EditHistory.undo_edit() do nil -> Kino.Frame.append(logs, Kino.Text.new("Error! Cannot `Undo`. No previous edit.")) {%Upload{} = previous_upload, _previous_prompt} -> Upload.to_kino(previous_upload) |> then(&Kino.Frame.render(original, &1)) Kino.Frame.clear(logs) if EditHistory.previous_edit() == nil do Kino.Frame.clear(reset_frame) Kino.Frame.clear(undo_frame) end end end) ``` <!-- livebook:{"offset":31158,"stamp":{"token":"XCP.-dXK9M1IdoAJTNHSUcZ6Rv0sMA89UvvID_8Z4tgPbMAUByOl80mlnN1cszFNnwvQdvwQsDc4afMz7wnRulcIz8hJLdoQr3YwzAN1cXOhoxiL3zSXWFOpAKprIXhpfc99wDUbW7RzF2LEAuy8cCGV9iU","version":2}} -->
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×