AI chat components

AI Elements for Phoenix

React has assistant-ui and Vercel AI Elements. Phoenix has petal_components. Streaming chat bubbles, markdown, tool-call widgets, a composer. Tokens stream over the LiveView socket you already have, so there is no client AI SDK, no API route, and no useChat plumbing.

petal.build/petal-components/ai LiveView socket
Hi! Ask me anything. Replies stream over the LiveView socket you already have - no client AI SDK.

Why it's simpler in LiveView

In React you wire up the AI SDK, a useChat hook, an API route, and a streaming response handler. The browser talks to your server, your server talks to the model, and you glue the two together.

In LiveView the socket is already open. The model streams tokens to your LiveView process, and you push each one straight to the bubble. The component owns its own DOM, so nothing clobbers the text as it arrives. That is the whole architecture.

A streaming chat, start to finish

The template is the easy part. Add alias PetalComponents.Chat to your LiveView, then drop the thread, render committed messages, and put a streaming bubble in while a reply is in flight.

<Chat.conversation id="chat">
  <Chat.chat_message :for={msg <- @messages} role={msg.role}>
    <Chat.markdown :if={msg.role == "assistant"} content={msg.text} />
  </Chat.chat_message>

  <Chat.chat_message :if={@streaming?} role="assistant">
    <Chat.streaming_text id="answer" />
  </Chat.chat_message>

  <:footer>
    <Chat.prompt_input phx-submit="send" loading={@streaming?} on_stop="stop" />
  </:footer>
</Chat.conversation>

In the LiveView, push each token from the model to the bubble. That is the only contract.

def handle_info({:llm_delta, text}, socket) do
  {:noreply, push_event(socket, "pc-chat-token", %{id: "answer", text: text})}
end

Full walkthrough in the streaming chat guide, including markdown that renders as it streams and tool calls that map to your own components.

What's in the box

<Chat.conversation>

Scrollable thread with a composer footer slot.

<Chat.chat_message>

User and assistant bubbles, with an avatar slot.

<Chat.streaming_text>

Token-by-token output, no flicker, no re-render churn.

<Chat.prompt_input>

Composer. Enter to send, Shift+Enter for a newline.

<Chat.markdown>

Sanitized, syntax-highlighted markdown. Streams live too.

<Chat.tool_call>

Render a function call as a real, interactive Phoenix component.

<Chat.reasoning>

A collapsible thinking block for reasoning models.

<Chat.suggestions>

Prompt-starter chips for the empty state.

Tool calls render real components

When the model calls a function, you map the name to one of your own Phoenix components and render it inside the chat. The model emits data, not markup, so there is no injection surface. And because the widget is a LiveView component, it can have its own clicks, forms, and streams.

This is generative UI, and LiveView is a better home for it than React: the widget is server-stateful by default.

Build it this weekend

petal_components is free and open source. Install it, register the hooks, and follow the guide.