README.md

# LiveRender

**Generative UI for Phoenix LiveView.**

AI generates a JSON spec → LiveView resolves it to function components server-side → pushes only HTML diffs over the WebSocket.

Inspired by Vercel's [json-render](https://github.com/vercel-labs/json-render), built idiomatically for the BEAM.

## Why LiveRender?

- **Guardrailed** — AI can only use components you register in a catalog
- **Server-side** — specs stay on the server; no JSON runtime shipped to the client
- **Streaming** — stable elements freeze with `phx-update="ignore"`, only the active node re-renders
- **Batteries included** — 18 built-in components ready to use
- **Bring your own LLM** — works with ReqLLM, Jido, or any client that produces a spec map

## Quick Start

### 1. Define a catalog

```elixir
defmodule MyApp.AI.Catalog do
  use LiveRender.Catalog

  component LiveRender.Components.Card
  component LiveRender.Components.Metric
  component LiveRender.Components.Button
end
```

`system_prompt/0` generates an LLM prompt describing every registered component — props, types, descriptions. `json_schema/0` returns a JSON Schema for structured output.

### 2. Render a spec

```heex
<LiveRender.render
  spec={@spec}
  catalog={MyApp.AI.Catalog}
  streaming={@streaming?}
/>
```

**That's it.** AI generates JSON, LiveRender renders it safely through your catalog.

### 3. Connect an LLM

With [ReqLLM](https://hex.pm/packages/req_llm):

```elixir
defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view
  use LiveRender.Live

  def mount(_params, _session, socket), do: {:ok, init_live_render(socket)}

  def handle_event("generate", %{"prompt" => prompt}, socket) do
    LiveRender.Generate.stream_spec("anthropic:claude-haiku-4-5", prompt,
      catalog: MyApp.AI.Catalog,
      pid: self()
    )

    {:noreply, start_live_render(socket)}
  end
end
```

`use LiveRender.Live` injects `handle_info` clauses for `:text_chunk`, `:spec`, `:done`, and `:error` messages.

Or with [Jido](https://hex.pm/packages/jido_ai) for full ReAct agent loops with tool calling — see the [example app](example/).

Or bring your own client — anything that produces a spec map works.

## Installation

```elixir
def deps do
  [
    {:live_render, "~> 0.1"}
  ]
end
```

Optional dependencies unlock extra features:

| Dependency | Unlocks |
|---|---|
| `{:req_llm, "~> 1.6"}` | `LiveRender.Generate` — streaming/one-shot spec generation |
| `{:nimble_options, "~> 1.0"}` | Keyword list schema validation with defaults and coercion |
| `{:json_spec, "~> 1.1"}` | Elixir typespec-style schemas that compile to JSON Schema |

## Spec Format

```json
{
  "root": "card-1",
  "state": { "temperature": 72 },
  "elements": {
    "card-1": {
      "type": "card",
      "props": { "title": "Weather" },
      "children": ["metric-1"]
    },
    "metric-1": {
      "type": "metric",
      "props": {
        "label": "Temperature",
        "value": { "$state": "/temperature" }
      },
      "children": []
    }
  }
}
```

Each element has `type` (catalog component name), `props`, `children` (IDs), and optionally `visible` (conditions) and `on` (actions).

## Custom Components

```elixir
defmodule MyApp.AI.Components.PriceCard do
  use LiveRender.Component,
    name: "price_card",
    description: "Displays a price with currency formatting",
    schema: [
      label: [type: :string, required: true, doc: "Item name"],
      price: [type: :float, required: true, doc: "Price value"],
      currency: [type: {:in, [:usd, :eur, :gbp]}, default: :usd, doc: "Currency"]
    ]

  use Phoenix.Component

  def render(assigns) do
    ~H"""
    <div class="rounded-lg border p-4">
      <span class="text-gray-500"><%= @label %></span>
      <span class="text-2xl font-bold"><%= format(@price, @currency) %></span>
    </div>
    """
  end

  defp format(price, :usd), do: "$#{:erlang.float_to_binary(price, decimals: 2)}"
  defp format(price, :eur), do: "€#{:erlang.float_to_binary(price, decimals: 2)}"
  defp format(price, :gbp), do: "£#{:erlang.float_to_binary(price, decimals: 2)}"
end
```

Register it:

```elixir
defmodule MyApp.AI.Catalog do
  use LiveRender.Catalog

  component MyApp.AI.Components.PriceCard
  component LiveRender.Components.Card
  component LiveRender.Components.Stack
  # ...

  action :add_to_cart, description: "Add an item to the shopping cart"
end
```

Schemas support [NimbleOptions](https://hex.pm/packages/nimble_options) keyword lists, [JSONSpec](https://hex.pm/packages/json_spec) maps, or raw JSON Schema.

## Data Binding

Any prop value can be an expression resolved against the spec's `state`:

```json
{ "$state": "/user/name" }
{ "$cond": { "$state": "/loggedIn" }, "$then": "Welcome!", "$else": "Sign in" }
{ "$template": "Hello, ${/user/name}!" }
```

## Visibility Conditions

```json
{
  "type": "alert",
  "props": { "message": "Error occurred" },
  "visible": { "$state": "/hasError" }
}
```

Supports equality checks (`"eq"`), negation (`"not"`), and arrays (all must be truthy).

## Built-in Components

| Category | Components |
|---|---|
| **Layout** | Stack, Card, Grid, Separator |
| **Typography** | Heading, Text |
| **Data** | Metric, Badge, Table, Link |
| **Interactive** | Button, Tabs, TabContent |
| **Rich** | Callout, Timeline, Accordion, Progress, Alert |

Use them directly with `LiveRender.StandardCatalog`, or pick individual ones for your own catalog.

## Tools

With ReqLLM:

```elixir
LiveRender.Generate.stream_spec(model, prompt,
  catalog: MyApp.AI.Catalog,
  pid: self(),
  tools: [
    LiveRender.Tool.new!(
      name: "get_weather",
      description: "Get current weather",
      parameter_schema: [location: [type: :string, required: true, doc: "City"]],
      callback: &MyApp.Weather.fetch/1
    )
  ]
)
```

With Jido, define tools as `Jido.Action` modules for a full ReAct agent loop.

## Example App

The [`example/`](example/) directory contains a chat application porting Vercel's json-render chat example:

- Tools: weather, crypto, GitHub, Hacker News
- Jido ReAct agent with streaming
- PhoenixStreamdown for streaming markdown

```bash
cd example
cp .env.example .env  # add ANTHROPIC_API_KEY
mix setup
mix phx.server
```

## Development

```bash
mix ci  # compile, format, credo, dialyzer, ex_dna, test
```

## License

MIT — see [LICENSE](LICENSE).