# Ask AI
URL: /docs/customization/ai-chat
LLM index: /llms.txt
Description: Add a RAG-powered AI chat to your documentation

# Ask AI

Use this page when the user asks about this topic: Add a RAG-powered AI chat to your documentation.
Keep answers grounded in the exact options, routes, commands, and examples documented here.
If the request moves beyond this page, point to the closest related docs instead of inventing config.

Add a built-in AI chat that lets users ask questions about your documentation. The AI searches relevant pages, builds context, and streams a response from any OpenAI-compatible LLM.

## Quick Start

```ts title="docs.config.ts"
ai: {
  enabled: true,
}
```

That's it. The AI reads your `OPENAI_API_KEY` environment variable and uses `gpt-4o-mini` by default.

<Tabs items={["Next.js", "TanStack Start", "SvelteKit", "Astro", "Nuxt"]}>
  <Tab value="Next.js">
    Add `OPENAI_API_KEY` to your `.env` file:

    ```bash title=".env"
    OPENAI_API_KEY=sk-...
    ```

    The key is automatically read from `process.env.OPENAI_API_KEY`.

  </Tab>
  <Tab value="TanStack Start">
    Add `OPENAI_API_KEY` to your `.env` file:

    ```bash title=".env"
    OPENAI_API_KEY=sk-...
    ```

    Pass it through `src/lib/docs.server.ts`:

    ```ts title="src/lib/docs.server.ts"
    import { createDocsServer } from "@farming-labs/tanstack-start/server";
    import docsConfig from "../../docs.config";

    export const docsServer = createDocsServer({
      ...docsConfig,
      rootDir: process.cwd(),
      ai: { apiKey: process.env.OPENAI_API_KEY, ...docsConfig.ai },
    });
    ```

  </Tab>
  <Tab value="SvelteKit">
    Add `OPENAI_API_KEY` to your `.env` file:

    ```bash title=".env"
    OPENAI_API_KEY=sk-...
    ```

    Pass it through `docs.server.ts` (SvelteKit requires server-only env access):

    ```ts title="src/lib/docs.server.ts"
    import { createDocsServer } from "@farming-labs/svelte/server";
    import { env } from "$env/dynamic/private";
    import config from "./docs.config";

    const contentFiles = import.meta.glob(
      ["/docs/**/*.{md,mdx,svx}", "/AGENTS.md", "/AGENT.md", "/skill.md", "/.farming-labs/sitemap-manifest.json"],
      {
        query: "?raw",
        import: "default",
        eager: true,
      },
    ) as Record<string, string>;

    export const { load, GET, POST } = createDocsServer({
      ...config,
      ai: { apiKey: env.OPENAI_API_KEY, ...config.ai },
      _preloadedContent: contentFiles,
    });
    ```

  </Tab>
  <Tab value="Astro">
    Add `OPENAI_API_KEY` to your `.env` file:

    ```bash title=".env"
    OPENAI_API_KEY=sk-...
    ```

    Pass it through `docs.server.ts`:

    ```ts title="src/lib/docs.server.ts"
    import { createDocsServer } from "@farming-labs/astro/server";
    import config from "./docs.config";

    const contentFiles = import.meta.glob(
      ["/docs/**/*.{md,mdx}", "/AGENTS.md", "/AGENT.md", "/skill.md", "/.farming-labs/sitemap-manifest.json"],
      {
        query: "?raw",
        import: "default",
        eager: true,
      },
    ) as Record<string, string>;

    export const { load, GET, POST } = createDocsServer({
      ...config,
      ai: { apiKey: import.meta.env.OPENAI_API_KEY, ...config.ai },
      _preloadedContent: contentFiles,
    });
    ```

  </Tab>
  <Tab value="Nuxt">
    Add `OPENAI_API_KEY` to your `.env` file:

    ```bash title=".env"
    OPENAI_API_KEY=sk-...
    ```

    Nuxt automatically reads environment variables via Nitro's runtime config. The `defineDocsHandler` reads `process.env.OPENAI_API_KEY` on the server.

    ```ts title="server/api/docs.ts"
    import { defineDocsHandler } from "@farming-labs/nuxt/server";
    import config from "../../docs.config";

    export default defineDocsHandler(config, useStorage);
    ```

  </Tab>
</Tabs>

## Configuration Reference

All options go inside the `ai` object in `docs.config.ts`:

```ts title="docs.config.ts"
export default defineDocs({
  ai: {
    // ... options
  },
});
```

### `enabled`

Whether to enable AI chat functionality.

| Type      | Default |
| --------- | ------- |
| `boolean` | `false` |

```ts 
ai: {
  enabled: true,
}
```

### `mode`

How the AI chat UI is presented.

| Type                     | Default    |
| ------------------------ | ---------- |
| `"search" \| "floating"` | `"search"` |

- **`"search"`** — AI tab integrated into the `Cmd+K` search dialog. Users switch between "Search" and "AI" tabs.
- **`"floating"`** — A floating chat widget with a button on screen. Opens as a panel, modal, or full-screen overlay.

```ts 
ai: {
  enabled: true,
  mode: "floating",
}
```

### `position`

Position of the floating chat button on screen. Only used when `mode` is `"floating"`.

| Type                                                 | Default          |
| ---------------------------------------------------- | ---------------- |
| `"bottom-right" \| "bottom-left" \| "bottom-center"` | `"bottom-right"` |

```ts 
ai: {
  enabled: true,
  mode: "floating",
  position: "bottom-left",
}
```

### `floatingStyle`

Visual style of the floating chat when opened. Only used when `mode` is `"floating"`.

| Type                                              | Default   |
| ------------------------------------------------- | --------- |
| `"panel" \| "modal" \| "popover" \| "full-modal"` | `"panel"` |

- **`"panel"`** — A tall panel that slides up from the button position. No backdrop overlay.
- **`"modal"`** — A centered modal dialog with a backdrop overlay, similar to the `Cmd+K` search dialog.
- **`"popover"`** — A compact popover near the button. Suitable for quick questions.
- **`"full-modal"`** — A full-screen immersive overlay. Messages scroll in the center, input is pinned at the bottom, suggested questions appear as horizontal pills.

```ts 
ai: {
  enabled: true,
  mode: "floating",
  floatingStyle: "full-modal",
}
```

### `model`

The LLM model configuration. Can be a simple string (single model) or an object with multiple selectable models.

**Simple — single model:**

| Type     | Default         |
| -------- | --------------- |
| `string` | `"gpt-4o-mini"` |

```ts 
ai: {
  enabled: true,
  model: "gpt-4o",
}
```

**Advanced — multiple models with UI dropdown:**

| Type     | Default |
| -------- | ------- |
| `object` | —       |

```ts 
ai: {
  enabled: true,
  model: {
    models: [
      { id: "gpt-4o-mini", label: "GPT-4o mini (fast)", provider: "openai" },
      { id: "gpt-4o", label: "GPT-4o (quality)", provider: "openai" },
      { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", provider: "groq" },
    ],
    defaultModel: "gpt-4o-mini",
  },
}
```

Each model entry has:
- **`id`** — The model identifier sent to the LLM API (e.g. `"gpt-4o-mini"`)
- **`label`** — Display name shown in the UI dropdown (e.g. `"GPT-4o mini (fast)"`)
- **`provider`** — (optional) Key matching a named provider in the `providers` config. If omitted, uses the default `baseUrl` and `apiKey`.

When `model` is an object with a `models` array, a model selector dropdown appears in the AI chat interface so users can pick which model to use.

### `providers`

Named provider configurations. Each provider has its own `baseUrl` and `apiKey`, allowing models from different providers to coexist in a single config.

| Type     | Default |
| -------- | ------- |
| `object` | —       |

```ts title="providers.ts"
ai: {
  enabled: true,
  providers: {
    openai: {
      baseUrl: "https://api.openai.com/v1",
      apiKey: process.env.OPENAI_API_KEY,
    },
    groq: {
      baseUrl: "https://api.groq.com/openai/v1",
      apiKey: process.env.GROQ_API_KEY,
    },
  },
  model: {
    models: [
      { id: "gpt-4o-mini", label: "GPT-4o mini", provider: "openai" },
      { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", provider: "groq" },
    ],
    defaultModel: "gpt-4o-mini",
  },
}
```

When a user selects a model in the dropdown, the backend automatically uses that model's provider to resolve the correct `baseUrl` and `apiKey`. All providers must be OpenAI Chat Completions API compatible (OpenAI, Groq, Together, Fireworks, OpenRouter, Ollama, any vLLM deployment).

### `baseUrl`

Default base URL for an OpenAI-compatible API endpoint. Used when no per-model `provider` is configured.

| Type     | Default                       |
| -------- | ----------------------------- |
| `string` | `"https://api.openai.com/v1"` |

```ts title="baseurl.ts"
ai: {
  enabled: true,
  model: "llama-3.1-70b-versatile",
  baseUrl: "https://api.groq.com/openai/v1",
}
```

### `apiKey`

Default API key for the LLM provider. Used when no per-model `provider` is configured. Falls back to `process.env.OPENAI_API_KEY` if not set.

| Type     | Default                      |
| -------- | ---------------------------- |
| `string` | `process.env.OPENAI_API_KEY` |

```ts title="apikey.ts"
ai: {
  enabled: true,
  apiKey: process.env.GROQ_API_KEY,
}
```

> **Warning:** Never hardcode API keys. Always use environment variables.

### `systemPrompt`

Custom system prompt prepended to the AI conversation. Documentation context is automatically appended after this prompt.

| Type     | Default                                          |
| -------- | ------------------------------------------------ |
| `string` | `"You are a helpful documentation assistant..."` |

```ts 
ai: {
  enabled: true,
  systemPrompt: "You are a friendly assistant for Acme Corp. Always mention our support email for complex issues.",
}
```

### `maxResults`

Maximum number of search results to include as context for the AI. More results = more context but higher token usage.

| Type     | Default |
| -------- | ------- |
| `number` | `5`     |

```ts title="maxresults.ts"
ai: {
  enabled: true,
  maxResults: 10,
}
```

### `useMcp`

Route Ask AI retrieval through the MCP server your docs site already exposes, without changing the
normal docs search API.

| Type | Default |
| ---- | ------- |
| `boolean \| DocsAskAIMcpConfig` | `false` |

```ts title="docs.config.ts"
export default defineDocs({
  ai: {
    enabled: true,
    useMcp: true,
  },
});
```

`useMcp: true` uses the built-in docs MCP server and calls its `search_docs` tool to retrieve Ask AI
context. By default that is the canonical `mcp.route` at `/api/docs/mcp`; if you configure
`mcp.route`, Ask AI uses that route automatically:

```ts title="docs.config.ts"
export default defineDocs({
  mcp: {
    route: "/custom/docs/mcp",
  },
  ai: {
    enabled: true,
    useMcp: true,
  },
});
```

Pass an object only when Ask AI should use a hosted or external MCP endpoint instead of this site's
own MCP route. MCP handles retrieval only; the `model`, `apiKey`, `baseUrl`, or `providers` config
still controls the LLM generation request.

```ts title="docs.config.ts"
export default defineDocs({
  ai: {
    enabled: true,
    model: "gpt-4o-mini",
    apiKey: process.env.OPENAI_API_KEY,
    useMcp: {
      endpoint: "https://docs.example.com/mcp",
      headers: {
        Authorization: `Bearer ${process.env.DOCS_MCP_TOKEN}`,
      },
    },
  },
});
```

If your top-level `search` config already uses `provider: "mcp"`, Ask AI follows that automatically.
Use `ai.useMcp` when only Ask AI should route retrieval through MCP.

`DocsAskAIMcpConfig` accepts the MCP search options (`endpoint`, `headers`, `toolName`,
`protocolVersion`, `maxResults`, and `enabled`). If MCP is disabled or `search_docs` is turned off,
Ask AI falls back to the normal top-level `search` config.

End-to-end, Ask AI does two server-side calls:

1. Retrieve docs context from `search_docs` on the configured MCP endpoint.
2. Send the prompt plus retrieved context to the configured OpenAI-compatible model endpoint.

So a custom MCP endpoint must support Streamable HTTP MCP, expose a search tool (default
`search_docs`), and return results with `url`, `content`, optional `description`, and optional
`section`. Stateful endpoints may return an `mcp-session-id`; stateless endpoints can omit it.

### Retrieval Quality

Ask AI uses the same configured docs search pipeline as the search API. If you configure simple
search, Typesense, Algolia, MCP search, or a custom search adapter, Ask AI uses those results before
building the model context.

The context passed to the model is hydrated from the local docs page or matching section, preserving
fenced code blocks so install commands, config snippets, and examples can be quoted accurately.

```ts title="docs.config.ts"
import { createCustomSearchAdapter } from "@farming-labs/docs";

export default defineDocs({
  search: createCustomSearchAdapter({
    name: "my-index",
    async search(query) {
      const results = await fetch("https://search.example.com/docs", {
        method: "POST",
        body: JSON.stringify(query),
      }).then((res) => res.json());

      return results;
    },
  }),
  ai: {
    enabled: true,
  },
});
```

### `feedback`

Completed Ask AI responses show copy, like, and dislike actions by default. Set `feedback: false`
to hide the action row.

| Type | Default |
| ---- | ------- |
| `boolean \| { enabled?: boolean; onFeedback?: (data) => void \| Promise<void> }` | `true` |

### `onActions`

Single callback for Ask AI response actions. Use `data.type` to handle `"copy"`, `"like"`, and
`"dislike"` from one place.

```ts title="docs.config.ts"
ai: {
  enabled: true,
  onActions(data) {
    if (data.type === "copy") {
      console.log("Copied AI response", data.answer);
    }

    if (data.type === "like" || data.type === "dislike") {
      console.log(data.type, data.question, data.answer, data.model);
    }
  },
}
```

The callback receives `type`, `question`, `answer`, `model`, `surface`, `url`, `path`, and the
visible chat messages up to that answer. Copy actions also include `copied`. Like/dislike actions
also include `value` for compatibility with `feedback.onFeedback`.

Like/dislike still dispatch the legacy `fd:ai-feedback` browser event and emit an `ai_feedback`
analytics event when analytics is enabled. All three actions dispatch `fd:ai-action`.

### `suggestedQuestions`

Pre-filled suggested questions shown in the AI chat when the conversation is empty. Clicking one fills the input and submits automatically.

| Type       | Default |
| ---------- | ------- |
| `string[]` | `[]`    |

```ts 
ai: {
  enabled: true,
  suggestedQuestions: [
    "How do I get started?",
    "What themes are available?",
    "How do I create a custom component?",
  ],
}
```

### `aiLabel`

Display name for the AI assistant in the chat UI. Shown as the message label and header title.

| Type     | Default |
| -------- | ------- |
| `string` | `"AI"`  |

```ts
ai: {
  enabled: true,
  aiLabel: "DocsBot",
}
```

### `packageName`

Optional package-name override for unusual docs where install/import examples do not mention the
main package clearly. Most projects should leave this unset: Ask AI infers package names, install
commands, and exact import lines from the retrieved docs context.

| Type     | Default |
| -------- | ------- |
| `string` | inferred from docs context |

```ts
ai: {
  enabled: true,
  packageName: "@farming-labs/docs",
}
```

### `docsUrl`

The public URL of your documentation site. The AI will use this for absolute links instead of relative paths.

| Type     | Default |
| -------- | ------- |
| `string` | —       |

```ts
ai: {
  enabled: true,
  docsUrl: "https://docs.farming-labs.dev",
}
```

### `loader`

Loading indicator variant shown while the AI generates a response.

| Type     | Default          |
| -------- | ---------------- |
| `string` | `"shimmer-dots"` |

Available variants: `"shimmer-dots"`, `"circular"`, `"dots"`, `"typing"`, `"wave"`, `"bars"`, `"pulse"`, `"pulse-dot"`, `"terminal"`, `"text-blink"`, `"text-shimmer"`, `"loading-dots"`.

```ts 
ai: {
  enabled: true,
  loader: "wave",
}
```

### `loadingComponent`

Custom React component that completely overrides the built-in `loader` variant. Receives `{ name }` (the `aiLabel` value). Only works in Next.js — for other frameworks, use the `loader` option.

| Type                                       | Default |
| ------------------------------------------ | ------- |
| `(props: { name: string }) => ReactNode`   | —       |

```tsx
ai: {
  enabled: true,
  aiLabel: "Sage",
  loadingComponent: ({ name }) => (
    <div className="flex items-center gap-2 text-sm text-zinc-400">
      <span className="animate-pulse">🤔</span>
      <span>{name} is thinking...</span>
    </div>
  ),
}
```

### `triggerComponent`

Custom trigger button for the floating chat. Replaces the default sparkles button. Only used when `mode` is `"floating"`. Each framework accepts its native component format — pass it as a prop on `DocsLayout` (or a slot in Astro).

| Type        | Default                  |
| ----------- | ------------------------ |
| `Component` | Built-in sparkles button |

<Tabs items={["Next.js", "SvelteKit", "Astro", "Nuxt"]}>
  <Tab value="Next.js">
    Pass a React component via `docs.config.tsx`:

    ```tsx title="docs.config.tsx"
    ai: {
      enabled: true,
      mode: "floating",
      triggerComponent: <button className="my-chat-btn">Ask AI</button>,
    }
    ```

  </Tab>
  <Tab value="SvelteKit">
    Import a Svelte component and pass it as a prop on `DocsLayout`:

    ```svelte title="src/routes/docs/+layout.svelte"
    <script>
      import { DocsLayout } from "@farming-labs/svelte-theme";
      import AskAITrigger from "$lib/components/AskAITrigger.svelte";
      import config from "../../lib/docs.config";
      let { data, children } = $props();
    </script>

    <DocsLayout tree={data.tree} {config} triggerComponent={AskAITrigger}>
      {@render children()}
    </DocsLayout>
    ```

  </Tab>
  <Tab value="Astro">
    Use the `trigger-component` slot on `DocsLayout`:

    ```astro title="src/pages/docs/[...slug].astro"
    ---
    import DocsLayout from "@farming-labs/astro-theme/src/components/DocsLayout.astro";
    import AskAITrigger from "../../components/AskAITrigger.astro";
    ---

    <DocsLayout tree={data.tree} config={config}>
      <AskAITrigger slot="trigger-component" />
      <DocsContent data={data} config={config} />
    </DocsLayout>
    ```

  </Tab>
  <Tab value="Nuxt">
    Import a Vue component and pass it as a prop on `DocsLayout`:

    ```vue title="pages/docs/[...slug].vue"
    <script setup lang="ts">
    import { DocsLayout, DocsContent } from "@farming-labs/nuxt-theme";
    import AskAITrigger from "~/components/AskAITrigger.vue";
    import config from "~/docs.config";

    const route = useRoute();
    const pathname = computed(() => route.path);
    const { data } = await useFetch("/api/docs", {
      query: { pathname }, watch: [pathname],
    });
    </script>

    <template>
      <DocsLayout :tree="data.tree" :config="config" :trigger-component="AskAITrigger">
        <DocsContent :data="data" :config="config" />
      </DocsLayout>
    </template>
    ```

  </Tab>
</Tabs>

## Full Example — Single Provider

```ts title="docs.config.ts"
export default defineDocs({
  ai: {
    enabled: true,
    mode: "floating",
    position: "bottom-right",
    floatingStyle: "full-modal",
    model: "gpt-4o-mini",
    aiLabel: "DocsBot",
    docsUrl: "https://docs.farming-labs.dev",
    maxResults: 5,
    suggestedQuestions: [
      "How do I get started?",
      "What themes are available?",
      "How do I configure the sidebar?",
      "How do I set up AI chat?",
    ],
  },
});
```

## Full Example — Multiple Providers

```ts title="docs.config.ts"
export default defineDocs({
  ai: {
    enabled: true,
    mode: "floating",
    position: "bottom-right",
    floatingStyle: "full-modal",
    providers: {
      openai: {
        baseUrl: "https://api.openai.com/v1",
        apiKey: process.env.OPENAI_API_KEY,
      },
      groq: {
        baseUrl: "https://api.groq.com/openai/v1",
        apiKey: process.env.GROQ_API_KEY,
      },
    },
    model: {
      models: [
        { id: "gpt-4o-mini", label: "GPT-4o mini (fast)", provider: "openai" },
        { id: "gpt-4o", label: "GPT-4o (quality)", provider: "openai" },
        { id: "llama-3.3-70b-versatile", label: "Llama 3.3 70B", provider: "groq" },
      ],
      defaultModel: "gpt-4o-mini",
    },
    aiLabel: "DocsBot",
    suggestedQuestions: [
      "How do I get started?",
      "What themes are available?",
    ],
  },
});
```

```bash title=".env"
OPENAI_API_KEY=sk-...
GROQ_API_KEY=gsk_...
```

Users see a model dropdown in the AI chat interface. When they pick a model, the backend automatically routes the request to the correct provider's API with the right credentials.

## Using a Different LLM Provider

### Single provider (simple)

Use any OpenAI-compatible API by setting `baseUrl` and `model`:

```ts title="docs.config.ts"
ai: {
  enabled: true,
  baseUrl: "https://api.groq.com/openai/v1",
  model: "llama-3.1-70b-versatile",
}
```

```bash title=".env"
OPENAI_API_KEY=gsk_...
```

### Multiple providers

Use the `providers` map to configure multiple APIs, then reference them from each model entry:

```ts title="docs.config.ts"
ai: {
  enabled: true,
  providers: {
    openai: {
      baseUrl: "https://api.openai.com/v1",
      apiKey: process.env.OPENAI_API_KEY,
    },
    together: {
      baseUrl: "https://api.together.xyz/v1",
      apiKey: process.env.TOGETHER_API_KEY,
    },
    ollama: {
      baseUrl: "http://localhost:11434/v1",
    },
  },
  model: {
    models: [
      { id: "gpt-4o-mini", label: "GPT-4o mini", provider: "openai" },
      { id: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Llama 3.3 70B", provider: "together" },
      { id: "llama3.2", label: "Llama 3.2 (local)", provider: "ollama" },
    ],
    defaultModel: "gpt-4o-mini",
  },
}
```

Compatible providers: OpenAI, Groq, Together AI, Fireworks, OpenRouter, Azure OpenAI, Ollama (local), any vLLM deployment — anything that speaks the OpenAI Chat Completions API format.