From 65fd3cc10168a228182542f71cf377db69898218 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:37:03 +0200 Subject: [PATCH] Gate AI features server-actions (#4353) --- .../[siteData]/~gitbook/embed/assistant/page.tsx | 4 ++-- .../[siteURL]/[siteData]/~gitbook/embed/page.tsx | 4 ++-- .../[siteData]/~gitbook/embed/assistant/page.tsx | 4 ++-- .../[siteURL]/[siteData]/~gitbook/embed/page.tsx | 4 ++-- .../gitbook/src/components/AI/server-actions/chat.ts | 8 +++++++- packages/gitbook/src/components/AI/useAI.tsx | 7 ++++--- .../AIChat/AskAITextSelection/AskAITextSelection.tsx | 4 ++-- .../DocumentView/CodeBlock/AskAICodeButton.tsx | 5 ++--- .../components/Embeddable/EmbeddableIframeAPI.tsx | 6 ++---- .../gitbook/src/components/Search/server-actions.tsx | 10 ++++++++++ .../src/components/SpaceLayout/SpaceLayout.tsx | 9 +++------ .../gitbook/src/components/utils/isAIChatEnabled.ts | 12 +++++++++--- packages/gitbook/src/routes/llms.ts | 5 +++++ packages/gitbook/src/routes/markdownAsk.ts | 5 +++++ packages/gitbook/src/routes/markdownPage.ts | 5 +++++ 15 files changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 59736ceef..274f55334 100644 --- a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,7 +1,7 @@ import type { RouteLayoutParams } from '@/app/utils'; import { EmbeddableAssistantPage } from '@/components/Embeddable'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableDynamicContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; type PageProps = { @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const { context } = await getEmbeddableDynamicContext(params); // If the assistant is not enabled, redirect to the docs - if (context.customization.ai.mode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(context.customization.ai.mode)) { redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`); } diff --git a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx index b847b5cf7..4abe02997 100644 --- a/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx +++ b/packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx @@ -1,6 +1,6 @@ import type { RouteLayoutParams } from '@/app/utils'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableDynamicContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; type PageProps = { @@ -13,7 +13,7 @@ export default async function Page(props: PageProps) { const baseURL = context.linker.toPathInSite('~gitbook/embed/'); // If assistant is enabled, redirect to assistant, otherwise to docs - if (context.customization.ai.mode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(context.customization.ai.mode)) { redirect(`${baseURL}/assistant`); } else { redirect(`${baseURL}/page/`); diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx index 9b8b40a76..b5e5cfa42 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/assistant/page.tsx @@ -1,7 +1,7 @@ import type { RouteParams } from '@/app/utils'; import { EmbeddableAssistantPage } from '@/components/Embeddable'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableStaticContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; export const dynamic = 'force-static'; @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const { context } = await getEmbeddableStaticContext(params); // If the assistant is not enabled, redirect to the docs - if (context.customization.ai.mode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(context.customization.ai.mode)) { redirect(`${context.linker.toPathInSite('~gitbook/embed/page/')}`); } diff --git a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx index 2f667d280..0198f23f2 100644 --- a/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx +++ b/packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx @@ -1,6 +1,6 @@ import type { RouteLayoutParams } from '@/app/utils'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getEmbeddableStaticContext } from '@/lib/embeddable'; -import { CustomizationAIMode } from '@gitbook/api'; import { redirect } from 'next/navigation'; export const dynamic = 'force-static'; @@ -15,7 +15,7 @@ export default async function Page(props: PageProps) { const baseURL = context.linker.toPathInSite('~gitbook/embed/'); // If assistant is enabled, redirect to assistant, otherwise to docs - if (context.customization.ai.mode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(context.customization.ai.mode)) { redirect(`${baseURL}/assistant`); } else { redirect(`${baseURL}/page/`); diff --git a/packages/gitbook/src/components/AI/server-actions/chat.ts b/packages/gitbook/src/components/AI/server-actions/chat.ts index c5f65b285..beca9500b 100644 --- a/packages/gitbook/src/components/AI/server-actions/chat.ts +++ b/packages/gitbook/src/components/AI/server-actions/chat.ts @@ -1,6 +1,7 @@ 'use server'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { getSiteURLDataFromMiddleware } from '@/lib/middleware'; -import { getServerActionBaseContext } from '@/lib/server-actions'; +import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions'; import { traceErrorOnly } from '@/lib/tracing'; import { type AIMessageContext, @@ -39,6 +40,11 @@ export async function* streamAIChatResponse({ isEmbeddable: options?.asEmbeddable, }); + const siteContext = await fetchServerActionSiteContext(context); + if (!isAIChatEnabled(siteContext.customization.ai.mode)) { + throw new Error('The AI Assistant is not enabled for this site.'); + } + const siteURLData = await getSiteURLDataFromMiddleware(); const api = await context.dataFetcher.api(); diff --git a/packages/gitbook/src/components/AI/useAI.tsx b/packages/gitbook/src/components/AI/useAI.tsx index 9c6c81e06..e8322c1a2 100644 --- a/packages/gitbook/src/components/AI/useAI.tsx +++ b/packages/gitbook/src/components/AI/useAI.tsx @@ -1,10 +1,11 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; +import type { CustomizationAIMode } from '@gitbook/api'; import { Icon, type IconName } from '@gitbook/icons'; import * as React from 'react'; import type { ReactNode } from 'react'; +import { isAIChatEnabled, isAISearchEnabled } from '@/components/utils/isAIChatEnabled'; import { tString, useLanguage } from '@/intl/client'; import type { GitBookAssistant } from '@gitbook/browser-types'; import { useAIChatController, useAIChatState } from '.'; @@ -89,7 +90,7 @@ export function useAI(): AIContext { const assistants: Assistant[] = []; - if (config.aiMode === CustomizationAIMode.Assistant) { + if (isAIChatEnabled(config.aiMode)) { assistants.push({ id: 'gitbook-assistant', label: config.assistantName ?? getAIChatName(language, config.trademark), @@ -110,7 +111,7 @@ export function useAI(): AIContext { ui: true, mode: 'sidebar', }); - } else if (config.aiMode === CustomizationAIMode.Search) { + } else if (isAISearchEnabled(config.aiMode)) { assistants.push({ id: 'gitbook-ai-search', label: tString(language, 'ai_chat_context_badge'), diff --git a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx index 94056b6ee..ee3d00909 100644 --- a/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx +++ b/packages/gitbook/src/components/AIChat/AskAITextSelection/AskAITextSelection.tsx @@ -1,6 +1,5 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; import fnv1a from '@sindresorhus/fnv1a'; import { AnimatePresence, motion } from 'motion/react'; import * as React from 'react'; @@ -10,6 +9,7 @@ import { useAIChatController, useAIConfig } from '@/components/AI'; import { useIsMobile } from '@/components/hooks/useIsMobile'; import { useIsMounted } from '@/components/hooks/useIsMounted'; import { Button } from '@/components/primitives'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { t, useLanguage } from '@/intl/client'; import { AIChatIcon } from '../AIChatIcon'; @@ -31,7 +31,7 @@ export function AskAITextSelection() { const isMobile = useIsMobile(); const isMounted = useIsMounted(); - const enabled = config.aiMode === CustomizationAIMode.Assistant && !isMobile; + const enabled = isAIChatEnabled(config.aiMode) && !isMobile; const toolbarRef = React.useRef(null); const { selection, clear } = useStableTextSelection({ diff --git a/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx b/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx index 10a784439..850988337 100644 --- a/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx +++ b/packages/gitbook/src/components/DocumentView/CodeBlock/AskAICodeButton.tsx @@ -1,10 +1,9 @@ 'use client'; -import { CustomizationAIMode } from '@gitbook/api'; - import { useAIChatController, useAIConfig } from '@/components/AI'; import { AIChatIcon } from '@/components/AIChat'; import { Button } from '@/components/primitives'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { t, useLanguage } from '@/intl/client'; import { type ClassValue, tcls } from '@/lib/tailwind'; @@ -22,7 +21,7 @@ export function AskAICodeButton(props: { const config = useAIConfig(); const chatController = useAIChatController(); - if (config.aiMode !== CustomizationAIMode.Assistant) { + if (!isAIChatEnabled(config.aiMode)) { return null; } diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx index f4e116e23..b2b612ad5 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableIframeAPI.tsx @@ -4,8 +4,8 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb import React, { useEffect, useRef } from 'react'; import { useAI, useAIChatController } from '@/components/AI'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { tString, useLanguage } from '@/intl/client'; -import { CustomizationAIMode } from '@gitbook/api'; import { useRouter } from 'next/navigation'; import { createStore, useStore } from 'zustand'; import { integrationsAssistantTools } from '../Integrations'; @@ -186,9 +186,7 @@ export function EmbeddableIframeTabs(props: { const router = useRouter(); const enabledTabs = [ - config.aiMode === CustomizationAIMode.Assistant && - assistants[0] && - tabs.includes('assistant') + isAIChatEnabled(config.aiMode) && assistants[0] && tabs.includes('assistant') ? { key: 'assistant', label: assistants[0].label, diff --git a/packages/gitbook/src/components/Search/server-actions.tsx b/packages/gitbook/src/components/Search/server-actions.tsx index 1dca529a2..41e065e06 100644 --- a/packages/gitbook/src/components/Search/server-actions.tsx +++ b/packages/gitbook/src/components/Search/server-actions.tsx @@ -1,5 +1,6 @@ 'use server'; +import { isAIEnabled, isAISearchEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { resolvePageId } from '@/lib/pages'; import { fetchServerActionSiteContext, getServerActionBaseContext } from '@/lib/server-actions'; @@ -55,6 +56,10 @@ export async function streamAskQuestion({ await getServerActionBaseContext({ isEmbeddable: asEmbeddable }) ); + if (!isAISearchEnabled(context.customization.ai.mode)) { + throw new Error('AI Search is not enabled for this site.'); + } + const apiClient = await context.dataFetcher.api(); const stream = apiClient.orgs.streamAskInSite( @@ -151,6 +156,11 @@ export async function streamRecommendedQuestions(args: { siteSpaceId?: string }) >(); (async () => { + const siteContext = await fetchServerActionSiteContext(context); + if (!isAIEnabled(siteContext.customization.ai.mode)) { + throw new Error('AI is not enabled for this site.'); + } + const apiClient = await context.dataFetcher.api(); const apiStream = apiClient.orgs.streamRecommendedQuestionsInSite( siteURLData.organization, diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 5c095579c..ae3e69456 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -1,14 +1,11 @@ import type { GitBookSiteContext } from '@/lib/context'; -import { - CustomizationAIMode, - CustomizationHeaderPreset, - CustomizationSearchStyle, -} from '@gitbook/api'; +import { CustomizationHeaderPreset, CustomizationSearchStyle } from '@gitbook/api'; import type React from 'react'; import { Footer } from '@/components/Footer'; import { Header, HeaderLogo } from '@/components/Header'; import { TableOfContents } from '@/components/TableOfContents'; +import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import type { VisitorAuthClaims } from '@/lib/adaptive'; import { GITBOOK_APP_URL } from '@/lib/env'; import { tcls } from '@/lib/tailwind'; @@ -128,7 +125,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
- {customization.ai?.mode === CustomizationAIMode.Assistant ? ( + {isAIChatEnabled(customization.ai?.mode) ? ( <> diff --git a/packages/gitbook/src/components/utils/isAIChatEnabled.ts b/packages/gitbook/src/components/utils/isAIChatEnabled.ts index c23b60181..6d0051fbb 100644 --- a/packages/gitbook/src/components/utils/isAIChatEnabled.ts +++ b/packages/gitbook/src/components/utils/isAIChatEnabled.ts @@ -1,5 +1,11 @@ -import type { GitBookSiteContext } from '@/lib/context'; import { CustomizationAIMode } from '@gitbook/api'; -export const isAIChatEnabled = (context: GitBookSiteContext) => - context.customization.ai.mode === CustomizationAIMode.Assistant; +// `ai.mode` is exclusive: Assistant (chat) and Search (ask) are distinct features; None means no AI. +export const isAIChatEnabled = (mode: CustomizationAIMode | undefined) => + mode === CustomizationAIMode.Assistant; + +export const isAISearchEnabled = (mode: CustomizationAIMode | undefined) => + mode === CustomizationAIMode.Search; + +export const isAIEnabled = (mode: CustomizationAIMode | undefined) => + isAIChatEnabled(mode) || isAISearchEnabled(mode); diff --git a/packages/gitbook/src/routes/llms.ts b/packages/gitbook/src/routes/llms.ts index d9c68b6cc..18faaea93 100644 --- a/packages/gitbook/src/routes/llms.ts +++ b/packages/gitbook/src/routes/llms.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import { type GitBookSiteContext, checkIsRootSiteContext } from '@/lib/context'; import { throwIfDataError } from '@/lib/data'; import { type GitBookLinker, linkerWithMarkdownPages } from '@/lib/links'; @@ -203,6 +204,10 @@ export async function getMarkdownForPagesTree( } function renderAskFooter(context: GitBookSiteContext) { + if (!isAIEnabled(context.customization.ai.mode)) { + return ''; + } + return `\n\n---\n\n# Agent Instructions This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com. diff --git a/packages/gitbook/src/routes/markdownAsk.ts b/packages/gitbook/src/routes/markdownAsk.ts index dca64d5f5..87508cc49 100644 --- a/packages/gitbook/src/routes/markdownAsk.ts +++ b/packages/gitbook/src/routes/markdownAsk.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { throwIfDataError } from '@/lib/data'; import { linkerWithMarkdownPages } from '@/lib/links'; @@ -27,6 +28,10 @@ export async function serveAskMarkdown( rawQuestion: string, options: ServeAskMarkdownOptions = {} ) { + if (!isAIEnabled(context.customization.ai.mode)) { + return new Response('Not Found', { status: 404 }); + } + return serveMarkdown(async () => { const question = rawQuestion.trim(); const goal = options.goal?.trim() || undefined; diff --git a/packages/gitbook/src/routes/markdownPage.ts b/packages/gitbook/src/routes/markdownPage.ts index 21070b231..039e80a50 100644 --- a/packages/gitbook/src/routes/markdownPage.ts +++ b/packages/gitbook/src/routes/markdownPage.ts @@ -1,3 +1,4 @@ +import { isAIEnabled } from '@/components/utils/isAIChatEnabled'; import type { GitBookSiteContext } from '@/lib/context'; import { getExposableError } from '@/lib/data'; import { linkerWithMarkdownPages } from '@/lib/links'; @@ -100,6 +101,10 @@ function renderAskFooter( context: GitBookSiteContext, pageLookup: ResolvedPagePath ) { + if (!isAIEnabled(context.customization.ai.mode)) { + return ''; + } + const pageUrl = context.linker.toAbsoluteURL( context.linker.toPathForPage({ page: pageLookup.page,