defmodule LivebookWeb.SessionLive.CellComponent do use LivebookWeb, :live_component def render(assigns) do ~L"""
<%= render_cell_content(assigns) %>
""" end def render_cell_content(%{cell_view: %{type: :markdown}} = assigns) do ~L"""
<%= render_cell_anchor_link(assigns) %> <%= live_patch to: Routes.session_path(@socket, :cell_upload, @session_id, @cell_view.id), class: "icon-button" do %> <%= remix_icon("image-add-line", class: "text-xl") %> <% end %>
<%= render_editor(assigns) %>
<%= render_content_placeholder("bg-gray-200", @cell_view.empty?) %>
""" end def render_cell_content(%{cell_view: %{type: :elixir}} = assigns) do ~L"""
<%= if @cell_view.evaluation_status == :ready do %> <% else %> <% end %>
<%= render_cell_anchor_link(assigns) %> <%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %> <%= remix_icon("list-settings-line", class: "text-xl") %> <% end %>
<%= render_editor(assigns) %> <%= if @cell_view.outputs != [] do %>
<%= render_outputs(assigns, @socket) %>
<% end %>
""" end def render_cell_content(%{cell_view: %{type: :input}} = assigns) do ~L"""
<%= render_cell_anchor_link(assigns) %> <%= live_patch to: Routes.session_path(@socket, :cell_settings, @session_id, @cell_view.id), class: "icon-button" do %> <%= remix_icon("list-settings-line", class: "text-xl") %> <% end %>
<%= @cell_view.name %>
" data-element="input" class="input <%= if(@cell_view.error, do: "input--error") %>" name="value" value="<%= @cell_view.value %>" phx-debounce="300" spellcheck="false" autocomplete="off" tabindex="-1" /> <%= if @cell_view.error do %>
<%= String.capitalize(@cell_view.error) %>
<% end %>
""" end defp render_editor(assigns) do ~L"""
<%= render_content_placeholder("bg-gray-500", @cell_view.empty?) %>
<%= if @cell_view.type == :elixir do %>
<%= render_cell_status( @cell_view.validity_status, @cell_view.evaluation_status, @cell_view.evaluation_time_ms, "cell-#{@cell_view.id}-evaluation#{@cell_view.number_of_evaluations}" ) %>
<% end %>
""" end defp render_cell_anchor_link(assigns) do ~L""" <%= remix_icon("link", class: "text-xl") %> """ end # The whole page has to load and then hooks are mounded. # There may be a tiny delay before the markdown is rendered # or editors are mounted, so show neat placeholders immediately. defp render_content_placeholder(_bg_class, true = _empty) do assigns = %{} ~L"""
""" end defp render_content_placeholder(bg_class, false = _empty) do assigns = %{bg_class: bg_class} ~L"""
""" end defp render_outputs(assigns, socket) do ~L"""
<%= for {output, index} <- @cell_view.outputs |> Enum.reverse() |> Enum.with_index(), output != :ignored do %>
<%= render_output(socket, output, "cell-#{@cell_view.id}-output#{index}") %>
<% end %>
""" end defp render_output(_socket, text, id) when is_binary(text) do # Captured output usually has a trailing newline that we can ignore, # because each line is itself an HTML block anyway. text = String.replace_suffix(text, "\n", "") live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: true) end defp render_output(_socket, {:text, text}, id) do live_component(LivebookWeb.Output.TextComponent, id: id, content: text, follow: false) end defp render_output(_socket, {:image, content, mime_type}, id) do live_component(LivebookWeb.Output.ImageComponent, id: id, content: content, mime_type: mime_type ) end defp render_output(_socket, {:vega_lite_static, spec}, id) do live_component(LivebookWeb.Output.VegaLiteStaticComponent, id: id, spec: spec) end defp render_output(socket, {:vega_lite_dynamic, pid}, id) do live_render(socket, LivebookWeb.Output.VegaLiteDynamicLive, id: id, session: %{"id" => id, "pid" => pid} ) end defp render_output(socket, {:table_dynamic, pid}, id) do live_render(socket, LivebookWeb.Output.TableDynamicLive, id: id, session: %{"id" => id, "pid" => pid} ) end defp render_output(_socket, {:error, formatted}, _id) do render_error_message_output(formatted) end defp render_output(_socket, output, _id) do render_error_message_output(""" Unknown output format: #{inspect(output)}. If you're using Kino, you may want to update Kino and Livebook to the latest version. """) end defp render_error_message_output(message) do assigns = %{message: message} ~L"""
<%= @message %>
""" end defp render_cell_status(cell_view, evaluation_status, evaluation_time_ms, evaluation_id) defp render_cell_status(_, :evaluating, _, evaluation_id) do timer = content_tag(:span, nil, phx_hook: "Timer", # Make sure each evaluation gets its own timer id: "#{evaluation_id}-timer", phx_update: "ignore", class: "font-mono" ) render_status_indicator(timer, "bg-blue-500", animated_circle_class: "bg-blue-400", change_indicator: true ) end defp render_cell_status(_, :queued, _, _) do render_status_indicator("Queued", "bg-gray-500", animated_circle_class: "bg-gray-400") end defp render_cell_status(:evaluated, _, evaluation_time_ms, _) do render_status_indicator("Evaluated", "bg-green-400", change_indicator: true, tooltip: evaluated_label(evaluation_time_ms) ) end defp render_cell_status(:stale, _, evaluation_time_ms, _) do render_status_indicator("Stale", "bg-yellow-200", change_indicator: true, tooltip: evaluated_label(evaluation_time_ms) ) end defp render_cell_status(:aborted, _, _, _) do render_status_indicator("Aborted", "bg-red-400") end defp render_cell_status(_, _, _, _), do: nil defp render_status_indicator(element, circle_class, opts \\ []) do assigns = %{ element: element, circle_class: circle_class, animated_circle_class: Keyword.get(opts, :animated_circle_class), change_indicator: Keyword.get(opts, :change_indicator, false), tooltip: Keyword.get(opts, :tooltip) } ~L"""
bottom distant-medium" aria-label="<%= @tooltip %>">
<%= @element %> <%= if @change_indicator do %> * <% end %>
<%= if @animated_circle_class do %> <% end %>
""" end defp evaluated_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 evaluated_label(_time_ms), do: nil end