Home /

docs

Ask AI

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

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.

Add OPENAI_API_KEY to your .env file:

.env
OPENAI_API_KEY=sk-...

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

Add OPENAI_API_KEY to your .env file:

.env
OPENAI_API_KEY=sk-...

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

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 },
});

Add OPENAI_API_KEY to your .env file:

.env
OPENAI_API_KEY=sk-...

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

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,
});

Add OPENAI_API_KEY to your .env file:

.env
OPENAI_API_KEY=sk-...

Pass it through docs.server.ts:

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,
});

Add OPENAI_API_KEY to your .env file:

.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.

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

export default defineDocsHandler(config, useStorage);

Configuration Reference

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

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

enabled

Whether to enable AI chat functionality.

TypeDefault
booleanfalse
ai: {
  enabled: true,
}

mode

How the AI chat UI is presented.

TypeDefault
"search" | "floating""search"
ai: {
  enabled: true,
  mode: "floating",
}

position

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

TypeDefault
"bottom-right" | "bottom-left" | "bottom-center""bottom-right"
ai: {
  enabled: true,
  mode: "floating",
  position: "bottom-left",
}

floatingStyle

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

TypeDefault
"panel" | "modal" | "popover" | "full-modal""panel"
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:

TypeDefault
string"gpt-4o-mini"
ai: {
  enabled: true,
  model: "gpt-4o",
}

Advanced — multiple models with UI dropdown:

TypeDefault
object—
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:

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.

TypeDefault
object—
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.

TypeDefault
string"https://api.openai.com/v1"
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.

TypeDefault
stringprocess.env.OPENAI_API_KEY
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.

TypeDefault
string"You are a helpful documentation assistant..."
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.

TypeDefault
number5
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.

TypeDefault
boolean | DocsAskAIMcpConfigfalse
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:

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.

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.

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.

TypeDefault
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.

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.

TypeDefault
string[][]
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.

TypeDefault
string"AI"
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.

TypeDefault
stringinferred from docs context
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.

TypeDefault
string—
ai: {
  enabled: true,
  docsUrl: "https://docs.farming-labs.dev",
}

loader

Loading indicator variant shown while the AI generates a response.

TypeDefault
string"shimmer-dots"

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

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.

TypeDefault
(props: { name: string }) => ReactNode—
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).

TypeDefault
ComponentBuilt-in sparkles button

Pass a React component via docs.config.tsx:

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

Import a Svelte component and pass it as a prop on DocsLayout:

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>

Use the trigger-component slot on DocsLayout:

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>

Import a Vue component and pass it as a prop on DocsLayout:

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>

Full Example — Single Provider

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

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?",
    ],
  },
});
.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:

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

Multiple providers

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

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.