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.
Enable Python
<.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)}
{render_slot(@secondary)}
"""
end
defp cell_body(assigns) do
~H"""
{render_slot(@inner_block)}
"""
end
defp cell_evaluation_button(%{status: :ready} = assigns) do
~H"""
<%= cond do %>
<% @reevaluates_automatically -> %>
<.remix_icon icon="check-line" class="text-xl" />
Reevaluates automatically
<% @validity == :evaluated -> %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
Reevaluate
<% true -> %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
Evaluate
<% end %>
<.menu id={"cell-#{@cell_id}-evaluation-menu"} position="bottom-left" distant>
<:toggle>
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
<.menu_item variant={if(not @reevaluate_automatically, do: "selected", else: "default")}>
<.remix_icon icon="check-line" class={if(@reevaluate_automatically, do: "invisible")} />
Evaluate on demand
<.menu_item variant={if(@reevaluate_automatically, do: "selected", else: "default")}>
<.remix_icon icon="check-line" class={if(not @reevaluate_automatically, do: "invisible")} />
Reevaluate automatically
"""
end
defp cell_evaluation_button(assigns) do
~H"""
<.remix_icon icon="stop-circle-fill" class="text-xl" />
Stop
"""
end
defp setup_cell_evaluation_button(%{status: :ready} = assigns) do
~H"""
<%= if @validity == :fresh do %>
<.remix_icon icon="play-circle-fill" class="text-xl" />
Setup
<% else %>
<.remix_icon icon="restart-fill" class="text-xl" />
Reconnect and setup
<% end %>
<%= unless Livebook.Runtime.fixed_dependencies?(@runtime) do %>
<.menu id="setup-menu" position="bottom-left" distant>
<:toggle>
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
<.menu_item>
<.remix_icon icon="play-circle-fill" />
Setup without cache
<% end %>
"""
end
defp setup_cell_evaluation_button(assigns) do
~H"""
<.remix_icon icon="stop-circle-fill" class="text-xl" />
Stop
"""
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()}>
<.cell_icon cell_type={:code} language={language.language} />
{language.name}
<% 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"""
"""
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