# AI SDK Integration

Use json-render with the [Vercel AI SDK](https://sdk.vercel.ai) for seamless streaming. json-render supports two modes: **Standalone** (standalone UI) and **Inline** (UI embedded in conversation). See [Generation Modes](/docs/generation-modes) for a detailed comparison.

## Installation

```bash
npm install ai @ai-sdk/react
```

## Standalone Mode

In standalone mode, the AI outputs only JSONL patches. The entire response is a UI spec with no prose. This is the default mode and is ideal for playgrounds, builders, and dashboard generators.

### API Route

```typescript
// app/api/generate/route.ts

  const { prompt, currentTree } = await req.json();

  const systemPrompt = catalog.prompt();

  // Optionally include current UI state for context
  const contextPrompt = currentTree
    ? `\n\nCurrent UI state:\n${JSON.stringify(currentTree, null, 2)}`
    : "";

  const result = streamText({
    model: yourModel,
    system: systemPrompt + contextPrompt,
    prompt,
  });

  return result.toTextStreamResponse();
}
```

### Client

Use `useUIStream` on the client to compile the JSONL stream into a spec:

```tsx
"use client";


function GenerativeUI() {
  const { spec, isStreaming, error, send } = useUIStream({
    api: "/api/generate",
  });

  return (
    <div>
      <button
        onClick={() => send("Create a dashboard with metrics")}
        disabled={isStreaming}
      >
        {isStreaming ? "Generating..." : "Generate"}
      </button>

      {error && <p className="text-red-500">{error.message}</p>}

      <Renderer spec={spec} registry={registry} loading={isStreaming} />
    </div>
  );
}
```

## Inline Mode

In inline mode, the AI responds conversationally and includes JSONL patches inline. Text-only replies are allowed when no UI is needed. This is ideal for chatbots, copilots, and educational assistants.

### API Route

Use `pipeJsonRender` to separate text from JSONL patches in the stream. Patches are emitted as data parts that the client can pick up.

```typescript
// app/api/chat/route.ts
  createUIMessageStream,
  createUIMessageStreamResponse,
} from "ai";

  const { messages } = await req.json();

  const result = streamText({
    model: yourModel,
    system: catalog.prompt({ mode: "inline" }),
    messages,
  });

  const stream = createUIMessageStream({
    execute: async ({ writer }) => {
      writer.merge(pipeJsonRender(result.toUIMessageStream()));
    },
  });

  return createUIMessageStreamResponse({ stream });
}
```

### Client

Use `useChat` from the AI SDK and `useJsonRenderMessage` from json-render to extract the spec from each message:

```tsx
"use client";


function Chat() {
  const { messages, input, handleInputChange, handleSubmit } = useChat({
    api: "/api/chat",
  });

  return (
    <div>
      <div>
        {messages.map((msg) => (
          <ChatMessage key={msg.id} message={msg} />
        ))}
      </div>

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Ask something..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

function ChatMessage({ message }: { message: { parts: Array<{ type: string; text?: string; data?: unknown }> } }) {
  const { spec, text, hasSpec } = useJsonRenderMessage(message.parts);

  return (
    <div>
      {text && <p>{text}</p>}
      {hasSpec && spec && (
        <Renderer spec={spec} registry={registry} />
      )}
    </div>
  );
}
```

**Preserving part order**

The example above is a message-level layout: `text` joins all text parts, and `spec` combines all spec data parts into one cumulative spec. It renders the joined text followed by the UI, not the original part order.

For mixed inline layouts, iterate `message.parts` instead. Render the latest merged `spec` once at the **first** `SPEC_DATA_PART_TYPE` position and skip subsequent spec parts. Individual spec parts may be patches, not standalone UI blocks. Later patches update that same UI in place, even when text or tools occur between them; this does not create a separate UI snapshot at each patch position.

```tsx

function OrderedChatMessage({
  message,
  renderPart,
}: {
  message: UIMessage;
  renderPart: (part: UIMessage["parts"][number]) => ReactNode;
}) {
  const { spec, hasSpec } = useJsonRenderMessage(message.parts);
  const firstSpecIndex = message.parts.findIndex(
    (part) => part.type === SPEC_DATA_PART_TYPE,
  );

  return (
    <div>
      {message.parts.map((part, index) => {
        if (part.type === SPEC_DATA_PART_TYPE) {
          if (index !== firstSpecIndex || !hasSpec || !spec) return null;
          return <Renderer key={index} spec={spec} registry={registry} />;
        }
        if (part.type === "text") {
          return <p key={index}>{part.text}</p>;
        }
        return <Fragment key={index}>{renderPart(part)}</Fragment>;
      })}
    </div>
  );
}
```

Here, `registry` is your component registry and `renderPart` is your application's renderer for tool, reasoning, source, and other parts (return `null` for parts you do not display). For `text → tool → text → spec patch → text → spec patch`, the layout is `text → tool → text → merged UI → text`. Do not also render the hook's aggregated `text`, or the text will appear twice.

During streaming, `hasSpec` can be false while the initial patches have not added any elements yet. The UI appears at the first spec-part position once `hasSpec` is true; text-only messages render without a UI block. See the [chat example's ordered segments](https://github.com/vercel-labs/json-render/blob/main/examples/chat/app/page.tsx) for a complete implementation that also groups adjacent text and tool parts.

## Prompt Engineering

The `catalog.prompt()` method creates an optimized system prompt that:

- Lists all available components and their props
- Describes available actions
- Specifies the expected output format (JSONL-only or text + JSONL depending on mode)
- Includes examples for better generation

### Custom Rules

Pass custom rules to tailor AI behavior:

```typescript
const systemPrompt = catalog.prompt({
  customRules: [
    "Always use Card components for grouping related content",
    "Prefer horizontal layouts (Row) for metrics",
    "Use consistent spacing with padding=\"md\"",
  ],
});
```

### Inline Mode Prompt

```typescript
const inlinePrompt = catalog.prompt({ mode: "inline" });
```

In inline mode, the prompt instructs the AI to respond conversationally first, then include JSONL patches on their own lines when UI is needed. Text-only replies are allowed.

## Which Mode?


Learn more in the [Generation Modes](/docs/generation-modes) guide.

## Next

- Learn about [progressive streaming](/docs/streaming)
- See the [chat example](https://github.com/vercel-labs/json-render/tree/main/examples/chat) for a complete implementation