defmodule ElixirAiWeb.ChatMessage do use Phoenix.Component alias Phoenix.LiveView.JS import ElixirAiWeb.JsonDisplay defp max_width_class, do: "max-w-full xl:max-w-300" attr :content, :string, required: true attr :tool_call_id, :string, required: true def tool_result_message(assigns) do ~H"""
tool result {@tool_call_id}
{@content}
""" end attr :content, :string, required: true def user_message(assigns) do ~H"""
{@content}
""" end attr :content, :string, required: true attr :reasoning_content, :string, default: nil attr :tool_calls, :list, default: [] def assistant_message(assigns) do assigns = assigns |> assign( :_reasoning_id, "reasoning-#{:erlang.phash2({assigns.content, assigns.reasoning_content, assigns.tool_calls})}" ) |> assign(:_expanded, false) ~H""" <.message_bubble reasoning_id={@_reasoning_id} content={@content} reasoning_content={@reasoning_content} tool_calls={@tool_calls} expanded={@_expanded} /> """ end attr :content, :string, required: true attr :reasoning_content, :string, default: nil attr :tool_calls, :list, default: [] # Renders the in-progress streaming message. Content and reasoning are rendered # entirely client-side via the MarkdownStream hook — the server sends push_event # chunks instead of re-rendering the full markdown on every token. def streaming_assistant_message(assigns) do ~H"""
<%= if @reasoning_content && @reasoning_content != "" do %> <% end %>
<%= for tool_call <- @tool_calls do %> <.tool_call_item tool_call={tool_call} /> <% end %>
""" end attr :content, :string, required: true attr :reasoning_content, :string, default: nil attr :tool_calls, :list, default: [] attr :reasoning_id, :string, required: true attr :expanded, :boolean, default: false defp message_bubble(assigns) do ~H"""
<%= if @reasoning_content && @reasoning_content != "" do %>
<% end %> <%= for tool_call <- @tool_calls do %> <.tool_call_item tool_call={tool_call} /> <% end %> <%= if @content && @content != "" do %>
<% end %>
""" end # Dispatches to the unified tool_call_card component, determining state from the map keys: # :error key → :error (runtime failure) # :result key → :success (completed) # :index key → :pending (streaming in-progress) # none → :called (DB-loaded; result is a separate message) attr :tool_call, :map, required: true defp tool_call_item(%{tool_call: tool_call} = assigns) do state = cond do Map.has_key?(tool_call, :error) -> :error Map.has_key?(tool_call, :result) -> :success Map.has_key?(tool_call, :index) -> :pending true -> :called end assigns = assigns |> assign(:_state, state) |> assign(:_name, tool_call.name) |> assign(:_arguments, tool_call[:arguments]) |> assign(:_result, tool_call[:result]) |> assign(:_error, tool_call[:error]) ~H"<.tool_call_card state={@_state} name={@_name} arguments={@_arguments} result={@_result} error={@_error} />" end attr :state, :atom, required: true attr :name, :string, required: true attr :arguments, :any, default: nil attr :result, :any, default: nil attr :error, :string, default: nil defp tool_call_card(assigns) do assigns = assigns |> assign(:_id, "tc-#{:erlang.phash2({assigns.name, assigns.arguments})}") |> assign(:_truncated, truncate_args(assigns.arguments)) |> assign( :_result_str, case assigns.result do nil -> nil s when is_binary(s) -> s other -> inspect(other, pretty: true, limit: :infinity) end ) ~H"""
JS.toggle_class("rotate-180", to: "##{@_id}-chevron") } > <.tool_call_icon /> {@name} <.json_display json={@_truncated} inline /> called running done error
result
{@_result_str}
error
{@error}
""" end defp truncate_args(nil), do: nil defp truncate_args(""), do: nil defp truncate_args(args) when is_binary(args) do compact = case Jason.decode(args) do {:ok, decoded} -> Jason.encode!(decoded) _ -> args end if String.length(compact) > 72, do: String.slice(compact, 0, 69) <> "\u2026", else: compact end defp truncate_args(args) do compact = Jason.encode!(args) if String.length(compact) > 72, do: String.slice(compact, 0, 69) <> "\u2026", else: compact end attr :arguments, :any, default: nil defp tool_call_args(%{arguments: args} = assigns) when not is_nil(args) and args != "" do ~H"""
arguments
<.json_display json={@arguments} />
""" end defp tool_call_args(assigns), do: ~H"" defp tool_call_icon(assigns) do ~H""" """ end end