mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-09-07 05:24:40 +08:00
937 lines
28 KiB
Elixir
937 lines
28 KiB
Elixir
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"""
|
|
<div
|
|
class="flex flex-col relative scroll-mt-[50px] sm:scroll-mt-0"
|
|
data-el-cell
|
|
id={"cell-#{@cell_view.id}"}
|
|
data-type={@cell_view.type}
|
|
data-setup={@cell_view[:setup]}
|
|
data-focusable-id={@cell_view.id}
|
|
data-js-empty={@cell_view.empty}
|
|
data-eval-validity={get_in(@cell_view, [:eval, :validity])}
|
|
data-eval-errored={get_in(@cell_view, [:eval, :errored])}
|
|
phx-hook="Cell"
|
|
data-p-cell-id={hook_prop(@cell_view.id)}
|
|
data-p-type={hook_prop(@cell_view.type)}
|
|
data-p-session-path={hook_prop(~p"/sessions/#{@session_id}")}
|
|
data-p-evaluation-digest={hook_prop(get_in(@cell_view, [:eval, :evaluation_digest]))}
|
|
data-p-smart-cell-js-view-ref={hook_prop(smart_cell_js_view_ref(@cell_view))}
|
|
data-p-allowed-uri-schemes={hook_prop(@allowed_uri_schemes)}
|
|
>
|
|
{render_cell(assigns)}
|
|
</div>
|
|
"""
|
|
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} />
|
|
</:secondary>
|
|
</.cell_actions>
|
|
<.cell_body>
|
|
<div class="pb-4" data-el-editor-box>
|
|
<.cell_editor
|
|
cell_id={@cell_view.id}
|
|
tag="primary"
|
|
empty={@cell_view.empty}
|
|
language="markdown"
|
|
/>
|
|
</div>
|
|
<div
|
|
class="markdown break-words"
|
|
data-el-markdown-container
|
|
id={"markdown-container-#{@cell_view.id}"}
|
|
phx-update="ignore"
|
|
>
|
|
<.content_skeleton empty={@cell_view.empty} />
|
|
</div>
|
|
</.cell_body>
|
|
"""
|
|
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}
|
|
/>
|
|
</:primary>
|
|
<:secondary>
|
|
<.package_search_button session_id={@session_id} runtime={@runtime} />
|
|
<.cell_link_button cell_id={@cell_view.id} />
|
|
<.setup_cell_info />
|
|
</:secondary>
|
|
</.cell_actions>
|
|
<.cell_body>
|
|
<div class="relative" data-el-cell-body-root>
|
|
<div data-el-info-box>
|
|
<div class="py-2 px-3 flex items-center justify-between border border-gray-200 text-sm text-gray-400 rounded-lg">
|
|
<span class="font-medium">Notebook dependencies and setup</span>
|
|
</div>
|
|
</div>
|
|
<div data-el-editor-box>
|
|
<.cell_editor
|
|
cell_id={@cell_view.id}
|
|
tag="primary"
|
|
empty={@cell_view.empty}
|
|
language="elixir"
|
|
intellisense
|
|
/>
|
|
</div>
|
|
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
|
|
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
|
|
</div>
|
|
</div>
|
|
<.evaluation_outputs
|
|
outputs={@streams.outputs}
|
|
cell_view={@cell_view}
|
|
session_id={@session_id}
|
|
session_pid={@session_pid}
|
|
client_id={@client_id}
|
|
/>
|
|
</.cell_body>
|
|
"""
|
|
end
|
|
|
|
defp render_cell(
|
|
%{cell_view: %{type: :code, setup: true, language: :"pyproject.toml"}} = assigns
|
|
) do
|
|
~H"""
|
|
<.cell_actions>
|
|
<:primary>
|
|
<div class="flex gap-1 items-center text-gray-500 text-sm">
|
|
<.language_icon language="python" class="w-4 h-4" />
|
|
<span>Python (pyproject.toml)</span>
|
|
</div>
|
|
</:primary>
|
|
<:secondary>
|
|
<.cell_link_button cell_id={@cell_view.id} />
|
|
<.disable_language_button language={:python} />
|
|
<.pyproject_toml_cell_info />
|
|
</:secondary>
|
|
</.cell_actions>
|
|
<.cell_body>
|
|
<div class="relative" data-el-cell-body-root>
|
|
<div data-el-editor-box>
|
|
<.cell_editor
|
|
cell_id={@cell_view.id}
|
|
tag="primary"
|
|
empty={@cell_view.empty}
|
|
language="pyproject.toml"
|
|
/>
|
|
</div>
|
|
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
|
|
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
|
|
</div>
|
|
</div>
|
|
<.evaluation_outputs
|
|
outputs={@streams.outputs}
|
|
cell_view={@cell_view}
|
|
session_id={@session_id}
|
|
session_pid={@session_pid}
|
|
client_id={@client_id}
|
|
/>
|
|
</.cell_body>
|
|
"""
|
|
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}
|
|
/>
|
|
</:primary>
|
|
<: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} />
|
|
</:secondary>
|
|
</.cell_actions>
|
|
<.cell_body>
|
|
<div class="relative" data-el-cell-body-root>
|
|
<div class="relative" data-el-editor-box>
|
|
<.cell_editor
|
|
cell_id={@cell_view.id}
|
|
tag="primary"
|
|
empty={@cell_view.empty}
|
|
language={@cell_view.language}
|
|
intellisense={@cell_view.language == :elixir}
|
|
/>
|
|
</div>
|
|
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
|
|
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} langauge_toggle />
|
|
</div>
|
|
</div>
|
|
<div :if={@cell_view.language not in @enabled_languages} class="mt-2">
|
|
<.message_box kind="error">
|
|
<div class="flex items-center justify-between">
|
|
{language_name(@cell_view.language)} is not enabled for the current notebook.
|
|
<button
|
|
class="flex gap-1 items-center font-medium text-blue-600"
|
|
phx-click="enable_language"
|
|
phx-value-language="python"
|
|
>
|
|
Enable Python
|
|
</button>
|
|
</div>
|
|
</.message_box>
|
|
</div>
|
|
<.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}
|
|
/>
|
|
</.cell_body>
|
|
"""
|
|
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}
|
|
/>
|
|
</:primary>
|
|
<: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} />
|
|
</:secondary>
|
|
</.cell_actions>
|
|
<.cell_body>
|
|
<div class="relative" data-el-cell-body-root>
|
|
<div data-el-ui-box>
|
|
<%= case @cell_view.status do %>
|
|
<% :started -> %>
|
|
<div class={
|
|
"flex #{if(@cell_view.editor && @cell_view.editor.placement == :top, do: "flex-col-reverse", else: "flex-col")}"
|
|
}>
|
|
<.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}
|
|
/>
|
|
</div>
|
|
<% :dead -> %>
|
|
<div class="info-box">
|
|
<%= if @installing? do %>
|
|
Waiting for dependency installation to complete...
|
|
<% else %>
|
|
Run the notebook setup to show the contents of this Smart cell.
|
|
<% end %>
|
|
</div>
|
|
<% :down -> %>
|
|
<div class="info-box flex justify-between items-center">
|
|
<span>
|
|
The Smart cell crashed unexpectedly, this is most likely a bug.
|
|
</span>
|
|
<.button
|
|
color="gray"
|
|
phx-click={JS.push("recover_smart_cell", value: %{cell_id: @cell_view.id})}
|
|
>
|
|
Restart Smart cell
|
|
</.button>
|
|
</div>
|
|
<% :starting -> %>
|
|
<div class="delay-200">
|
|
<.content_skeleton empty={false} />
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<div data-el-editor-box>
|
|
<div class="relative">
|
|
<.cell_editor
|
|
cell_id={@cell_view.id}
|
|
tag="primary"
|
|
empty={@cell_view.empty}
|
|
language="elixir"
|
|
intellisense
|
|
read_only
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="absolute bottom-2 right-2" data-el-cell-indicators>
|
|
<.cell_indicators id={@cell_view.id} cell_view={@cell_view} />
|
|
</div>
|
|
</div>
|
|
<.evaluation_outputs
|
|
outputs={@streams.outputs}
|
|
cell_view={@cell_view}
|
|
session_id={@session_id}
|
|
session_pid={@session_pid}
|
|
client_id={@client_id}
|
|
/>
|
|
</.cell_body>
|
|
"""
|
|
end
|
|
|
|
defp cell_actions(assigns) do
|
|
assigns =
|
|
assigns
|
|
|> assign_new(:primary, fn -> [] end)
|
|
|> assign_new(:secondary, fn -> [] end)
|
|
|
|
~H"""
|
|
<div class="mb-1 flex items-center justify-between">
|
|
<div class="relative z-20 flex items-center justify-end space-x-2" data-el-actions data-primary>
|
|
{render_slot(@primary)}
|
|
</div>
|
|
<div
|
|
class="relative z-20 flex items-center justify-end md:space-x-2"
|
|
role="toolbar"
|
|
aria-label="cell actions"
|
|
data-el-actions
|
|
data-secondary
|
|
>
|
|
{render_slot(@secondary)}
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp cell_body(assigns) do
|
|
~H"""
|
|
<!-- By setting tabindex we can programmatically focus this element,
|
|
also we actually want to make this element tab-focusable -->
|
|
<div class="flex relative focus-visible:outline-none" data-el-cell-body tabindex="0">
|
|
<div class="w-1 h-full rounded-lg absolute top-0 -left-3" data-el-cell-focus-indicator></div>
|
|
<div class="w-full">
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp cell_evaluation_button(%{status: :ready} = assigns) do
|
|
~H"""
|
|
<div class="flex items-center space-x-1">
|
|
<button
|
|
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
|
|
data-el-queue-cell-evaluation-button
|
|
data-cell-id={@cell_id}
|
|
>
|
|
<%= cond do %>
|
|
<% @reevaluates_automatically -> %>
|
|
<.remix_icon icon="check-line" class="text-xl" />
|
|
<span class="text-sm font-medium">Reevaluates automatically</span>
|
|
<% @validity == :evaluated -> %>
|
|
<.remix_icon icon="play-circle-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">Reevaluate</span>
|
|
<% true -> %>
|
|
<.remix_icon icon="play-circle-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">Evaluate</span>
|
|
<% end %>
|
|
</button>
|
|
<.menu id={"cell-#{@cell_id}-evaluation-menu"} position="bottom-left" distant>
|
|
<:toggle>
|
|
<button class="flex text-gray-600 hover:text-gray-800">
|
|
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
|
|
</button>
|
|
</:toggle>
|
|
<.menu_item variant={if(not @reevaluate_automatically, do: "selected", else: "default")}>
|
|
<button
|
|
role="menuitem"
|
|
phx-click={
|
|
JS.push("set_reevaluate_automatically", value: %{value: false, cell_id: @cell_id})
|
|
}
|
|
>
|
|
<.remix_icon icon="check-line" class={if(@reevaluate_automatically, do: "invisible")} />
|
|
<span>Evaluate on demand</span>
|
|
</button>
|
|
</.menu_item>
|
|
<.menu_item variant={if(@reevaluate_automatically, do: "selected", else: "default")}>
|
|
<button
|
|
role="menuitem"
|
|
phx-click={
|
|
JS.push("set_reevaluate_automatically", value: %{value: true, cell_id: @cell_id})
|
|
}
|
|
>
|
|
<.remix_icon icon="check-line" class={if(not @reevaluate_automatically, do: "invisible")} />
|
|
<span>Reevaluate automatically</span>
|
|
</button>
|
|
</.menu_item>
|
|
</.menu>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp cell_evaluation_button(assigns) do
|
|
~H"""
|
|
<button
|
|
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
|
|
phx-click="cancel_cell_evaluation"
|
|
phx-value-cell_id={@cell_id}
|
|
>
|
|
<.remix_icon icon="stop-circle-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">
|
|
Stop
|
|
</span>
|
|
</button>
|
|
"""
|
|
end
|
|
|
|
defp setup_cell_evaluation_button(%{status: :ready} = assigns) do
|
|
~H"""
|
|
<div class="flex items-center space-x-1">
|
|
<button
|
|
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
|
|
data-el-queue-cell-evaluation-button
|
|
data-cell-id={@cell_id}
|
|
>
|
|
<%= if @validity == :fresh do %>
|
|
<.remix_icon icon="play-circle-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">Setup</span>
|
|
<% else %>
|
|
<.remix_icon icon="restart-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">Reconnect and setup</span>
|
|
<% end %>
|
|
</button>
|
|
<%= unless Livebook.Runtime.fixed_dependencies?(@runtime) do %>
|
|
<.menu id="setup-menu" position="bottom-left" distant>
|
|
<:toggle>
|
|
<button class="flex text-gray-600 hover:text-gray-800">
|
|
<.remix_icon icon="arrow-down-s-line" class="text-xl" />
|
|
</button>
|
|
</:toggle>
|
|
<.menu_item>
|
|
<button
|
|
role="menuitem"
|
|
data-el-queue-cell-evaluation-button
|
|
data-cell-id={@cell_id}
|
|
data-disable-dependencies-cache
|
|
>
|
|
<.remix_icon icon="play-circle-fill" />
|
|
<span>Setup without cache</span>
|
|
</button>
|
|
</.menu_item>
|
|
</.menu>
|
|
<% end %>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
defp setup_cell_evaluation_button(assigns) do
|
|
~H"""
|
|
<button
|
|
class="text-gray-600 hover:text-gray-800 flex space-x-1 items-center"
|
|
phx-click="cancel_cell_evaluation"
|
|
phx-value-cell_id={@cell_id}
|
|
>
|
|
<.remix_icon icon="stop-circle-fill" class="text-xl" />
|
|
<span class="text-sm font-medium">
|
|
Stop
|
|
</span>
|
|
</button>
|
|
"""
|
|
end
|
|
|
|
defp enable_insert_mode_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Edit content" data-el-enable-insert-mode-button>
|
|
<.icon_button aria-label="edit content">
|
|
<.remix_icon icon="pencil-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp toggle_source_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Toggle source" data-el-toggle-source-button>
|
|
<.icon_button aria-label="toggle source">
|
|
<.remix_icon icon="code-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp convert_smart_cell_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Convert to Code cell">
|
|
<.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" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp package_search_button(assigns) do
|
|
~H"""
|
|
<%= if Livebook.Runtime.fixed_dependencies?(@runtime) do %>
|
|
<span
|
|
class="tooltip top"
|
|
data-tooltip="The current runtime does not support adding dependencies"
|
|
>
|
|
<.icon_button disabled>
|
|
<.remix_icon icon="play-list-add-line" />
|
|
</.icon_button>
|
|
</span>
|
|
<% else %>
|
|
<span class="tooltip top" data-tooltip="Add package (sp)">
|
|
<.icon_button
|
|
patch={~p"/sessions/#{@session_id}/package-search"}
|
|
role="button"
|
|
data-btn-package-search
|
|
>
|
|
<.remix_icon icon="play-list-add-line" />
|
|
</.icon_button>
|
|
</span>
|
|
<% end %>
|
|
"""
|
|
end
|
|
|
|
defp cell_link_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Link">
|
|
<.icon_button href={"#cell-#{@cell_id}"} role="button" aria-label="link to cell">
|
|
<.remix_icon icon="link" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
def amplify_output_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Amplify output" data-el-amplify-outputs-button>
|
|
<.icon_button aria-label="amplify outputs">
|
|
<.remix_icon icon="zoom-in-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp cell_settings_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Cell settings">
|
|
<.icon_button
|
|
patch={~p"/sessions/#{@session_id}/cell-settings/#{@cell_id}"}
|
|
aria-label="cell settings"
|
|
role="button"
|
|
>
|
|
<.remix_icon icon="settings-3-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp move_cell_up_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Move up">
|
|
<.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" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp move_cell_down_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Move down">
|
|
<.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" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp delete_cell_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Delete">
|
|
<.icon_button
|
|
aria-label="delete cell"
|
|
phx-click={JS.push("delete_cell", value: %{cell_id: @cell_id})}
|
|
>
|
|
<.remix_icon icon="delete-bin-6-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp disable_language_button(assigns) do
|
|
~H"""
|
|
<span class="tooltip top" data-tooltip="Delete">
|
|
<.icon_button
|
|
aria-label="delete cell"
|
|
phx-click="disable_language"
|
|
phx-value-language={@language}
|
|
>
|
|
<.remix_icon icon="delete-bin-6-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp setup_cell_info(assigns) do
|
|
~H"""
|
|
<span
|
|
class="tooltip left"
|
|
data-tooltip={
|
|
~s'''
|
|
The setup cell includes code that initializes the notebook
|
|
and should run only once. This is the best place to install
|
|
dependencies and set global configuration.\
|
|
'''
|
|
}
|
|
>
|
|
<.icon_button>
|
|
<.remix_icon icon="question-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
end
|
|
|
|
defp pyproject_toml_cell_info(assigns) do
|
|
~H"""
|
|
<span
|
|
class="tooltip left"
|
|
data-tooltip={
|
|
~s'''
|
|
This cell specifies the Python environment using pyproject.toml
|
|
configuration. While standardized to a certain extent, this
|
|
configuration is used specifically with the uv package manager.\
|
|
'''
|
|
}
|
|
>
|
|
<.icon_button>
|
|
<.remix_icon icon="question-line" />
|
|
</.icon_button>
|
|
</span>
|
|
"""
|
|
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"""
|
|
<div
|
|
class={[@hidden && "hidden"]}
|
|
id={"cell-editor-#{@cell_id}-#{@tag}"}
|
|
phx-hook="CellEditor"
|
|
data-p-cell-id={hook_prop(@cell_id)}
|
|
data-p-tag={hook_prop(@tag)}
|
|
data-p-language={hook_prop(@language)}
|
|
data-p-intellisense={hook_prop(@intellisense)}
|
|
data-p-read-only={hook_prop(@read_only)}
|
|
>
|
|
<div
|
|
id={"cell-editor-#{@cell_id}-#{@tag}-container"}
|
|
phx-update="ignore"
|
|
class={["bg-editor", rounded_class(@rounded)]}
|
|
data-el-editor-container
|
|
>
|
|
<div data-el-skeleton>
|
|
<div class="py-3 px-8">
|
|
<.content_skeleton bg_class="bg-gray-500" empty={@empty} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
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"""
|
|
<div :if={@doctest_summary.failures_count > 0} class="pt-2" id={"doctest-summary-#{@cell_id}"}>
|
|
<div class="error-box py-3">
|
|
{doctest_summary_message(@doctest_summary)}
|
|
</div>
|
|
</div>
|
|
"""
|
|
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"""
|
|
<div
|
|
class="flex flex-col"
|
|
data-el-outputs-container
|
|
id={"outputs-#{@cell_view.id}-#{@cell_view.eval.outputs_batch_number}"}
|
|
phx-update="stream"
|
|
>
|
|
<LivebookWeb.Output.output
|
|
:for={{dom_id, output} <- @outputs}
|
|
id={dom_id}
|
|
output={output.output}
|
|
session_id={@session_id}
|
|
session_pid={@session_pid}
|
|
client_id={@client_id}
|
|
cell_id={@cell_view.id}
|
|
input_views={@cell_view.eval.input_views}
|
|
/>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :id, :string, required: true
|
|
attr :cell_view, :map, required: true
|
|
attr :langauge_toggle, :boolean, default: false
|
|
|
|
defp cell_indicators(assigns) do
|
|
~H"""
|
|
<div class="flex gap-1">
|
|
<.cell_indicator :if={has_status?(@cell_view)}>
|
|
<.cell_status id={@id} cell_view={@cell_view} />
|
|
</.cell_indicator>
|
|
<%= 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" />
|
|
</.cell_indicator>
|
|
</:toggle>
|
|
<.menu_item :for={language <- Livebook.Notebook.Cell.Code.languages()}>
|
|
<button
|
|
role="menuitem"
|
|
phx-click="set_cell_language"
|
|
phx-value-language={language.language}
|
|
phx-value-cell_id={@id}
|
|
>
|
|
<.cell_icon cell_type={:code} language={language.language} />
|
|
<span>{language.name}</span>
|
|
</button>
|
|
</.menu_item>
|
|
</.menu>
|
|
<% else %>
|
|
<.cell_indicator>
|
|
<.language_icon language={cell_language(@cell_view)} class="w-3 h-3" />
|
|
</.cell_indicator>
|
|
<% end %>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
attr :class, :string, default: nil
|
|
slot :inner_block, required: true
|
|
|
|
defp cell_indicator(assigns) do
|
|
~H"""
|
|
<div
|
|
data-el-cell-indicator
|
|
class={[
|
|
"px-1.5 h-[22px] rounded-lg flex items-center border bg-editor-lighter border-editor text-editor",
|
|
@class
|
|
]}
|
|
>
|
|
{render_slot(@inner_block)}
|
|
</div>
|
|
"""
|
|
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}>
|
|
<span
|
|
class="font-mono"
|
|
id={"#{@id}-cell-timer"}
|
|
phx-hook="Timer"
|
|
phx-update="ignore"
|
|
data-p-start={hook_prop(DateTime.to_iso8601(@cell_view.eval.evaluation_start))}
|
|
>
|
|
</span>
|
|
</.cell_status_indicator>
|
|
"""
|
|
end
|
|
|
|
defp cell_status(%{cell_view: %{eval: %{status: :queued}}} = assigns) do
|
|
~H"""
|
|
<.cell_status_indicator variant="waiting">
|
|
Queued
|
|
</.cell_status_indicator>
|
|
"""
|
|
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
|
|
</.cell_status_indicator>
|
|
"""
|
|
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
|
|
</.cell_status_indicator>
|
|
"""
|
|
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
|
|
</.cell_status_indicator>
|
|
"""
|
|
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"""
|
|
<div class={[@tooltip && "tooltip", "bottom distant-medium"]} data-tooltip={@tooltip}>
|
|
<div class="flex items-center space-x-1">
|
|
<div class="flex text-[11px]" data-el-cell-status>
|
|
{render_slot(@inner_block)}
|
|
<span :if={@change_indicator} data-el-change-indicator>*</span>
|
|
</div>
|
|
<.status_indicator variant={@variant} />
|
|
</div>
|
|
</div>
|
|
"""
|
|
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
|