PhiaUi.Components.ChatMessage (phia_ui v0.1.17)

Copy Markdown View Source

Chat interface components for PhiaUI.

Provides a complete AI/human chat UI: a scrollable message log, individual message rows aligned by role, styled content bubbles with optional avatars and feedback buttons, clickable suggestion chips, and a compose form.

CSS-only layout — no JS hook required. Fully compatible with LiveView streams for real-time message delivery, including streaming AI token output.

When to use

  • AI assistant chat (GPT-style, Anthropic Claude, etc.)
  • Customer support live chat embedded in an app
  • Team messaging feed within a product

Anatomy

ComponentElementPurpose
chat_container/1divScrollable log (role="log", live region)
chat_message/1divA message row, aligned by :role
chat_bubble/1divStyled content balloon with optional avatar
chat_suggestions/1divRow of suggestion chips below an AI message
chat_input/1formBottom compose form with phx-submit

Role alignment

RoleAlignmentBackground
userRightbg-primary
assistantLeftbg-muted
systemCentertransparent, italic

Minimal chat example

<.chat_container id="ai-chat" class="h-96 p-4">
  <.chat_message role="assistant">
    <.chat_bubble role="assistant" timestamp="2:30 PM">
      Welcome! How can I help you today?
    </.chat_bubble>
    <.chat_suggestions
      suggestions={["Show me an example", "What can you do?"]}
      on_select="select_suggestion"
    />
  </.chat_message>

  <.chat_message role="user">
    <.chat_bubble role="user" timestamp="2:31 PM">
      Show me an example
    </.chat_bubble>
  </.chat_message>
</.chat_container>

<.chat_input id="chat-compose" on_submit="send_message" placeholder="Ask anything..." />

Full AI assistant with LiveView streams

Using streams means only new messages are DOM-patched; the full message list is never re-rendered, which is critical for long conversations.

# LiveView mount/3 — initialise the stream
def mount(_params, _session, socket) do
  {:ok, stream(socket, :messages, initial_messages())}
end

# Push a new AI message (e.g. from a Task or GenServer)
def handle_info({:ai_message, msg}, socket) do
  {:noreply, stream_insert(socket, :messages, msg)}
end

# Template
<.chat_container id="ai-chat" class="flex-1 overflow-y-auto p-4">
  <.chat_message
    :for={{dom_id, msg} <- @streams.messages}
    id={dom_id}
    role={msg.role}
  >
    <.chat_bubble
      role={msg.role}
      timestamp={Calendar.strftime(msg.inserted_at, "%I:%M %p")}
      on_feedback={if msg.role == "assistant", do: "rate_message"}
      message_id={msg.id}
    >
      <:avatar :if={msg.role == "assistant"}>
        <.avatar size="sm">
          <.avatar_fallback name="AI" />
        </.avatar>
      </:avatar>
      {msg.content}
    </.chat_bubble>
  </.chat_message>
</.chat_container>

<.chat_input
  id="chat-compose"
  on_submit="send_message"
  placeholder="Type a message..."
  max_chars={2000}
/>

Streaming AI token output

For token-by-token streaming responses, stream_insert/4 with at: -1 updates the last message in-place via LiveView's morphdom patching:

def handle_info({:token, token}, socket) do
  # Append the token to the last AI message in your stream
  {:noreply, stream_insert(socket, :messages, updated_message, at: -1)}
end

Summary

Functions

Renders the styled chat content balloon.

Renders the chat log scroll container.

Renders the chat compose form.

Renders a message row with role-based alignment.

Renders a row of clickable conversation suggestion chips.

Functions

chat_bubble(assigns)

Renders the styled chat content balloon.

  • role="user"bg-primary text-primary-foreground (right-aligned)
  • role="assistant"bg-muted text-foreground (left-aligned)
  • role="system" — transparent, italic, centered small text

Supply an :avatar slot to show a user or AI avatar beside the bubble. Set on_feedback + message_id to enable thumbs up/down rating buttons for assistant responses (common in AI assistant UIs for RLHF collection).

Example — assistant bubble with avatar and feedback

<.chat_bubble
  role="assistant"
  timestamp="2:34 PM"
  on_feedback="rate_message"
  message_id={msg.id}
>
  <:avatar>
    <.avatar size="sm"><.avatar_fallback name="AI" /></.avatar>
  </:avatar>
  The capital of France is Paris.
</.chat_bubble>

Attributes

  • role (:string) (required) - Bubble role — controls background colour and text colour. Must be one of "user", "assistant", or "system".

  • timestamp (:string) - Optional time label rendered beneath the bubble, e.g. "2:34 PM". Defaults to nil.

  • on_feedback (:string) - phx-click event name for thumbs up/down feedback buttons. Only rendered for role="assistant" bubbles. Pair with :message_id to identify which message received feedback.

    Defaults to nil.

  • message_id (:string) - ID passed as phx-value-message-id to feedback buttons. The LiveView receives %{"message_id" => id, "feedback" => "up" | "down"}.

    Defaults to nil.

  • class (:string) - Additional CSS classes for the bubble wrapper. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the bubble wrapper div.

Slots

  • avatar - Optional avatar/1 component displayed beside the bubble. For role="assistant" the avatar appears on the left; for role="user" it appears on the right (matching message alignment).

  • inner_block (required) - Bubble text content. May include inline HTML or Markdown-rendered content. For streaming AI output, update this via stream_insert/4.

chat_container(assigns)

Renders the chat log scroll container.

Sets role="log" and aria-live="polite" so screen readers announce new messages as they arrive via LiveView streams without interrupting the user's current focus or reading flow.

Apply a fixed height and overflow-y-auto via :class to make the log scrollable within a fixed layout region:

<.chat_container id="chat" class="h-[600px] overflow-y-auto p-4">
  ...
</.chat_container>

Attributes

  • id (:string) - DOM id for the container. Recommended when using LiveView streams so the engine can locate and patch individual messages efficiently.

    Defaults to nil.

  • class (:string) - Additional CSS classes for the scroll container (e.g. h-96 overflow-y-auto). Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the root div.

Slots

chat_input(assigns)

Renders the chat compose form.

The form fires on_submit via phx-submit. A max_chars counter is displayed when provided. Attachment chips (e.g. file uploads) can be rendered above the textarea via the :attachments slot.

Example

<.chat_input
  id="chat-compose"
  on_submit="send_message"
  placeholder="Ask me anything..."
  max_chars={4000}
  phx-change="typing"
>
  <:attachments>
    <.badge :for={file <- @pending_attachments} variant="outline">
      <.icon name="paperclip" size={:xs} />
      {file.name}
    </.badge>
  </:attachments>
</.chat_input>

Attributes

  • id (:string) (required) - DOM id for the form element.

  • on_submit (:string) (required) - phx-submit event name. The LiveView receives %{"message" => text}. After handling, reset the textarea with Phoenix.LiveView.push_event/3 or by clearing the assign that controls the input value.

  • placeholder (:string) - Textarea placeholder text. Defaults to "Type a message...".

  • max_chars (:integer) - Optional character limit displayed as a static counter below the textarea. Note: actual enforcement must be done server-side in the LiveView handler.

    Defaults to nil.

  • class (:string) - Additional CSS classes for the form wrapper. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the form element. Supports all globals plus: ["phx-change"].

Slots

  • attachments - Optional attachment chips rendered above the textarea. Use badge/1 or custom chips to show files the user has attached.

chat_message(assigns)

Renders a message row with role-based alignment.

Alignment is determined by :role:

  • useritems-end (right side, mirroring human side of a conversation)
  • assistantitems-start (left side, mirroring AI/agent side)
  • systemitems-center (centered, for notices like "Session started")

Attributes

  • role (:string) (required) - Message role — controls horizontal alignment:
    • user → right-aligned (items-end)
    • assistant → left-aligned (items-start)
    • system → centered (items-center) Must be one of "user", "assistant", or "system".
  • id (:string) - DOM id — required when using LiveView streams (pass dom_id from stream tuple). Defaults to nil.
  • class (:string) - Additional CSS classes for the message wrapper. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the message wrapper div.

Slots

chat_suggestions(assigns)

Renders a row of clickable conversation suggestion chips.

Each chip fires on_select with phx-value-suggestion set to the chip text. Render these below an assistant bubble to help users discover common queries.

Example

<.chat_suggestions
  suggestions={["Tell me more", "Show an example", "How does billing work?"]}
  on_select="select_suggestion"
/>

# LiveView handler
def handle_event("select_suggestion", %{"suggestion" => text}, socket) do
  # Treat the suggestion as if the user typed and sent it
  {:noreply, send_user_message(socket, text)}
end

Attributes

  • suggestions (:list) (required) - List of suggestion strings shown as clickable pill buttons below an assistant bubble. Typically 2–4 concise prompts that guide the user.

  • on_select (:string) (required) - phx-click event emitted when a suggestion is selected. The LiveView receives %{"suggestion" => text}.

  • class (:string) - Additional CSS classes for the suggestions row. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the wrapper div.