defmodule LivebookWeb.Output do
use LivebookWeb, :html
import LivebookWeb.Helpers
alias LivebookWeb.Output
@doc """
Renders a list of cell outputs.
"""
attr :outputs, :list, required: true
attr :session_id, :string, required: true
attr :session_pid, :any, required: true
attr :input_values, :map, required: true
attr :dom_id_map, :map, required: true
attr :client_id, :string, required: true
attr :cell_id, :string, required: true
def outputs(assigns) do
~H"""
<%= render_output(output, %{
id: "output-#{idx}",
session_id: @session_id,
session_pid: @session_pid,
input_values: @input_values,
client_id: @client_id,
cell_id: @cell_id
}) %>
"""
end
defp border?({:stdout, _text}), do: true
defp border?({:text, _text}), do: true
defp border?({:error, _message, {:interrupt, _, _}}), do: false
defp border?({:error, _message, _type}), do: true
defp border?({:grid, _, info}), do: Map.get(info, :boxed, false)
defp border?(_output), do: false
defp render_output({:stdout, text}, %{id: id}) do
text = if(text == :__pruned__, do: nil, else: text)
live_component(Output.StdoutComponent, id: id, text: text)
end
defp render_output({:text, text}, %{id: id}) do
assigns = %{id: id, text: text}
~H"""
"""
end
defp render_output({:plain_text, text}, %{id: id}) do
assigns = %{id: id, text: text}
~H"""
<%= @text %>
"""
end
defp render_output({:markdown, markdown}, %{id: id, session_id: session_id}) do
live_component(Output.MarkdownComponent,
id: id,
session_id: session_id,
content: markdown
)
end
defp render_output({:image, content, mime_type}, %{id: id}) do
assigns = %{id: id, content: content, mime_type: mime_type}
~H"""
"""
end
defp render_output({:js, js_info}, %{id: id, session_id: session_id, client_id: client_id}) do
live_component(LivebookWeb.JSViewComponent,
id: id,
js_view: js_info.js_view,
session_id: session_id,
client_id: client_id,
timeout_message: "Output data no longer available, please reevaluate this cell"
)
end
defp render_output({:frame, outputs, info}, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
}) do
live_component(Output.FrameComponent,
id: id,
outputs: outputs,
placeholder: Map.get(info, :placeholder, true),
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
)
end
defp render_output({:tabs, outputs, info}, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
}) do
{labels, active_idx} =
if info.labels == :__pruned__ do
{[], nil}
else
labels =
Enum.zip_with(info.labels, outputs, fn label, {output_idx, _} -> {output_idx, label} end)
active_idx = get_in(outputs, [Access.at(0), Access.elem(0)])
{labels, active_idx}
end
assigns = %{
id: id,
active_idx: active_idx,
labels: labels,
outputs: outputs,
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
}
# After pruning we don't render labels and we render only those
# outputs that are kept during pruning
~H"""
JS.add_class("active")
|> JS.add_class("hidden", to: "##{@id}-tab-contents > *:not(.hidden)")
|> JS.remove_class("hidden", to: "##{@id}-tab-content-#{output_idx}")
}
>
<%= label %>
<% # We use data-keep-attribute, because we know active_idx only on the first render %>
<.outputs
outputs={[{output_idx, output}]}
dom_id_map={%{}}
session_id={@session_id}
session_pid={@session_pid}
input_values={@input_values}
client_id={@client_id}
cell_id={@cell_id}
/>
"""
end
defp render_output({:grid, outputs, info}, %{
id: id,
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
}) do
columns = info[:columns] || 1
gap = info[:gap] || 8
assigns = %{
id: id,
columns: columns,
gap: gap,
outputs: outputs,
session_id: session_id,
session_pid: session_pid,
input_values: input_values,
client_id: client_id,
cell_id: cell_id
}
~H"""
"""
end
defp render_output({:input, attrs}, %{
id: id,
input_values: input_values,
session_pid: session_pid,
client_id: client_id
}) do
live_component(Output.InputComponent,
id: id,
attrs: attrs,
input_values: input_values,
session_pid: session_pid,
client_id: client_id
)
end
defp render_output({:control, attrs}, %{
id: id,
input_values: input_values,
session_pid: session_pid,
client_id: client_id
}) do
live_component(Output.ControlComponent,
id: id,
attrs: attrs,
input_values: input_values,
session_pid: session_pid,
client_id: client_id
)
end
defp render_output({:error, formatted, {:missing_secret, secret_name}}, %{
session_id: session_id
}) do
assigns = %{
message: formatted,
secret_name: secret_name,
session_id: session_id
}
~H"""
<.remix_icon icon="close-circle-line" />
Missing secret <%= inspect(@secret_name) %>
<.link
patch={~p"/sessions/#{@session_id}/secrets?secret_name=#{@secret_name}"}
class="button-base button-gray"
>
Add secret
<%= render_formatted_error_message(@message) %>
"""
end
defp render_output({:error, _formatted, {:interrupt, variant, message}}, %{cell_id: cell_id}) do
assigns = %{variant: variant, message: message, cell_id: cell_id}
~H"""
"text-red-400 border-red-400"
:normal -> "text-gray-500 border-gray-300"
end
]}>
<%= @message %>
"border-red-400 text-red-400 hover:bg-red-50 focus:bg-red-50"
:normal -> "border-gray-300 text-gray-500 hover:bg-gray-100 focus:bg-gray-100"
end
]}
phx-click="queue_interrupted_cell_evaluation"
phx-value-cell_id={@cell_id}
>
<.remix_icon icon="play-circle-fill" class="align-middle mr-1" />
Continue
"""
end
defp render_output({:error, formatted, _type}, %{}) do
render_formatted_error_message(formatted)
end
defp render_output(output, %{}) do
render_error_message("""
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(message) do
assigns = %{message: message}
~H"""
<%= @message %>
"""
end
defp render_formatted_error_message(formatted) do
assigns = %{message: formatted}
~H"""
<%= ansi_string_to_html(@message) %>
"""
end
end