# Create custom UIs using the Vercel AI SDK (/talk-to-your-agents/vercel-ai-sdk/use-chat)

Use the Vercel AI SDK's useChat hook to build React chat interfaces that stream responses from your agent.



## Overview

Agents built with Inkeep have an `/api/chat` [API endpoint](/talk-to-your-agents/chat-api) that is compatible with Vercel AI SDK's streaming protocol, so it works out of the box with Vercel's AI SDK ecosystem.

To build totally custom UIs, you have two options in the AI SDK ecosystem:

* Use `useChat` hook to build custom UIs in React, Svelte, Vue.js, and Angular. (**← this guide**)
* Use the [AI Elements](/talk-to-your-agents/vercel-ai-sdk/ai-elements) library for a shadcn-like chat primitives.

<Tip>
  If your agent uses tools that require user confirmation, the stream can include tool approval requests. See [Tool approvals](/typescript-sdk/tools/tool-approvals).
</Tip>

## Using `useChat`

Install the Vercel AI SDK in your React application:

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

## Basic Configuration

Here's a minimal example of using `useChat` with your agent:

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export default function Page() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "${process.env.NEXT_PUBLIC_INKEEP_AGENTS_API_URL}/run/api/chat", // 
      headers: {
        "x-inkeep-tenant-id": "default",
        "x-inkeep-project-id": "activities-planner",
        "x-inkeep-agent-id": "activities-planner",
      },
    }),
  });
  const [input, setInput] = useState("");

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="space-y-4 mb-4">
        {messages.map((message) => (
          <div key={message.id} className="border rounded p-3">
            <div className="font-semibold mb-2">
              {message.role === "user" ? "👤 User" : "🤖 Assistant"}
            </div>

            <div className="space-y-2">
              {message.parts.map((part, partIndex) => {
                const partKey = `${message.id}-${part.type}-${partIndex}`;

                if (part.type === "text") {
                  return (
                    <div key={partKey} className="whitespace-pre-wrap">
                      {part.text}
                    </div>
                  );
                }
                
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput("");
          }
        }}
        className="flex gap-2"
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask about weather, request confirmation, or ask for location..."
          className="flex-1 p-2 border rounded"
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Send
        </button>
      </form>

      <div className="mt-4 text-sm text-gray-600">
        <p>Try asking:</p>
        <ul className="list-disc list-inside space-y-1">
          <li>"What's the weather in New York?"</li>
          <li>"Can you get my location?"</li>
          <li>"Ask me to confirm something"</li>
        </ul>
      </div>
    </div>
  );
}
```

## Configuration Options

### API Endpoint

The `api` parameter should point to your Run API's chat endpoint:

* **Local development**: `http://localhost:3002/run/api/chat`
* **Production**: `https://your-run-api-domain.com/run/api/chat`

<>
  ## Authentication

  Choose the authentication method:

  <Tabs>
    <Tab title="Using an API Key">
      **Create an API Key:**

      1. Open the Visual Builder Dashboard
      2. Go to your Project → **API Keys**
      3. Click **Create**, select the target agent
      4. Copy the API key (it will be shown once) and store it securely

      **Request Header:**

      ```http
      Authorization: Bearer <api_key>
      ```
    </Tab>

    <Tab title="Without API key">
      When running the API server locally with `pnpm dev`, authentication is automatically bypassed. You can use headers in the request instead:

      **Request Headers:**

      ```http
      x-inkeep-tenant-id: <tenant-id>
      x-inkeep-project-id: <project-id>
      x-inkeep-agent-id: <agent-id>
      ```

      <Warning>
        This mode is for development only. Never use in production as it bypasses all security checks.
      </Warning>
    </Tab>
  </Tabs>

  See [Authentication → Run API](/api-reference/authentication/run-api) for more details.
</>

### Optional Configuration

```tsx
const { messages, sendMessage, status } = useChat({
  api: 'http://localhost:3002/run/api/chat',
  headers: { /* ... */ },

  // Handle completion
  onFinish: (message) => {
    console.log('Agent finished responding:', message);
  },

  // Handle errors
  onError: (error) => {
    console.error('Chat error:', error);
  },
});
```

## Headers

To pass required headers (validated against your Context Config), include them as custom headers:

```tsx
const { messages, sendMessage } = useChat({
  api: 'http://localhost:3002/run/api/chat',
  headers: {
    'Authorization': `Bearer ${apiKey}`,
    // Custom context headers
    'x-user-id': 'user-123',
    'x-organization-id': 'org-456',
  },
});
```

<Note>
  See [Headers](/typescript-sdk/headers) for details.
</Note>

## Complete Example

Here's a more complete example with conversation management and custom styling:

```tsx
"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { useState } from "react";

export default function Page() {
  const { messages, sendMessage } = useChat({
    transport: new DefaultChatTransport({
      api: "{process.env.NEXT_PUBLIC_INKEEP_AGENTS_API_URL}/run/api/chat",
      headers: {
        "x-inkeep-tenant-id": "default",
        "x-inkeep-project-id": "activities-planner",
        "x-inkeep-agent-id": "activities-planner",
        tz: "US/Pacific",
      },
    }),
  });
  const [input, setInput] = useState("");

  return (
    <div className="max-w-2xl mx-auto p-4">
      <div className="space-y-4 mb-4">
        {messages.map((message) => (
          <div key={message.id} className="border rounded p-3">
            <div className="font-semibold mb-2">
              {message.role === "user" ? "👤 User" : "🤖 Assistant"}
            </div>

            <div className="space-y-2">
              {message.parts.map((part, partIndex) => {
                const partKey = `${message.id}-${part.type}-${partIndex}`;

                if (part.type === "text") {
                  return (
                    <div key={partKey} className="whitespace-pre-wrap">
                      {part.text}
                    </div>
                  );
                }

                if (part.type?.startsWith("data-")) {
                  return (
                    <div
                      key={partKey}
                      className="bg-blue-50 p-3 rounded border-l-4 border-blue-400"
                    >
                      <div className="font-semibold text-blue-700">
                        Data: {JSON.stringify(part, null, 2)}
                      </div>
                    </div>
                  );
                }
                return null;
              })}
            </div>
          </div>
        ))}
      </div>

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput("");
          }
        }}
        className="flex gap-2"
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Ask about weather, request confirmation, or ask for location..."
          className="flex-1 p-2 border rounded"
        />
        <button
          type="submit"
          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
        >
          Send
        </button>
      </form>

      <div className="mt-4 text-sm text-gray-600">
        <p>Try asking:</p>
        <ul className="list-disc list-inside space-y-1">
          <li>"What's the weather in New York?"</li>
          <li>"Can you get my location?"</li>
          <li>"Ask me to confirm something"</li>
        </ul>
      </div>
    </div>
  );
}
```

## Environment Variables

For production deployments, store configuration in environment variables:

```bash
NEXT_PUBLIC_INKEEP_AGENTS_API_URL=your_domain_for_run_api
NEXT_PUBLIC_INKEEP_API_KEY=your-api-key
NEXT_PUBLIC_TENANT_ID=your-tenant-id
NEXT_PUBLIC_PROJECT_ID=your-project-id
NEXT_PUBLIC_AGENT_ID=your-agent-id
```

<Note>
  In local development, the Agents API runs at `http://localhost:3002`. For production, update this to your deployed API URL.
</Note>

## Next Steps

<Cards>
  <Card title="AI Elements" icon="LuPackage" href="/talk-to-your-agents/vercel-ai-sdk/ai-elements">
    Explore Vercel's prebuilt UI components library built on shadcn/ui for rapid development.
  </Card>

  <Card title="React UI Components" icon="LuLayers" href="/talk-to-your-agents/react/chat-button">
    Use prebuilt React components for a complete chat UI experience.
  </Card>

  <Card title="Via API" icon="LuNetwork" href="/talk-to-your-agents/chat-api">
    Learn about the underlying HTTP API and streaming protocol.
  </Card>
</Cards>