# `PetalComponents.Chat`
[🔗](https://github.com/petalframework/petal_components/blob/v4.1.2/lib/petal_components/chat.ex#L1)

AI chat / conversation components — the LiveView-native answer to React's
AI Elements / assistant-ui. Build streaming chat UIs without a client AI SDK:
tokens stream over the LiveView socket you already have.

Components:

  * `conversation/1`  — scrollable thread container (slot-driven)
  * `chat_message/1`  — a single message bubble (user or assistant)
  * `streaming_text/1` — token-by-token output via the `PetalChatStream` JS hook
  * `prompt_input/1`   — the composer (textarea + send)

## Importing

Unlike the core components, `Chat` is **not** brought in by `use PetalComponents`
— it defines generic names (`markdown/1`, `reasoning/1`, …) that would clash with
your app's own helpers. Alias it and call it namespaced:

    alias PetalComponents.Chat

    <Chat.conversation id="chat">
      <Chat.chat_message role="assistant"><Chat.markdown content={@text} /></Chat.chat_message>
    </Chat.conversation>

The examples below use that `Chat.` prefix.

## Streaming

`streaming_text/1` is driven by the bundled `PetalChatStream` JS hook. The
parent LiveView pushes each delta and the hook appends it to the bubble:

    # per Gemini/OpenAI delta:
    socket = push_event(socket, "pc-chat-token", %{id: "answer", text: delta})

    <Chat.streaming_text id="answer" />

Register the hooks once in your LiveSocket:

    import PetalComponents from "../../deps/petal_components/assets/js/petal_components"
    new LiveSocket("/live", Socket, { hooks: { ...PetalComponents }, ... })

## Styling

Every component takes a `class` that is appended last (CSS specificity wins,
matching the rest of petal_components). Theme tokens are exposed as CSS
variables (`--pc-chat-*`) for reskinning without touching markup, and any part
can be fully replaced via slots.

# `chat_error`

An error notice with an optional retry button.

    <Chat.chat_error on_retry="retry">Something went wrong.</Chat.chat_error>

## Attributes

* `on_retry` (`:string`) - Defaults to `nil`.
* `retry_label` (`:string`) - Defaults to `"Retry"`.
* `class` (`:any`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `chat_message`

A single message bubble.

Default markup, or replace it entirely — the `class` is appended last so your
utilities win, and the `:inner_block` is yours to fill.

## Attributes

* `role` (`:string`) - Defaults to `"assistant"`. Must be one of `"user"`, `"assistant"`, or `"system"`.
* `class` (`:any`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `avatar` - optional leading avatar/icon.
* `inner_block` (required)

# `conversation`

A scrollable conversation thread. Composition-first: drop `chat_message/1`,
`streaming_text/1`, or your own markup inside.

    <Chat.conversation>
      <Chat.chat_message :for={msg <- @messages} role={msg.role}>{msg.text}</Chat.chat_message>
      <:footer>
        <Chat.prompt_input phx-submit="send" loading={@streaming?} />
      </:footer>
    </Chat.conversation>

## Attributes

* `id` (`:string`) - defaults to a generated id so multiple threads can coexist.
* `class` (`:any`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)
* `footer` - pinned below the scroll area, e.g. a prompt_input.

# `copy_button`

A copy-to-clipboard button (via the `PetalCopy` hook). Shows brief "Copied!"
feedback. Requires a unique `id`.

## Attributes

* `id` (`:string`) (required)
* `text` (`:string`) (required) - the text to copy.
* `label` (`:string`) - Defaults to `"Copy"`.
* `class` (`:any`) - Defaults to `nil`.

# `markdown`

Renders markdown as sanitized, syntax-highlighted HTML (via MDEx). Use it for
committed assistant messages so headings, lists, tables and code blocks render
properly:

    <Chat.chat_message role="assistant"><Chat.markdown content={msg.text} /></Chat.chat_message>

Output is sanitized server-side — model text is never rendered as live markup.

> #### Markdown is rendered faithfully {: .info}
>
> Code blocks come from the model's own fences. If a model wraps an example
> that itself contains a ` ``` ` fence inside another same-length ` ``` ` fence,
> that is invalid CommonMark and renders broken (the outer fence closes early) —
> every CommonMark renderer behaves this way. Steer the model with a system
> prompt: "when showing example markdown that contains code fences, wrap the
> outer block in MORE backticks than the inner fence."

## Attributes

* `content` (`:string`) (required)
* `id` (`:string`) - pass a unique id to enable per-code-block copy buttons. Defaults to `nil`.
* `class` (`:any`) - Defaults to `nil`.

# `message_actions`

A row of actions under a message (copy, regenerate, feedback). Compose with
`copy_button/1` and your own `phx-click` buttons using the `pc-chat__action`
class.

    <Chat.message_actions>
      <Chat.copy_button id={"copy-#{@id}"} text={@text} />
      <button class="pc-chat__action" phx-click="regenerate">Regenerate</button>
    </Chat.message_actions>

## Attributes

* `class` (`:any`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `prompt_input`

The composer. Wraps a form; pass `phx-submit` (and optionally `phx-change`)
through the global attrs.

    <Chat.prompt_input phx-submit="send" phx-change="draft" value={@draft} loading={@streaming?} on_stop="stop" />

While `loading`, the input stays editable (so you can draft your next message)
and the send button becomes a stop button that pushes `on_stop` — wire it to
cancel your generation task.

## Attributes

* `id` (`:string`) - defaults to a generated id so multiple composers can coexist.
* `name` (`:string`) - Defaults to `"prompt"`.
* `value` (`:string`) - Defaults to `""`.
* `placeholder` (`:string`) - Defaults to `"Send a message..."`.
* `aria_label` (`:string`) - accessible label for the textarea. Defaults to `"Message"`.
* `loading` (`:boolean`) - Defaults to `false`.
* `on_stop` (`:string`) - event pushed when the stop button is clicked while loading. Defaults to `nil`.
* `submit_label` (`:string`) - Defaults to `"Send"`.
* `class` (`:any`) - Defaults to `nil`.
* Global attributes are accepted. Supports all globals plus: `["phx-submit", "phx-change", "phx-target"]`.
## Slots

* `actions` - extra controls left of the send button.

# `reasoning`

A collapsible "thinking" / reasoning block for reasoning-model output. Native
`<details>`, so no JS.

    <Chat.reasoning>Chain of thought here...</Chat.reasoning>
    <Chat.reasoning label="Thought for 3s" open>...</Chat.reasoning>

## Attributes

* `label` (`:string`) - Defaults to `"Reasoning"`.
* `open` (`:boolean`) - Defaults to `false`.
* `class` (`:any`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `rich_text`

Markdown with inline widget directives ("MDX for Phoenix").

The model can drop a widget mid-prose with a fenced block tagged
` ```widget:<name> ` containing JSON args. Everything else renders as normal
markdown (normal code fences like ` ```elixir ` are untouched). You supply a
`render_widget` function that maps a name + args to a rendered component:

    <Chat.rich_text
      content={@text}
      render_widget={fn
        "weather", args -> ~H"<.weather_card city={args["city"]} .../>"
        _, _ -> nil
      end}
    />

Example model output:

    Here's the forecast:

    ```widget:weather
    {"city": "Paris"}
    ```

    Pack an umbrella.

## Attributes

* `content` (`:string`) (required)
* `render_widget` (`:any`) - fn(name :: String.t(), args :: map) -> rendered | nil. Defaults to `nil`.
* `class` (`:any`) - Defaults to `nil`.

# `streaming_text`

Token-by-token streaming output, driven by the `PetalChatStream` JS hook.

Render this in the in-progress assistant bubble. The parent LiveView pushes
each delta to it:

    socket = push_event(socket, "pc-chat-token", %{id: "answer", text: delta})

    <Chat.streaming_text id="answer" />

Until the first token lands it shows a typing indicator; on the first token it
swaps to live text with a blinking caret. The element owns its own DOM
(`phx-update="ignore"`), so no re-render clobbers the streamed text.

## Attributes

* `id` (`:string`) (required)
* `event` (`:string`) - push_event name the hook listens for. Defaults to `"pc-chat-token"`.
* `format` (`:string`) - "text" appends raw token deltas; "markdown" replaces innerHTML with rendered HTML you push (see `to_html/1`). Defaults to `"text"`. Must be one of `"text"`, or `"markdown"`.
* `class` (`:any`) - Defaults to `nil`.

# `suggestions`

Clickable prompt-starter chips for an empty state. Each pushes `on_select`
with `phx-value-prompt` set to the suggestion.

    <Chat.suggestions items={["Summarise this", "Write tests"]} on_select="suggest" />

## Attributes

* `items` (`:list`) (required)
* `on_select` (`:string`) - event pushed with phx-value-prompt. Defaults to `"suggestion"`.
* `class` (`:any`) - Defaults to `nil`.

# `to_html`

Render markdown to sanitized, syntax-highlighted HTML using the same engine
the `markdown/1` component uses. Use it to live-stream markdown: throttle calls
on your growing buffer and `push_event` the result to a `format="markdown"`
`streaming_text/1`:

    socket = push_event(socket, "pc-chat-token", %{id: "answer", html: PetalComponents.Chat.to_html(buffer)})

# `tool_call`

A tool-call card — the chrome around a generative-UI widget.

This is the "AI Elements" pattern done LiveView-native: the model emits a
structured tool call (function calling), you map the tool name to one of your
registered Phoenix components, and render it inside this card. The widget is a
real LiveView component — it can have its own `phx-click`, forms, streams.

    <Chat.tool_call name="get_weather" status={:complete}>
      <.weather_card city={@args["city"]} temp={@result.temp} />
    </Chat.tool_call>

`status` drives the header affordance: `:running` shows a spinner, `:complete`
a check, `:error` a warning.

## Attributes

* `name` (`:string`) (required)
* `status` (`:atom`) - Defaults to `:complete`. Must be one of `:running`, `:complete`, or `:error`.
* `label` (`:string`) - human label; defaults to the tool name. Defaults to `nil`.
* `class` (`:any`) - Defaults to `nil`.
## Slots

* `inner_block` - the rendered widget / tool result.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
