diff --git a/packages/gitbook-v2/src/lib/data/api.ts b/packages/gitbook-v2/src/lib/data/api.ts
index 44313bfc57..9329734844 100644
--- a/packages/gitbook-v2/src/lib/data/api.ts
+++ b/packages/gitbook-v2/src/lib/data/api.ts
@@ -719,6 +719,8 @@ async function* streamAIResponse(
             input: params.input,
             output: params.output,
             model: params.model,
+            instructions: params.instructions,
+            previousResponseId: params.previousResponseId,
         },
         {
             ...noCacheFetchOptions,
diff --git a/packages/gitbook-v2/src/lib/data/types.ts b/packages/gitbook-v2/src/lib/data/types.ts
index 178a0ba77d..86d6b41306 100644
--- a/packages/gitbook-v2/src/lib/data/types.ts
+++ b/packages/gitbook-v2/src/lib/data/types.ts
@@ -189,5 +189,7 @@ export interface GitBookDataFetcher {
         input: api.AIMessageInput[];
         output: api.AIOutputFormat;
         model: api.AIModel;
+        instructions?: string;
+        previousResponseId?: string;
     }): AsyncGenerator<api.AIStreamResponse, void, unknown>;
 }
diff --git a/packages/gitbook/src/components/Ask/AskDialog.tsx b/packages/gitbook/src/components/Ask/AskDialog.tsx
new file mode 100644
index 0000000000..6319024342
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/AskDialog.tsx
@@ -0,0 +1,66 @@
+'use client';
+
+import { tcls } from '@/lib/tailwind';
+import { Button } from '../primitives';
+import { AskInput } from './AskInput';
+import { AskMessages } from './AskMessages';
+import { useAskController, useAskState } from './state';
+
+export function AskDialog() {
+    const state = useAskState();
+    const controller = useAskController();
+
+    if (!state.opened) {
+        return null;
+    }
+
+    return (
+        <div
+            role="dialog"
+            className={`ask-dialog ${tcls(
+                'z-40',
+                'fixed',
+                'top-3',
+                'right-3',
+                'bottom-3',
+                'w-[480px]',
+                'flex',
+                'flex-col',
+                'bg-tint-base',
+                'rounded-lg',
+                'straight-corners:rounded-sm',
+                'circular-corners:rounded-2xl',
+                'ring-1',
+                'ring-tint',
+                'shadow-2xl',
+                'depth-flat:shadow-none',
+                'overflow-hidden',
+                'dark:ring-inset',
+                'dark:ring-tint'
+            )})`}
+        >
+            <div className="flex flex-row">
+                <div className="flex-1"></div>
+                <div className="p-2">
+                    <Button
+                        variant="blank"
+                        icon="close"
+                        iconOnly
+                        onClick={() => {
+                            controller.close();
+                        }}
+                    />
+                </div>
+            </div>
+            <div className="flex-1">
+                <AskMessages session={state.session} />
+            </div>
+            <div className="flex flex-row">
+                <AskInput />
+            </div>
+            {/* <div>
+            <h1>{state.session.title}</h1>
+        </div> */}
+        </div>
+    );
+}
diff --git a/packages/gitbook/src/components/Ask/AskInput.tsx b/packages/gitbook/src/components/Ask/AskInput.tsx
new file mode 100644
index 0000000000..bf7b478f87
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/AskInput.tsx
@@ -0,0 +1,28 @@
+import { tcls } from '@/lib/tailwind';
+import React from 'react';
+import { useAskController } from './state';
+
+export function AskInput() {
+    const controller = useAskController();
+    const [value, setValue] = React.useState('');
+
+    return (
+        <textarea
+            className={tcls('resize-none', 'bg-tint-base', 'rounded-lg', 'm-2', 'p-2', 'flex-1')}
+            value={value}
+            placeholder="Ask a question..."
+            onChange={(event) => {
+                setValue(event.currentTarget.value);
+            }}
+            onKeyDown={(event) => {
+                if (event.key === 'Enter' && !event.shiftKey) {
+                    event.preventDefault();
+                    controller.postMessage({
+                        message: value,
+                    });
+                    setValue('');
+                }
+            }}
+        />
+    );
+}
diff --git a/packages/gitbook/src/components/Ask/AskMessages.tsx b/packages/gitbook/src/components/Ask/AskMessages.tsx
new file mode 100644
index 0000000000..296b8dfb53
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/AskMessages.tsx
@@ -0,0 +1,15 @@
+import type { AskSession } from './state';
+
+export function AskMessages(props: {
+    session: AskSession;
+}) {
+    const { session } = props;
+
+    return (
+        <div className="flex flex-col gap-2">
+            {session.messages.map((message, index) => {
+                return <div key={index}>{message.content}</div>;
+            })}
+        </div>
+    );
+}
diff --git a/packages/gitbook/src/components/Ask/index.ts b/packages/gitbook/src/components/Ask/index.ts
new file mode 100644
index 0000000000..d40d8f4469
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/index.ts
@@ -0,0 +1,2 @@
+export * from './AskDialog';
+export * from './state';
diff --git a/packages/gitbook/src/components/Ask/server-actions/AIMessageView.tsx b/packages/gitbook/src/components/Ask/server-actions/AIMessageView.tsx
new file mode 100644
index 0000000000..42479fbc5b
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/server-actions/AIMessageView.tsx
@@ -0,0 +1,31 @@
+import type { AIMessage } from '@gitbook/api';
+import { DocumentView } from '../../DocumentView';
+
+/**
+ * Render a message from the API backend.
+ */
+export function AIMessageView(props: {
+    message: AIMessage;
+}) {
+    const { message } = props;
+
+    return (
+        <div className="flex flex-col gap-2">
+            {message.steps.map((step, index) => {
+                return (
+                    <div key={index} className="flex flex-col gap-2">
+                        <DocumentView
+                            document={step.content}
+                            context={{
+                                mode: 'default',
+                                contentContext: undefined,
+                                wrapBlocksInSuspense: false,
+                            }}
+                            style={['space-y-5']}
+                        />
+                    </div>
+                );
+            })}
+        </div>
+    );
+}
diff --git a/packages/gitbook/src/components/Ask/server-actions/api.tsx b/packages/gitbook/src/components/Ask/server-actions/api.tsx
new file mode 100644
index 0000000000..d500d4c49f
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/server-actions/api.tsx
@@ -0,0 +1,214 @@
+'use server';
+import {
+    type AIMessage,
+    AIMessageRole,
+    type AIMessageStep,
+    type AIStreamResponse,
+} from '@gitbook/api';
+import type { GitBookBaseContext } from '@v2/lib/context';
+import type { GitBookDataFetcher } from '@v2/lib/data';
+import { EventIterator } from 'event-iterator';
+import type { MaybePromise } from 'p-map';
+import * as partialJson from 'partial-json';
+import type { DeepPartial } from 'ts-essentials';
+import type { z } from 'zod';
+import { zodToJsonSchema } from 'zod-to-json-schema';
+import { AIMessageView } from './AIMessageView';
+
+/**
+ * Get the latest value from a stream and the response id.
+ */
+export async function generate<T>(
+    promise: MaybePromise<{
+        stream: EventIterator<T>;
+        response: Promise<{ responseId: string }>;
+    }>
+) {
+    const input = await promise;
+    let value: T | undefined;
+
+    for await (const event of input.stream) {
+        value = event;
+    }
+
+    const { responseId } = await input.response;
+    return {
+        responseId,
+        value,
+    };
+}
+
+/**
+ * Stream the generation of an object using the AI.
+ */
+export async function streamGenerateObject<T>(
+    context: GitBookBaseContext,
+    {
+        schema,
+        ...input
+    }: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'> & {
+        schema: z.ZodSchema<T>;
+    }
+) {
+    const rawStream = context.dataFetcher.streamAIResponse({
+        ...input,
+        output: {
+            type: 'object',
+            schema: zodToJsonSchema(schema),
+        },
+    });
+
+    let json = '';
+    return parseResponse<DeepPartial<T>>(rawStream, (event) => {
+        if (event.type === 'response_object') {
+            json += event.jsonChunk;
+
+            const parsed = partialJson.parse(json, partialJson.ALL);
+            return parsed;
+        }
+    });
+}
+
+/**
+ * Stream the generation of a document.
+ */
+export async function streamGenerateDocument(
+    context: GitBookBaseContext,
+    input: Omit<Parameters<GitBookDataFetcher['streamAIResponse']>[0], 'output'>
+) {
+    const rawStream = context.dataFetcher.streamAIResponse({
+        ...input,
+        output: {
+            type: 'document',
+        },
+    });
+
+    const message: AIMessage = {
+        id: '',
+        role: AIMessageRole.Assistant,
+        steps: [],
+    };
+
+    const updateProcessingMessageStep = (
+        stepIndex: number,
+        callback: (step: AIMessageStep) => void
+    ) => {
+        if (stepIndex > message.steps.length) {
+            throw new Error(
+                `Step index out of bounds ${stepIndex} (${message.steps.length} steps)`
+            );
+        }
+
+        if (message.steps[stepIndex]) {
+            message.steps = [...message.steps];
+            message.steps[stepIndex] = { ...message.steps[stepIndex] };
+            callback(message.steps[stepIndex]);
+        } else {
+            message.steps = [
+                ...message.steps,
+                {
+                    content: {
+                        object: 'document',
+                        data: {},
+                        nodes: [],
+                    },
+                },
+            ];
+            callback(message.steps[stepIndex]);
+        }
+    };
+
+    return parseResponse<React.ReactNode>(rawStream, (event) => {
+        switch (event.type) {
+            /**
+             * The agent is processing a tool call in a new message.
+             */
+            case 'response_tool_call': {
+                updateProcessingMessageStep(event.stepIndex, (step) => {
+                    step.toolCalls ??= [];
+                    step.toolCalls.push(event.toolCall);
+                });
+                break;
+            }
+
+            /**
+             * The agent is writing the content of a new message.
+             */
+            case 'response_reasoning':
+            case 'response_document': {
+                updateProcessingMessageStep(event.stepIndex, (step) => {
+                    const container = event.type === 'response_reasoning' ? 'reasoning' : 'content';
+
+                    step[container] ??= {
+                        object: 'document',
+                        data: {},
+                        nodes: [],
+                    };
+                    step[container] = {
+                        ...step[container],
+                        nodes: [...step[container].nodes],
+                    };
+                    if (event.operation === 'insert') {
+                        step[container].nodes.push(...event.blocks);
+                    } else {
+                        step[container].nodes.splice(
+                            -event.blocks.length,
+                            event.blocks.length,
+                            ...event.blocks
+                        );
+                    }
+                });
+                break;
+            }
+        }
+
+        return <AIMessageView message={message} />;
+    });
+}
+
+/**
+ * Parse a stream from the API to extract the responseId.
+ */
+function parseResponse<T>(
+    responseStream: EventIterator<AIStreamResponse>,
+    parse: (response: AIStreamResponse) => T | undefined
+): {
+    stream: EventIterator<T>;
+    response: Promise<{ responseId: string }>;
+} {
+    let resolveResponse: (value: { responseId: string }) => void;
+    const response = new Promise<{ responseId: string }>((resolve) => {
+        resolveResponse = resolve;
+    });
+
+    const stream = new EventIterator<T>((queue) => {
+        (async () => {
+            let foundResponse = false;
+
+            for await (const event of responseStream) {
+                if (event.type === 'response_finish') {
+                    foundResponse = true;
+                    resolveResponse({ responseId: event.responseId });
+                } else {
+                    const parsed = parse(event);
+                    if (parsed !== undefined) {
+                        queue.push(parsed);
+                    }
+                }
+            }
+
+            if (!foundResponse) {
+                throw new Error('No response found');
+            }
+        })().then(
+            () => {
+                queue.stop();
+            },
+            (error) => {
+                queue.fail(error);
+            }
+        );
+    });
+
+    return { stream, response };
+}
diff --git a/packages/gitbook/src/components/Ask/server-actions/ask.ts b/packages/gitbook/src/components/Ask/server-actions/ask.ts
new file mode 100644
index 0000000000..02ad705ebc
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/server-actions/ask.ts
@@ -0,0 +1,44 @@
+'use server';
+import { getV1BaseContext } from '@/lib/v1';
+import { isV2 } from '@/lib/v2';
+import { AIMessageRole, AIModel } from '@gitbook/api';
+import { getSiteURLDataFromMiddleware } from '@v2/lib/middleware';
+import { getServerActionBaseContext } from '@v2/lib/server-actions';
+import { streamGenerateDocument } from './api';
+
+const PROMPT = `
+You are a helpful assistant that can answer questions about the content of the site.
+`;
+
+export async function* streamAsk({
+    query,
+    previousResponseId,
+}: {
+    query: string;
+    previousResponseId?: string;
+}) {
+    const baseContext = isV2() ? await getServerActionBaseContext() : await getV1BaseContext();
+    const siteURLData = await getSiteURLDataFromMiddleware();
+
+    const { response, stream } = await streamGenerateDocument(baseContext, {
+        organizationId: siteURLData.organization,
+        siteId: siteURLData.site,
+        model: AIModel.Fast,
+        instructions: PROMPT,
+        previousResponseId,
+        input: [
+            {
+                role: AIMessageRole.User,
+                content: query,
+            },
+        ],
+    });
+
+    for await (const output of stream) {
+        yield { output };
+    }
+
+    // Wait for the responseId to be available and yield one final time
+    const { responseId } = await response;
+    yield { responseId };
+}
diff --git a/packages/gitbook/src/components/Ask/server-actions/index.ts b/packages/gitbook/src/components/Ask/server-actions/index.ts
new file mode 100644
index 0000000000..ff3414980d
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/server-actions/index.ts
@@ -0,0 +1,2 @@
+export * from './api';
+export * from './ask';
diff --git a/packages/gitbook/src/components/Ask/state.tsx b/packages/gitbook/src/components/Ask/state.tsx
new file mode 100644
index 0000000000..d693d549d7
--- /dev/null
+++ b/packages/gitbook/src/components/Ask/state.tsx
@@ -0,0 +1,189 @@
+'use client';
+
+import { AIMessageRole } from '@gitbook/api';
+import * as React from 'react';
+import { streamAsk } from './server-actions';
+
+export type AskMessage = {
+    role: AIMessageRole;
+    content: React.ReactNode;
+};
+
+export type AskSession = {
+    /**
+     * The title of the ask session.
+     * It can be auto-generated by the AI or an initial value.
+     */
+    title: string;
+
+    /**
+     * ID of the latest AI response.
+     */
+    responseId: string | null;
+
+    /**
+     * Messages in the session.
+     */
+    messages: AskMessage[];
+
+    /**
+     * If true, the session is in progress.
+     */
+    loading: boolean;
+};
+
+export type AskState = {
+    opened: boolean;
+    session: AskSession;
+};
+
+export type AskController = {
+    /** Open the dialog */
+    open: () => void;
+
+    /** Close the dialog */
+    close: () => void;
+
+    /** Post a message to the session */
+    postMessage: (input: {
+        /** If defined, the title of the session will be updated. */
+        title?: string;
+        /** The message to post to the session. */
+        message: string;
+    }) => void;
+};
+
+const AskStateContext = React.createContext<AskState | null>(null);
+const AskControllerContext = React.createContext<AskController | null>(null);
+
+export function AskStateProvider(props: React.PropsWithChildren) {
+    const { children } = props;
+
+    const [state, setState] = React.useState<AskState>({
+        opened: false,
+        session: {
+            title: '',
+            responseId: null,
+            messages: [],
+            loading: false,
+        },
+    });
+
+    const stateRef = React.useRef<AskState>(state);
+    stateRef.current = state;
+
+    const controller = React.useMemo(() => {
+        return {
+            open: () => {
+                setState((previous) => {
+                    return {
+                        ...previous,
+                        opened: true,
+                    };
+                });
+            },
+            close: () => {
+                setState((previous) => {
+                    return {
+                        ...previous,
+                        opened: false,
+                    };
+                });
+            },
+            postMessage: async (input: { title?: string; message: string }) => {
+                try {
+                    const stream = await streamAsk({
+                        query: input.message,
+                        previousResponseId: stateRef.current.session.responseId ?? undefined,
+                    });
+
+                    setState((previous) => {
+                        return {
+                            ...previous,
+                            session: {
+                                ...previous.session,
+                                messages: [
+                                    ...previous.session.messages,
+                                    {
+                                        role: AIMessageRole.User,
+                                        content: input.message,
+                                    },
+                                    {
+                                        role: AIMessageRole.Assistant,
+                                        content: null,
+                                    },
+                                ],
+                            },
+                        };
+                    });
+
+                    for await (const data of stream) {
+                        if (!data) continue;
+
+                        if (data.responseId) {
+                            setState((previous) => {
+                                return {
+                                    ...previous,
+                                    session: {
+                                        ...previous.session,
+                                        responseId: data.responseId,
+                                    },
+                                };
+                            });
+                        }
+
+                        if (data.output) {
+                            setState((previous) => {
+                                return {
+                                    ...previous,
+                                    session: {
+                                        ...previous.session,
+                                        messages: [
+                                            ...previous.session.messages.slice(0, -1),
+                                            {
+                                                role: AIMessageRole.Assistant,
+                                                content: data.output,
+                                            },
+                                        ],
+                                    },
+                                };
+                            });
+                        }
+                    }
+                } catch (error) {
+                    console.error('Error in summary stream:', error);
+                }
+            },
+        };
+    }, []);
+
+    return (
+        <AskStateContext.Provider value={state}>
+            <AskControllerContext.Provider value={controller}>
+                {children}
+            </AskControllerContext.Provider>
+        </AskStateContext.Provider>
+    );
+}
+
+/**
+ * Get the current state of the ask session.
+ */
+export function useAskState(): AskState {
+    const context = React.useContext(AskStateContext);
+    if (!context) {
+        throw new Error('useAskState must be used within a AskStateProvider');
+    }
+    return context;
+}
+
+/**
+ * Get the controller to interact with the ask session.
+ */
+export function useAskController(): AskController {
+    const context = React.useContext(AskControllerContext);
+    if (!context) {
+        throw new Error('useAskController must be used within a AskStateProvider');
+    }
+    return context;
+}
diff --git a/packages/gitbook/src/components/PageAside/PageAside.tsx b/packages/gitbook/src/components/PageAside/PageAside.tsx
index 06821d964c..588848a8f3 100644
--- a/packages/gitbook/src/components/PageAside/PageAside.tsx
+++ b/packages/gitbook/src/components/PageAside/PageAside.tsx
@@ -48,6 +48,7 @@ export function PageAside(props: {
                 'group/aside',
                 'hidden',
                 'xl:flex',
+                'ask-open:hidden',
                 // 'page-no-toc:lg:flex',
                 'flex-col',
                 'basis-56',
diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
index ffaad1fff8..9e186f4c53 100644
--- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
+++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx
@@ -156,7 +156,9 @@ export async function CustomizationRootLayout(props: {
                     'theme-muted:bg-tint-subtle',
 
                     'theme-gradient:bg-gradient-primary',
-                    'theme-gradient-tint:bg-gradient-tint'
+                    'theme-gradient-tint:bg-gradient-tint',
+
+                    'ask-open:mr-[500px]'
                 )}
             >
                 <IconsProvider
diff --git a/packages/gitbook/src/components/Search/SearchButton.tsx b/packages/gitbook/src/components/Search/SearchButton.tsx
index 7bcc4f7481..c8f1c9b739 100644
--- a/packages/gitbook/src/components/Search/SearchButton.tsx
+++ b/packages/gitbook/src/components/Search/SearchButton.tsx
@@ -6,6 +6,7 @@ import { useEffect, useState } from 'react';
 import { tString, useLanguage } from '@/intl/client';
 import { type ClassValue, tcls } from '@/lib/tailwind';
 
+import { useAskController } from '../Ask';
 import { useTrackEvent } from '../Insights';
 import { useSearch } from './useSearch';
 
@@ -17,14 +18,19 @@ export function SearchButton(props: { children?: React.ReactNode; style?: ClassV
 
     const language = useLanguage();
     const [, setSearchState] = useSearch();
+    const askController = useAskController();
     const trackEvent = useTrackEvent();
 
     const onClick = () => {
-        setSearchState({
-            ask: false,
-            global: false,
-            query: '',
-        });
+        if (1) {
+            askController.open();
+        } else {
+            setSearchState({
+                ask: false,
+                global: false,
+                query: '',
+            });
+        }
 
         trackEvent({
             type: 'search_open',
diff --git a/packages/gitbook/src/components/Search/SearchModal.tsx b/packages/gitbook/src/components/Search/SearchModal.tsx
index c8046ca333..4d1cfb95f9 100644
--- a/packages/gitbook/src/components/Search/SearchModal.tsx
+++ b/packages/gitbook/src/components/Search/SearchModal.tsx
@@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
 import { tString, useLanguage } from '@/intl/client';
 import { tcls } from '@/lib/tailwind';
 
+import { useAskController } from '../Ask';
 import { LoadingPane } from '../primitives/LoadingPane';
 import { SearchAskAnswer } from './SearchAskAnswer';
 import { SearchAskProvider, useSearchAskState } from './SearchAskContext';
@@ -146,6 +147,7 @@ function SearchModalBody(
     const language = useLanguage();
     const resultsRef = React.useRef<SearchResultsRef>(null);
     const inputRef = React.useRef<HTMLInputElement>(null);
+    const askController = useAskController();
 
     React.useEffect(() => {
         inputRef.current?.focus();
@@ -186,7 +188,9 @@ function SearchModalBody(
     };
 
     const onSwitchToAsk = () => {
-        setSearchState((state) => (state ? { ...state, ask: true } : null));
+        alert('switch to ask');
+        askController.open();
+        // setSearchState((state) => (state ? { ...state, ask: true } : null));
     };
 
     // We trim the query to avoid invalidating the search when the user is typing between words.
diff --git a/packages/gitbook/src/components/Search/SearchResults.tsx b/packages/gitbook/src/components/Search/SearchResults.tsx
index 1eca294d48..c2425127fc 100644
--- a/packages/gitbook/src/components/Search/SearchResults.tsx
+++ b/packages/gitbook/src/components/Search/SearchResults.tsx
@@ -177,7 +177,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
             block: 'nearest',
             inline: 'nearest',
         });
-    }, [cursor, refs]);
+    }, [cursor]);
 
     const moveBy = React.useCallback(
         (delta: number) => {
@@ -198,7 +198,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
         }
 
         refs.current[cursor]?.click();
-    }, [cursor, refs]);
+    }, [cursor]);
 
     React.useImperativeHandle(
         ref,
diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
index 6b9aefd929..128a5d262a 100644
--- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
+++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
@@ -14,6 +14,7 @@ import { tcls } from '@/lib/tailwind';
 import type { VisitorAuthClaims } from '@/lib/adaptive';
 import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env';
 import { Announcement } from '../Announcement';
+import { AskDialog, AskStateProvider } from '../Ask';
 import { SpacesDropdown } from '../Header/SpacesDropdown';
 import { InsightsProvider } from '../Insights';
 import { SiteSectionList, encodeClientSiteSections } from '../SiteSections';
@@ -49,110 +50,116 @@ export function SpaceLayout(props: {
         customization.footer.groups?.length;
 
     return (
-        <SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
-            <InsightsProvider
-                enabled={withTracking}
-                appURL={GITBOOK_APP_URL}
-                apiHost={GITBOOK_API_PUBLIC_URL}
-                organizationId={context.organizationId}
-                siteId={context.site.id}
-                siteSectionId={context.sections?.current?.id ?? null}
-                siteSpaceId={context.siteSpace.id}
-                siteShareKey={context.shareKey ?? null}
-                revisionId={context.revisionId}
-                spaceId={context.space.id}
-                visitorAuthClaims={visitorAuthClaims}
-                visitorCookieTrackingEnabled={context.customization.insights?.trackingCookie}
-            >
-                <Announcement context={context} />
-                <Header withTopHeader={withTopHeader} context={context} />
-                <div className="scroll-nojump">
-                    <div
-                        className={tcls(
-                            'flex',
-                            'flex-col',
-                            'lg:flex-row',
-                            CONTAINER_STYLE,
+        <AskStateProvider>
+            <SpaceLayoutContextProvider basePath={context.linker.toPathInSpace('')}>
+                <InsightsProvider
+                    enabled={withTracking}
+                    appURL={GITBOOK_APP_URL}
+                    apiHost={GITBOOK_API_PUBLIC_URL}
+                    organizationId={context.organizationId}
+                    siteId={context.site.id}
+                    siteSectionId={context.sections?.current?.id ?? null}
+                    siteSpaceId={context.siteSpace.id}
+                    siteShareKey={context.shareKey ?? null}
+                    revisionId={context.revisionId}
+                    spaceId={context.space.id}
+                    visitorAuthClaims={visitorAuthClaims}
+                    visitorCookieTrackingEnabled={context.customization.insights?.trackingCookie}
+                >
+                    <Announcement context={context} />
+                    <Header withTopHeader={withTopHeader} context={context} />
+                    <div className="scroll-nojump">
+                        <div
+                            className={tcls(
+                                'flex',
+                                'flex-col',
+                                'lg:flex-row',
+                                CONTAINER_STYLE,
 
-                            // Ensure the footer is display below the viewport even if the content is not enough
-                            withFooter && 'min-h-[calc(100vh-64px)]',
-                            withTopHeader ? null : 'lg:min-h-screen'
-                        )}
-                    >
-                        <TableOfContents
-                            context={context}
-                            header={
-                                withTopHeader ? null : (
-                                    <div
-                                        className={tcls(
-                                            'hidden',
-                                            'pr-4',
-                                            'lg:flex',
-                                            'grow-0',
-                                            'flex-wrap',
-                                            'dark:shadow-light/1',
-                                            'text-base/tight'
-                                        )}
-                                    >
-                                        <HeaderLogo context={context} />
-                                    </div>
-                                )
-                            }
-                            innerHeader={
-                                // displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
-                                <>
-                                    {!withTopHeader && (
-                                        <div className={tcls('hidden', 'lg:block')}>
-                                            <React.Suspense fallback={null}>
-                                                <SearchButton>
-                                                    <span className={tcls('flex-1')}>
-                                                        {t(
-                                                            getSpaceLanguage(customization),
-                                                            customization.aiSearch.enabled
-                                                                ? 'search_or_ask'
-                                                                : 'search'
-                                                        )}
-                                                        ...
-                                                    </span>
-                                                </SearchButton>
-                                            </React.Suspense>
-                                        </div>
-                                    )}
-                                    {!withTopHeader && withSections && sections && (
-                                        <SiteSectionList
-                                            className={tcls('hidden', 'lg:block')}
-                                            sections={encodeClientSiteSections(context, sections)}
-                                        />
-                                    )}
-                                    {isMultiVariants && (
-                                        <SpacesDropdown
-                                            context={context}
-                                            siteSpace={siteSpace}
-                                            siteSpaces={siteSpaces}
+                                // Ensure the footer is display below the viewport even if the content is not enough
+                                withFooter && 'min-h-[calc(100vh-64px)]',
+                                withTopHeader ? null : 'lg:min-h-screen'
+                            )}
+                        >
+                            <TableOfContents
+                                context={context}
+                                header={
+                                    withTopHeader ? null : (
+                                        <div
                                             className={tcls(
-                                                'w-full',
-                                                'page-no-toc:hidden',
-                                                'site-header-none:page-no-toc:flex'
+                                                'hidden',
+                                                'pr-4',
+                                                'lg:flex',
+                                                'grow-0',
+                                                'flex-wrap',
+                                                'dark:shadow-light/1',
+                                                'text-base/tight'
                                             )}
-                                        />
-                                    )}
-                                </>
-                            }
-                        />
-                        <div className="flex min-w-0 flex-1 flex-col">{children}</div>
+                                        >
+                                            <HeaderLogo context={context} />
+                                        </div>
+                                    )
+                                }
+                                innerHeader={
+                                    // displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC.
+                                    <>
+                                        {!withTopHeader && (
+                                            <div className={tcls('hidden', 'lg:block')}>
+                                                <React.Suspense fallback={null}>
+                                                    <SearchButton>
+                                                        <span className={tcls('flex-1')}>
+                                                            {t(
+                                                                getSpaceLanguage(customization),
+                                                                customization.aiSearch.enabled
+                                                                    ? 'search_or_ask'
+                                                                    : 'search'
+                                                            )}
+                                                            ...
+                                                        </span>
+                                                    </SearchButton>
+                                                </React.Suspense>
+                                            </div>
+                                        )}
+                                        {!withTopHeader && withSections && sections && (
+                                            <SiteSectionList
+                                                className={tcls('hidden', 'lg:block')}
+                                                sections={encodeClientSiteSections(
+                                                    context,
+                                                    sections
+                                                )}
+                                            />
+                                        )}
+                                        {isMultiVariants && (
+                                            <SpacesDropdown
+                                                context={context}
+                                                siteSpace={siteSpace}
+                                                siteSpaces={siteSpaces}
+                                                className={tcls(
+                                                    'w-full',
+                                                    'page-no-toc:hidden',
+                                                    'site-header-none:page-no-toc:flex'
+                                                )}
+                                            />
+                                        )}
+                                    </>
+                                }
+                            />
+                            <div className="flex min-w-0 flex-1 flex-col">{children}</div>
+                        </div>
                     </div>
-                </div>
 
-                {withFooter ? <Footer context={context} /> : null}
+                    {withFooter ? <Footer context={context} /> : null}
 
-                <React.Suspense fallback={null}>
-                    <SearchModal
-                        spaceTitle={siteSpace.title}
-                        withAsk={customization.aiSearch.enabled}
-                        isMultiVariants={isMultiVariants}
-                    />
-                </React.Suspense>
-            </InsightsProvider>
-        </SpaceLayoutContextProvider>
+                    <React.Suspense fallback={null}>
+                        <SearchModal
+                            spaceTitle={siteSpace.title}
+                            withAsk={customization.aiSearch.enabled}
+                            isMultiVariants={isMultiVariants}
+                        />
+                        <AskDialog />
+                    </React.Suspense>
+                </InsightsProvider>
+            </SpaceLayoutContextProvider>
+        </AskStateProvider>
     );
 }
diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx
index 58697d5b7f..f6ea532c8a 100644
--- a/packages/gitbook/src/components/primitives/Button.tsx
+++ b/packages/gitbook/src/components/primitives/Button.tsx
@@ -7,17 +7,6 @@ import { type ClassValue, tcls } from '@/lib/tailwind';
 import { Icon, type IconName } from '@gitbook/icons';
 import { Link, type LinkInsightsProps } from './Link';
 
-type ButtonProps = {
-    href?: string;
-    variant?: 'primary' | 'secondary' | 'blank';
-    icon?: IconName;
-    iconOnly?: boolean;
-    size?: 'default' | 'medium' | 'small';
-    className?: ClassValue;
-    label?: string;
-} & LinkInsightsProps &
-    HTMLAttributes<HTMLElement>;
-
 const variantClasses = {
     primary: [
         'bg-primary-solid',
@@ -49,6 +38,17 @@ const variantClasses = {
     ],
 };
 
+type ButtonProps = {
+    href?: string;
+    variant?: keyof typeof variantClasses;
+    icon?: IconName;
+    iconOnly?: boolean;
+    size?: 'default' | 'medium' | 'small';
+    className?: ClassValue;
+    label?: string;
+} & LinkInsightsProps &
+    HTMLAttributes<HTMLElement>;
+
 export function Button({
     href,
     variant = 'primary',
@@ -62,8 +62,14 @@ export function Button({
     ...rest
 }: ButtonProps & { target?: HTMLAttributeAnchorTarget }) {
     const sizes = {
-        default: ['text-base', 'font-semibold', 'px-5', 'py-2', 'circular-corners:px-6'],
-        medium: ['text-sm', 'px-3.5', 'py-1.5', 'circular-corners:px-4'],
+        default: [
+            'text-base',
+            'font-semibold',
+            iconOnly ? 'px-2' : 'px-5',
+            'py-2',
+            'circular-corners:px-6',
+        ],
+        medium: ['text-sm', iconOnly ? 'px-1.5' : 'px-3.5', 'py-1.5', 'circular-corners:px-4'],
         small: ['text-xs', 'py-2', iconOnly ? 'px-2' : 'px-3'],
     };
 
diff --git a/packages/gitbook/tailwind.config.ts b/packages/gitbook/tailwind.config.ts
index 7981c337b2..3f8b08284a 100644
--- a/packages/gitbook/tailwind.config.ts
+++ b/packages/gitbook/tailwind.config.ts
@@ -533,6 +533,11 @@ const config: Config = {
              * Variant when the page is displayed in print mode.
              */
             addVariant('print-mode', 'body:has(.print-mode) &');
+
+            /**
+             * Variant when the Ask dialog is open.
+             */
+            addVariant('ask-open', 'html:has(.ask-dialog) &');
         }),
         plugin(({ matchUtilities }) => {
             matchUtilities({
