defmodule LivebookWeb.SessionLive.CellComponent do use LivebookWeb, :live_component import LivebookWeb.NotebookComponents @impl true def mount(socket) do {:ok, stream(socket, :outputs, [])} end @impl true def update(assigns, socket) do socket = assign(socket, assigns) socket = case assigns.cell_view do %{eval: %{outputs: outputs}} -> stream_items = for {idx, output} <- Enum.reverse(outputs) do %{id: Integer.to_string(idx), idx: idx, output: output} end stream(socket, :outputs, stream_items) %{} -> socket end {:ok, socket} end @impl true def render(assigns) do ~H"""
{render_cell(assigns)}
""" end defp render_cell(%{cell_view: %{type: :markdown}} = assigns) do ~H""" <.cell_actions> <:secondary> <.enable_insert_mode_button /> <.cell_link_button cell_id={@cell_view.id} /> <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> <.cell_body>
<.cell_editor cell_id={@cell_view.id} tag="primary" empty={@cell_view.empty} language="markdown" />
<.content_skeleton empty={@cell_view.empty} />
""" end defp render_cell(%{cell_view: %{type: :code, setup: true, language: :elixir}} = assigns) do ~H""" <.cell_actions> <:primary> <.setup_cell_evaluation_button cell_id={@cell_view.id} validity={@cell_view.eval.validity} status={@cell_view.eval.status} runtime={@runtime} /> <:secondary> <.package_search_button session_id={@session_id} runtime={@runtime} /> <.cell_link_button cell_id={@cell_view.id} /> <.setup_cell_info /> <.cell_body>
Notebook dependencies and setup
<.cell_editor cell_id={@cell_view.id} tag="primary" empty={@cell_view.empty} language="elixir" intellisense />
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
<.evaluation_outputs outputs={@streams.outputs} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} client_id={@client_id} /> """ end defp render_cell( %{cell_view: %{type: :code, setup: true, language: :"pyproject.toml"}} = assigns ) do ~H""" <.cell_actions> <:primary>
<.language_icon language="python" class="w-4 h-4" /> Python (pyproject.toml)
<:secondary> <.cell_link_button cell_id={@cell_view.id} /> <.disable_language_button language={:python} /> <.pyproject_toml_cell_info /> <.cell_body>
<.cell_editor cell_id={@cell_view.id} tag="primary" empty={@cell_view.empty} language="pyproject.toml" />
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
<.evaluation_outputs outputs={@streams.outputs} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} client_id={@client_id} /> """ end defp render_cell(%{cell_view: %{type: :code}} = assigns) do ~H""" <.cell_actions> <:primary> <.cell_evaluation_button session_id={@session_id} cell_id={@cell_view.id} validity={@cell_view.eval.validity} status={@cell_view.eval.status} reevaluate_automatically={@cell_view.reevaluate_automatically} reevaluates_automatically={@cell_view.eval.reevaluates_automatically} /> <:secondary> <.cell_settings_button cell_id={@cell_view.id} session_id={@session_id} /> <.amplify_output_button /> <.cell_link_button cell_id={@cell_view.id} /> <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> <.cell_body>
<.cell_editor cell_id={@cell_view.id} tag="primary" empty={@cell_view.empty} language={@cell_view.language} intellisense={@cell_view.language == :elixir} />
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} langauge_toggle />
<.message_box kind="error">
{language_name(@cell_view.language)} is not enabled for the current notebook.
<.doctest_summary cell_id={@cell_view.id} doctest_summary={@cell_view.eval.doctest_summary} /> <.evaluation_outputs outputs={@streams.outputs} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} client_id={@client_id} /> """ end defp render_cell(%{cell_view: %{type: :smart}} = assigns) do ~H""" <.cell_actions> <:primary> <.cell_evaluation_button session_id={@session_id} cell_id={@cell_view.id} validity={@cell_view.eval.validity} status={@cell_view.eval.status} reevaluate_automatically={@cell_view.reevaluate_automatically} reevaluates_automatically={@cell_view.eval.reevaluates_automatically} /> <:secondary> <.toggle_source_button /> <.convert_smart_cell_button cell_id={@cell_view.id} /> <.amplify_output_button /> <.cell_link_button cell_id={@cell_view.id} /> <.move_cell_up_button cell_id={@cell_view.id} /> <.move_cell_down_button cell_id={@cell_view.id} /> <.delete_cell_button cell_id={@cell_view.id} /> <.cell_body>
<%= case @cell_view.status do %> <% :started -> %>
<.live_component module={LivebookWeb.JSViewComponent} id={@cell_view.id} js_view={@cell_view.js_view} session_id={@session_id} client_id={@client_id} /> <.cell_editor :if={@cell_view.editor} cell_id={@cell_view.id} tag="secondary" empty={@cell_view.editor.empty} language={@cell_view.editor.language} rounded={@cell_view.editor.placement} intellisense={@cell_view.editor.language == "elixir"} hidden={not @cell_view.editor.visible} />
<% :dead -> %>
<%= if @installing? do %> Waiting for dependency installation to complete... <% else %> Run the notebook setup to show the contents of this Smart cell. <% end %>
<% :down -> %>
The Smart cell crashed unexpectedly, this is most likely a bug. <.button color="gray" phx-click={JS.push("recover_smart_cell", value: %{cell_id: @cell_view.id})} > Restart Smart cell
<% :starting -> %>
<.content_skeleton empty={false} />
<% end %>
<.cell_editor cell_id={@cell_view.id} tag="primary" empty={@cell_view.empty} language="elixir" intellisense read_only />
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
<.evaluation_outputs outputs={@streams.outputs} cell_view={@cell_view} session_id={@session_id} session_pid={@session_pid} client_id={@client_id} /> """ end defp cell_actions(assigns) do assigns = assigns |> assign_new(:primary, fn -> [] end) |> assign_new(:secondary, fn -> [] end) ~H"""
{render_slot(@primary)}
""" end defp cell_body(assigns) do ~H"""
{render_slot(@inner_block)}
""" end defp cell_evaluation_button(%{status: :ready} = assigns) do ~H"""
<.menu id={"cell-#{@cell_id}-evaluation-menu"} position="bottom-left" distant> <:toggle> <.menu_item variant={if(not @reevaluate_automatically, do: "selected", else: "default")}> <.menu_item variant={if(@reevaluate_automatically, do: "selected", else: "default")}>
""" end defp cell_evaluation_button(assigns) do ~H""" """ end defp setup_cell_evaluation_button(%{status: :ready} = assigns) do ~H"""
<%= unless Livebook.Runtime.fixed_dependencies?(@runtime) do %> <.menu id="setup-menu" position="bottom-left" distant> <:toggle> <.menu_item> <% end %>
""" end defp setup_cell_evaluation_button(assigns) do ~H""" """ end defp enable_insert_mode_button(assigns) do ~H""" <.icon_button aria-label="edit content"> <.remix_icon icon="pencil-line" /> """ end defp toggle_source_button(assigns) do ~H""" <.icon_button aria-label="toggle source"> <.remix_icon icon="code-line" /> """ end defp convert_smart_cell_button(assigns) do ~H""" <.icon_button aria-label="toggle source" data-link-package-search phx-click={JS.push("convert_smart_cell", value: %{cell_id: @cell_id})} > <.remix_icon icon="pencil-line" /> """ end defp package_search_button(assigns) do ~H""" <%= if Livebook.Runtime.fixed_dependencies?(@runtime) do %> <.icon_button disabled> <.remix_icon icon="play-list-add-line" /> <% else %> <.icon_button patch={~p"/sessions/#{@session_id}/package-search"} role="button" data-btn-package-search > <.remix_icon icon="play-list-add-line" /> <% end %> """ end defp cell_link_button(assigns) do ~H""" <.icon_button href={"#cell-#{@cell_id}"} role="button" aria-label="link to cell"> <.remix_icon icon="link" /> """ end def amplify_output_button(assigns) do ~H""" <.icon_button aria-label="amplify outputs"> <.remix_icon icon="zoom-in-line" /> """ end defp cell_settings_button(assigns) do ~H""" <.icon_button patch={~p"/sessions/#{@session_id}/cell-settings/#{@cell_id}"} aria-label="cell settings" role="button" > <.remix_icon icon="settings-3-line" /> """ end defp move_cell_up_button(assigns) do ~H""" <.icon_button aria-label="move cell up" phx-click="move_cell" phx-value-cell_id={@cell_id} phx-value-offset="-1" > <.remix_icon icon="arrow-up-s-line" /> """ end defp move_cell_down_button(assigns) do ~H""" <.icon_button aria-label="move cell down" phx-click="move_cell" phx-value-cell_id={@cell_id} phx-value-offset="1" > <.remix_icon icon="arrow-down-s-line" /> """ end defp delete_cell_button(assigns) do ~H""" <.icon_button aria-label="delete cell" phx-click={JS.push("delete_cell", value: %{cell_id: @cell_id})} > <.remix_icon icon="delete-bin-6-line" /> """ end defp disable_language_button(assigns) do ~H""" <.icon_button aria-label="delete cell" phx-click="disable_language" phx-value-language={@language} > <.remix_icon icon="delete-bin-6-line" /> """ end defp setup_cell_info(assigns) do ~H""" <.icon_button> <.remix_icon icon="question-line" /> """ end defp pyproject_toml_cell_info(assigns) do ~H""" <.icon_button> <.remix_icon icon="question-line" /> """ end attr :cell_id, :string, required: true attr :tag, :string, required: true attr :empty, :boolean, required: true attr :language, :string, required: true attr :intellisense, :boolean, default: false attr :read_only, :boolean, default: false attr :rounded, :atom, default: :both attr :hidden, :boolean, default: false defp cell_editor(assigns) do ~H"""
<.content_skeleton bg_class="bg-gray-500" empty={@empty} />
""" end defp rounded_class(:both), do: "rounded-lg" defp rounded_class(:top), do: "rounded-t-lg" defp rounded_class(:bottom), do: "rounded-b-lg" defp doctest_summary(assigns) do ~H"""
0} class="pt-2" id={"doctest-summary-#{@cell_id}"}>
{doctest_summary_message(@doctest_summary)}
""" end defp doctest_summary_message(%{doctests_count: total, failures_count: failed}) do doctests_pl = LivebookWeb.HTMLHelpers.pluralize(total, "doctest", "doctests") failures_pl = if failed == 1, do: "failure has", else: "failures have" "#{failed} out of #{doctests_pl} failed (#{failures_pl} been reported above)" end defp evaluation_outputs(assigns) do ~H"""
""" end attr :id, :string, required: true attr :cell_view, :map, required: true attr :langauge_toggle, :boolean, default: false defp cell_indicators(assigns) do ~H"""
<.cell_indicator :if={has_status?(@cell_view)}> <.cell_status id={@id} cell_view={@cell_view} /> <%= if @langauge_toggle do %> <.menu id={"cell-#{@id}-language-menu"} position="bottom-right"> <:toggle> <.cell_indicator class="cursor-pointer"> <.language_icon language={cell_language(@cell_view)} class="w-3 h-3" /> <.menu_item :for={language <- Livebook.Notebook.Cell.Code.languages()}> <% else %> <.cell_indicator> <.language_icon language={cell_language(@cell_view)} class="w-3 h-3" /> <% end %>
""" end attr :class, :string, default: nil slot :inner_block, required: true defp cell_indicator(assigns) do ~H"""
{render_slot(@inner_block)}
""" end defp cell_language(%{language: language}), do: Atom.to_string(language) defp cell_language(%{type: :smart}), do: "elixir" defp has_status?(%{eval: %{status: :ready, validity: :fresh}}), do: false defp has_status?(_cell_view), do: true defp cell_status(%{cell_view: %{eval: %{status: :evaluating}}} = assigns) do ~H""" <.cell_status_indicator variant="progressing" change_indicator={true}> """ end defp cell_status(%{cell_view: %{eval: %{status: :queued}}} = assigns) do ~H""" <.cell_status_indicator variant="waiting"> Queued """ end defp cell_status(%{cell_view: %{eval: %{validity: :evaluated}}} = assigns) do ~H""" <.cell_status_indicator variant={if(@cell_view.eval.errored, do: "error", else: "success")} change_indicator={true} tooltip={duration_label(@cell_view.eval.evaluation_time_ms)} > Evaluated """ end defp cell_status(%{cell_view: %{eval: %{validity: :stale}}} = assigns) do ~H""" <.cell_status_indicator variant="warning" change_indicator={true} tooltip={duration_label(@cell_view.eval.evaluation_time_ms)} > Stale """ end defp cell_status(%{cell_view: %{eval: %{validity: :aborted}}} = assigns) do ~H""" <.cell_status_indicator variant="inactive" tooltip={duration_label(@cell_view.eval.evaluation_time_ms)} > Aborted """ end defp cell_status(assigns), do: ~H"" attr :variant, :string, required: true attr :tooltip, :string, default: nil attr :change_indicator, :boolean, default: false slot :inner_block, required: true defp cell_status_indicator(assigns) do ~H"""
{render_slot(@inner_block)} *
<.status_indicator variant={@variant} />
""" end defp duration_label(time_ms) when is_integer(time_ms) do evaluation_time = if time_ms > 100 do seconds = time_ms |> Kernel./(1000) |> Float.floor(1) "#{seconds}s" else "#{time_ms}ms" end "Took " <> evaluation_time end defp duration_label(_time_ms), do: nil defp smart_cell_js_view_ref(%{type: :smart, status: :started, js_view: %{ref: ref}}), do: ref defp smart_cell_js_view_ref(_cell_view), do: nil defp language_name(language) do Enum.find_value( Livebook.Notebook.Cell.Code.languages(), &(&1.language == language && &1.name) ) end end