> ## Documentation Index
> Fetch the complete documentation index at: https://www.mintlify.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Tutorial: Build an in-app documentation assistant

> Build and embed an in-app documentation assistant that answers user questions with cited information from your Mintlify documentation site.

## What you will build

A reusable widget that embeds the [assistant](/assistant/index) directly in your application. The widget provides:

* A floating button that opens a chat panel when clicked
* Real-time streaming responses based on information from your documentation
* Message rendering with Markdown support

Users can use the widget to get help with your product without leaving your application.

<Frame>
  <img src="https://mintcdn.com/mintlify/14gofcrk-ET1sjS3/images/assistant/assistant-embed-demo.gif?s=140b5b5da46b72b54045372b8a2759fc" alt="Demo of the assistant widget being opened and the user typing in How do I get started? Then the assistant responds." width="800" height="491" data-path="images/assistant/assistant-embed-demo.gif" />
</Frame>

## Prerequisites

* [Mintlify Pro or Enterprise plan](https://mintlify.com/pricing)
* Your domain name, which appears at the end of your dashboard URL. For example, if your dashboard URL is `https://dashboard.mintlify.com/org-name/domain-name`, your domain name is `domain-name`
* An [assistant API key](https://dashboard.mintlify.com/settings/organization/api-keys)
* Node.js v18 or higher and npm installed
* Basic React knowledge

### Get your assistant API key

1. Navigate to the [API keys](https://dashboard.mintlify.com/settings/organization/api-keys) page in your dashboard.
2. Click **Create Assistant API Key**.
3. Copy the assistant API key (starts with `mint_dsc_`) and save it securely.

<Note>
  The assistant API key is a public token that you can use in frontend code. Calls using this token count toward your plan's message allowance and can incur overages.
</Note>

## Set up the example

Clone the [example repository](https://github.com/mintlify/assistant-embed-example) and customize it for your needs.

<Steps>
  <Step title="Clone the repository">
    ```bash theme={null}
    git clone https://github.com/mintlify/assistant-embed-example.git
    cd assistant-embed-example
    ```
  </Step>

  <Step title="Choose your development tool">
    The repository includes Next.js and Vite examples. Choose the tool you prefer to use.

    <CodeGroup>
      ```bash title="Next.js" theme={null}
      cd nextjs
      npm install
      ```

      ```bash title="Vite" theme={null}
      cd vite
      npm install
      ```
    </CodeGroup>
  </Step>

  <Step title="Configure your project">
    Open `src/config.js` and update with your Mintlify project details.

    ```js src/config.js theme={null}
    export const ASSISTANT_CONFIG = {
      domain: 'your-domain',
      docsURL: 'https://yourdocs.mintlify.app',
    };
    ```

    Replace:

    * `your-domain` with your Mintlify project domain found at the end of your dashboard URL.
    * `https://yourdocs.mintlify.app` with your actual documentation URL.
  </Step>

  <Step title="Add your API token">
    Create a `.env` file in the project root.

    ```bash .env theme={null}
    VITE_MINTLIFY_TOKEN=mint_dsc_your_token_here
    ```

    Replace `mint_dsc_your_token_here` with your assistant API key.
  </Step>

  <Step title="Start the development server">
    ```bash theme={null}
    npm run dev
    ```

    Open your application in a browser and click the **Ask** button to open the assistant widget.
  </Step>
</Steps>

## Customization ideas

### Source citations

Extract and display sources from assistant responses:

```jsx theme={null}
const extractSources = (parts) => {
  return parts
    ?.filter(p => p.type === 'tool-invocation' && p.toolInvocation?.toolName === 'search')
    .flatMap(p => p.toolInvocation?.result || [])
    .map(source => ({
      url: source.url || source.path,
      title: source.metadata?.title || source.path,
    })) || [];
};

// In your message rendering:
{messages.map((message) => {
  const sources = message.role === 'assistant' ? extractSources(message.parts) : [];
  return (
    <div key={message.id}>
      {/* message content */}
      {sources.length > 0 && (
        <div className="mt-2 text-xs">
          <p className="font-semibold">Sources:</p>
          {sources.map((s, i) => (
            <a key={i} href={s.url} target="_blank" rel="noopener noreferrer" className="text-blue-600">
              {s.title}
            </a>
          ))}
        </div>
      )}
    </div>
  );
})}
```

### Track conversation threads

Store thread IDs and thread keys to maintain conversation history across sessions.

When a user creates a new conversation thread, the server returns two values in the response headers:

* `X-Thread-Id`: The thread identifier
* `X-Thread-Key`: A secret key for the thread (only returned once, when the thread is created)

You must capture and persist both values on the first response. On every subsequent message, include both `threadId` and `threadKey` in the request body. If you send a `threadId` without the corresponding `threadKey`, the server returns a `404` error.

<Warning>
  You must store the `X-Thread-Key` header immediately. The server only returns it when a new thread is created. You cannot retrieve it later.
</Warning>

```jsx theme={null}
import { useState, useEffect } from 'react';

export function AssistantWidget({ domain, docsURL }) {
  const [threadId, setThreadId] = useState(null);
  const [threadKey, setThreadKey] = useState(null);

  useEffect(() => {
    // Retrieve saved thread ID and key from localStorage
    const savedId = localStorage.getItem('assistant-thread-id');
    const savedKey = localStorage.getItem('assistant-thread-key');
    if (savedId && savedKey) {
      setThreadId(savedId);
      setThreadKey(savedKey);
    }
  }, []);

  const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
    api: `https://api.mintlify.com/discovery/v1/assistant/${domain}/message`,
    headers: {
      'Authorization': `Bearer ${import.meta.env.VITE_MINTLIFY_TOKEN}`,
    },
    body: {
      fp: 'anonymous',
      retrievalPageSize: 5,
      ...(threadId && { threadId }),
      ...(threadKey && { threadKey }),
    },
    streamProtocol: 'data',
    sendExtraMessageFields: true,
    fetch: async (url, options) => {
      const response = await fetch(url, options);
      const newThreadId = response.headers.get('x-thread-id');
      const newThreadKey = response.headers.get('x-thread-key');
      if (newThreadId) {
        setThreadId(newThreadId);
        localStorage.setItem('assistant-thread-id', newThreadId);
      }
      if (newThreadKey) {
        setThreadKey(newThreadKey);
        localStorage.setItem('assistant-thread-key', newThreadKey);
      }
      return response;
    },
  });

  // ... rest of component
}
```

### Add keyboard shortcuts

Allow users to open the widget and submit messages with keyboard shortcuts:

```jsx theme={null}
useEffect(() => {
  const handleKeyDown = (e) => {
    // Cmd/Ctrl + Shift + I to toggle widget
    if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'I') {
      e.preventDefault();
      setIsOpen((prev) => !prev);
    }

    // Enter (when widget is focused) to submit
    if (e.key === 'Enter' && !e.shiftKey && document.activeElement.id === 'assistant-input') {
      e.preventDefault();
      handleSubmit();
    }
  };

  window.addEventListener('keydown', handleKeyDown);
  return () => window.removeEventListener('keydown', handleKeyDown);
}, [handleSubmit]);
```


## Related topics

- [Guides](/docs/guides/index.md)
- [Assistant](/docs/assistant/index.md)
- [Workflows overview](/docs/workflows/index.md)
