Skip to main content
Integrate the Char agent into your Phoenix application with LiveView. The key is placing the agent outside your LiveView content and using phx-update="ignore" to prevent LiveView from touching it.

Installation

Add to your assets/package.json:
{
  "dependencies": {
    "@mcp-b/char": "latest"
  }
}
Or use a CDN in your layout.

Basic Usage

Place the agent in your root layout with phx-update="ignore":
<!-- lib/my_app_web/components/layouts/root.html.heex -->
<!DOCTYPE html>
<html>
<head>
  <meta name="id-token" content={assigns[:id_token]}>
  <meta name="client-id" content={assigns[:client_id]}>
  <script src="https://unpkg.com/@mcp-b/char/dist/web-component-standalone.iife.js" defer></script>
</head>
<body>
  <%= @inner_content %>

  <!-- phx-update="ignore" prevents LiveView from touching this element -->
  <char-agent id="chat-widget" phx-update="ignore"></char-agent>

  <script>
    const agent = document.getElementById('chat-widget');
    const idToken = document.querySelector('meta[name="id-token"]')?.content;
    const clientId = document.querySelector('meta[name="client-id"]')?.content;

    if (agent && idToken && clientId) {
      agent.connect({ idToken, clientId });
    }
  </script>
</body>
</html>

With Hooks

Create a LiveView hook for better control:
// assets/js/hooks/char_agent.js
const CharAgent = {
  mounted() {
    import('@mcp-b/char/web-component').then(() => {
      const idToken = this.el.dataset.idToken;
      const clientId = this.el.dataset.clientId;
      if (idToken && clientId) {
        this.el.connect({ idToken, clientId });
      }
    });
  },

  updated() {
    const idToken = this.el.dataset.idToken;
    const clientId = this.el.dataset.clientId;
    if (idToken && clientId) {
      this.el.connect({ idToken, clientId });
    }
  }
};

export default CharAgent;
// assets/js/app.js
import CharAgent from "./hooks/char_agent";

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { CharAgent },
  // ...
});
<!-- In your template -->
<char-agent
  id="chat-widget"
  phx-hook="CharAgent"
  phx-update="ignore"
  data-id-token={@id_token}
  data-client-id={@client_id}
></char-agent>

With Guardian/Ueberauth

If using Guardian and Ueberauth for authentication:
# lib/my_app_web/plugs/put_id_token.ex
defmodule MyAppWeb.Plugs.PutIdToken do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    case Guardian.Plug.current_token(conn) do
      nil -> conn
      token -> assign(conn, :id_token, token)
    end
  end
end
# lib/my_app_web/router.ex
pipeline :browser do
  # ...
  plug MyAppWeb.Plugs.PutIdToken
end

LiveView Component

Create a component that manages the agent:
# lib/my_app_web/components/char_agent.ex
defmodule MyAppWeb.Components.CharAgent do
  use Phoenix.Component

  attr :id_token, :string, required: true

  def char_agent(assigns) do
    ~H"""
    <char-agent
      id="chat-widget"
      phx-hook="CharAgent"
      phx-update="ignore"
      data-id-token={@id_token}
    ></char-agent>
    """
  end
end
<!-- Usage in your layout -->
<.char_agent id_token={@id_token} />

SSR with Ticket Exchange

For enhanced security, exchange tokens server-side:
# lib/my_app/char.ex
defmodule MyApp.Char do
  def get_ticket(id_token) do
    case HTTPoison.post(
      "https://api.usechar.ai/api/auth/ticket",
      "",
      [{"Authorization", "Bearer #{id_token}"}]
    ) do
      {:ok, %{status_code: 200, body: body}} ->
        {:ok, Jason.decode!(body)}
      _ ->
        {:error, :failed}
    end
  end
end
# In your LiveView
def mount(_params, session, socket) do
  case MyApp.Char.get_ticket(session["id_token"]) do
    {:ok, ticket_auth} ->
      {:ok, assign(socket, :ticket_auth, ticket_auth)}
    _ ->
      {:ok, assign(socket, :ticket_auth, nil)}
  end
end
<char-agent
  :if={@ticket_auth}
  id="chat-widget"
  phx-hook="CharAgentTicket"
  phx-update="ignore"
  data-ticket-auth={Jason.encode!(@ticket_auth)}
></char-agent>

See Also