# Next.js (/docs/quickstart/nextjs)

This page follows Taamtaam, a Next.js site whose "AI mode" button lets visitors search its restaurant catalog with their own AI. The provider lives in a client component in the root layout, and the chat panel loads the first time a visitor opens it.

You need React 19 or later, and Chrome with AI Wallet installed and one account connected to try it.

## Install the packages [#install-the-packages]

```bash
npm install @aiconnect/core @aiconnect/react
```

<Callout type="warn">
The packages are not published on npm yet. Until they are, build a checkout of the AI Connect repository with `npm run build` and point your `package.json` at its folders with `file:` paths, such as `"@aiconnect/core": "file:../aiconnect/packages/core"`.
</Callout>

## Create the client provider [#create-the-client-provider]

`AiConnectProvider` uses React state and the browser, so it goes in a client component. Create the client with a lazy `useState` initializer so each page load builds one.

```tsx title="src/features/ai-mode/components/AiModeProvider.tsx"
'use client';

import { useState } from 'react';
import type { ReactNode } from 'react';
import { AiConnect } from '@aiconnect/core';
import { AiConnectProvider } from '@aiconnect/react';
import '@aiconnect/react/styles.css';

function createClient() {
  return new AiConnect();
}

export default function AiModeProvider({ children }: { children: ReactNode }) {
  const [client] = useState(createClient);
  return <AiConnectProvider client={client}>{children}</AiConnectProvider>;
}
```

Render it in the root layout, around the parts of the page that use the AI:

```tsx title="src/app/layout.tsx"
import type { ReactNode } from 'react';
import AiModeProvider from '@/features/ai-mode/components/AiModeProvider';

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <AiModeProvider>{children}</AiModeProvider>
      </body>
    </html>
  );
}
```

## The server render state [#the-server-render-state]

The server cannot see the visitor's wallet. The SDK handles this in two places:

- `new AiConnect()` checks for `window`. Built during the server render, it sends no request and stays in the `checking` state.
- `AiConnectProvider` reads the client through `useSyncExternalStore` with a server snapshot of `{ status: 'checking' }`. The server HTML and the hydration both render `checking`, and the provider switches to the browser's state right after.

Render your AI button in every state, so the layout does not move when the state arrives, and treat `checking` as a state that is not ready yet. A visitor whose wallet already approved your site goes from `checking` to `connected` without a click.

## Open the panel once the site is connected [#open-the-panel-once-the-site-is-connected]

The button below opens the chat panel for a connected visitor and the connect window for the others. When the connection the visitor asked for arrives, the panel opens on its own. `next/dynamic` with `ssr: false` keeps the panel, the chat and the MCP client out of the first page load and off the server.

```tsx title="src/features/ai-mode/components/AiModeButton.tsx"
'use client';

import { useCallback, useEffect, useRef, useState } from 'react';
import dynamic from 'next/dynamic';
import { useAiConnect } from '@aiconnect/react';

// The panel, the chat and the MCP client load on the first open only.
const ChatPanel = dynamic(() => import('./ChatPanel'), { ssr: false });

export default function AiModeButton() {
  const { client, openPicker } = useAiConnect();
  const [isPanelOpen, setPanelOpen] = useState(false);
  const panelAwaitsConnection = useRef(false);
  const closePanel = useCallback(() => setPanelOpen(false), []);

  const openAiMode = useCallback(() => {
    if (client.state.status === 'connected') {
      setPanelOpen(true);
      return;
    }
    panelAwaitsConnection.current = true;
    openPicker();
  }, [client, openPicker]);

  // The panel opens once the awaited connection arrives, and closes when the visitor disconnects.
  useEffect(() => client.subscribe((state) => {
    if (state.status === 'disconnected') {
      setPanelOpen(false);
    }
    if (state.status === 'connected' && panelAwaitsConnection.current) {
      panelAwaitsConnection.current = false;
      setPanelOpen(true);
    }
  }), [client]);

  return (
    <>
      <button type="button" onClick={openAiMode}>AI mode</button>
      {isPanelOpen ? <ChatPanel onClose={closePanel} /> : null}
    </>
  );
}
```

## Build the chat panel [#build-the-chat-panel]

Create the `McpToolSource` inside the panel with `useMemo`. A relative URL such as `/api/mcp` resolves against the page's address, which exists in the browser only, so the panel must not render on the server. The `ssr: false` import above takes care of that.

```tsx title="src/features/ai-mode/components/ChatPanel.tsx"
'use client';

import { useMemo, useState } from 'react';
import type { FormEvent } from 'react';
import { McpToolSource } from '@aiconnect/core';
import { useAiChat, useAiConnect } from '@aiconnect/react';

const SYSTEM_PROMPT = [
  'You are the restaurant guide of this site. Recommend only restaurants returned by the tools.',
  'Call list_filters once to learn the accepted values, then search_restaurants with the filters that match',
  'the visitor\'s request. When a search returns nothing, relax one filter and search again.',
].join(' ');

export default function ChatPanel({ onClose }: { onClose: () => void }) {
  const { client, state } = useAiConnect();
  const tools = useMemo(() => new McpToolSource('/api/mcp', 'MySite'), []);
  const { turns, send, busy, error } = useAiChat({ system: SYSTEM_PROMPT, tools });
  const [question, setQuestion] = useState('');

  const submit = (event: FormEvent) => {
    event.preventDefault();
    send(question);
    setQuestion('');
  };

  return (
    <section role="dialog" aria-label="AI mode">
      <button type="button" aria-label="Close AI mode" onClick={onClose}>×</button>
      {state.status === 'connected' ? (
        <p>
          Answered by your {state.providerName} account.{' '}
          <button type="button" onClick={() => client.disconnect()}>Disconnect</button>
        </p>
      ) : null}
      <ol>
        {turns.map((turn, index) => (
          <li key={index}>
            <p>{turn.question}</p>
            <p>{turn.answer}</p>
          </li>
        ))}
      </ol>
      {error ? <p role="alert">{error}</p> : null}
      <form onSubmit={submit}>
        <input value={question} onChange={(event) => setQuestion(event.target.value)} />
        <button type="submit" disabled={busy || !question.trim()}>Ask</button>
      </form>
    </section>
  );
}
```

## Serve your MCP server on your origin [#serve-your-mcp-server-on-your-origin]

The visitor's browser calls your MCP server, so the server has to answer the page's origin. The simplest setup is a Route Handler at `/api/mcp` that relays POST requests to your MCP server, which is what Taamtaam does. The [MCP guide](/docs/guides/mcp-tools#serve-it-from-your-site) has the relay and the server.

## Theme the connect window [#theme-the-connect-window]

Import a stylesheet that sets the `--aic-*` tokens after `@aiconnect/react/styles.css`, in the same client component:

```tsx
import '@aiconnect/react/styles.css';
import './aiConnectTheme.css';
```

The [theming guide](/docs/guides/theming) lists every token.

## Move an existing AI route into the page [#move-an-existing-ai-route-into-the-page]

A Next.js site often calls its model in a Route Handler, such as `app/api/chat/route.ts`, because its API key has to stay on the server. With AI Connect the visitor's wallet answers and no key exists, so the call moves into a client component and the route goes away. The site's data stays on the server behind its MCP server.

```tsx
'use client';

import { useMemo } from 'react';
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { useAiConnect } from '@aiconnect/react';

function useOpenAi() {
  const { client } = useAiConnect();
  return useMemo(() => createOpenAI({ apiKey: client.sdkOptions.apiKey, fetch: client.fetch }), [client]);
}

// In the chat panel:
const openai = useOpenAi();
const result = streamText({ model: openai('gpt-6-luna'), system: SYSTEM, prompt: question, tools });
```

The [AI SDK guide](/docs/guides/existing-sdks#move-a-call-from-your-server-into-the-page) shows the route before and after, and the change for the other SDKs.

## Run it [#run-it]

Start `next dev`, open the site on `localhost` in Chrome with AI Wallet installed, click **AI mode**, then **Connect AI Wallet**, and approve the site in the wallet's window. The panel opens once the wallet connects the site.
