Skip to content

Commit ed684c1

Browse files
authored
Move AI Actions markdown fetching to client-side (#3459)
1 parent 52ab368 commit ed684c1

File tree

4 files changed

+52
-44
lines changed

4 files changed

+52
-44
lines changed

.changeset/three-baboons-prove.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Move AI Actions markdown fetching to client-side

packages/gitbook/src/components/AIActions/AIActions.tsx

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,22 +58,26 @@ export function OpenDocsAssistant(props: { type: AIActionType; trademark: boolea
5858
const useCopiedStore = create<{
5959
copied: boolean;
6060
setCopied: (copied: boolean) => void;
61+
loading: boolean;
62+
setLoading: (loading: boolean) => void;
6163
}>((set) => ({
6264
copied: false,
6365
setCopied: (copied: boolean) => set({ copied }),
66+
loading: false,
67+
setLoading: (loading: boolean) => set({ loading }),
6468
}));
6569

6670
/**
6771
* Copies the markdown version of the page to the clipboard.
6872
*/
6973
export function CopyMarkdown(props: {
70-
markdown: string;
74+
markdownPageUrl: string;
7175
type: AIActionType;
7276
isDefaultAction?: boolean;
7377
}) {
74-
const { markdown, type, isDefaultAction } = props;
78+
const { markdownPageUrl, type, isDefaultAction } = props;
7579
const language = useLanguage();
76-
const { copied, setCopied } = useCopiedStore();
80+
const { copied, setCopied, loading, setLoading } = useCopiedStore();
7781
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
7882

7983
// Close the dropdown menu manually after the copy button is clicked
@@ -88,6 +92,16 @@ export function CopyMarkdown(props: {
8892
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
8993
};
9094

95+
// Fetch the markdown from the page
96+
const fetchMarkdown = async () => {
97+
setLoading(true);
98+
99+
return fetch(markdownPageUrl)
100+
.then((res) => res.text())
101+
.finally(() => setLoading(false));
102+
};
103+
104+
// Reset the copied state when the component unmounts
91105
useEffect(() => {
92106
return () => {
93107
if (timeoutRef.current) {
@@ -96,21 +110,17 @@ export function CopyMarkdown(props: {
96110
};
97111
}, []);
98112

99-
const onClick = (e: React.MouseEvent) => {
113+
const onClick = async (e: React.MouseEvent) => {
100114
// Prevent default behavior for non-default actions to avoid closing the dropdown.
101115
// This allows showing transient UI (e.g., a "copied" state) inside the menu item.
102116
// Default action buttons are excluded from this behavior.
103117
if (!isDefaultAction) {
104118
e.preventDefault();
105119
}
106120

107-
// Cancel any pending timeout
108-
if (timeoutRef.current) {
109-
clearTimeout(timeoutRef.current);
110-
}
121+
const markdown = await fetchMarkdown();
111122

112123
navigator.clipboard.writeText(markdown);
113-
114124
setCopied(true);
115125

116126
// Reset the copied state after 2 seconds
@@ -132,6 +142,7 @@ export function CopyMarkdown(props: {
132142
shortLabel={copied ? tString(language, 'code_copied') : tString(language, 'code_copy')}
133143
description={tString(language, 'copy_page_markdown')}
134144
onClick={onClick}
145+
loading={loading}
135146
/>
136147
);
137148
}
@@ -149,7 +160,7 @@ export function ViewAsMarkdown(props: { markdownPageUrl: string; type: AIActionT
149160
icon={<MarkdownIcon className="size-4 fill-current" />}
150161
label={tString(language, 'view_page_markdown')}
151162
description={tString(language, 'view_page_plaintext')}
152-
href={`${markdownPageUrl}.md`}
163+
href={markdownPageUrl}
153164
/>
154165
);
155166
}
@@ -200,21 +211,24 @@ function AIActionWrapper(props: {
200211
description?: string;
201212
href?: string;
202213
disabled?: boolean;
214+
loading?: boolean;
203215
}) {
204-
const { type, icon, label, shortLabel, onClick, href, description, disabled } = props;
216+
const { type, icon, label, shortLabel, onClick, href, description, disabled, loading } = props;
205217

206218
if (type === 'button') {
207219
return (
208220
<Button
209-
icon={icon}
221+
icon={
222+
loading ? <Icon icon="spinner-third" className="size-4 animate-spin" /> : icon
223+
}
210224
size="xsmall"
211225
variant="secondary"
212226
label={shortLabel || label}
213227
className="hover:!scale-100 !shadow-none !rounded-r-none border-r-0 bg-tint-base text-sm"
214228
onClick={onClick}
215229
href={href}
216230
target={href ? '_blank' : undefined}
217-
disabled={disabled}
231+
disabled={disabled || loading}
218232
/>
219233
);
220234
}

packages/gitbook/src/components/AIActions/AIActionsDropdown.tsx

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import {
88
} from '@/components/AIActions/AIActions';
99
import { Button } from '@/components/primitives/Button';
1010
import { DropdownMenu } from '@/components/primitives/DropdownMenu';
11+
1112
import { Icon } from '@gitbook/icons';
1213
import { useRef } from 'react';
1314

1415
/**
1516
* Dropdown menu for the AI Actions (Ask Docs Assistant, Copy page, View as Markdown, Open in LLM).
1617
*/
1718
export function AIActionsDropdown(props: {
18-
markdown?: string;
1919
markdownPageUrl: string;
2020
/**
2121
* Whether to include the "Ask Docs Assistant" entry in the dropdown menu.
@@ -57,29 +57,26 @@ export function AIActionsDropdown(props: {
5757
* The content of the dropdown menu.
5858
*/
5959
function AIActionsDropdownMenuContent(props: {
60-
markdown?: string;
6160
markdownPageUrl: string;
6261
withAIChat?: boolean;
6362
pageURL: string;
6463
trademark: boolean;
6564
}) {
66-
const { markdown, markdownPageUrl, withAIChat, pageURL, trademark } = props;
65+
const { markdownPageUrl, withAIChat, pageURL, trademark } = props;
6766

6867
return (
6968
<>
7069
{withAIChat ? (
7170
<OpenDocsAssistant trademark={trademark} type="dropdown-menu-item" />
7271
) : null}
73-
{markdown ? (
74-
<>
75-
<CopyMarkdown
76-
markdown={markdown}
77-
isDefaultAction={!withAIChat}
78-
type="dropdown-menu-item"
79-
/>
80-
<ViewAsMarkdown markdownPageUrl={markdownPageUrl} type="dropdown-menu-item" />
81-
</>
82-
) : null}
72+
73+
<CopyMarkdown
74+
isDefaultAction={!withAIChat}
75+
markdownPageUrl={markdownPageUrl}
76+
type="dropdown-menu-item"
77+
/>
78+
<ViewAsMarkdown markdownPageUrl={markdownPageUrl} type="dropdown-menu-item" />
79+
8380
<OpenInLLM provider="chatgpt" url={pageURL} type="dropdown-menu-item" />
8481
<OpenInLLM provider="claude" url={pageURL} type="dropdown-menu-item" />
8582
</>
@@ -90,25 +87,21 @@ function AIActionsDropdownMenuContent(props: {
9087
* A default action shown as a quick-access button beside the dropdown menu
9188
*/
9289
function DefaultAction(props: {
93-
markdown?: string;
94-
withAIChat?: boolean;
95-
pageURL: string;
9690
markdownPageUrl: string;
91+
withAIChat?: boolean;
9792
trademark: boolean;
9893
}) {
99-
const { markdown, withAIChat, pageURL, markdownPageUrl, trademark } = props;
94+
const { markdownPageUrl, withAIChat, trademark } = props;
10095

10196
if (withAIChat) {
10297
return <OpenDocsAssistant trademark={trademark} type="button" />;
10398
}
10499

105-
if (markdown) {
106-
return <CopyMarkdown isDefaultAction={!withAIChat} markdown={markdown} type="button" />;
107-
}
108-
109-
if (markdownPageUrl) {
110-
return <ViewAsMarkdown markdownPageUrl={markdownPageUrl} type="button" />;
111-
}
112-
113-
return <OpenInLLM provider="chatgpt" url={pageURL} type="button" />;
100+
return (
101+
<CopyMarkdown
102+
isDefaultAction={!withAIChat}
103+
markdownPageUrl={markdownPageUrl}
104+
type="button"
105+
/>
106+
);
114107
}

packages/gitbook/src/components/PageBody/PageHeader.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { AIActionsDropdown } from '@/components/AIActions/AIActionsDropdown';
22
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
33
import type { GitBookSiteContext } from '@/lib/context';
4-
import { getMarkdownForPage } from '@/lib/markdownPage';
54
import type { AncestorRevisionPage } from '@/lib/pages';
65
import { tcls } from '@/lib/tailwind';
76
import type { RevisionPageDocument } from '@gitbook/api';
@@ -18,8 +17,6 @@ export async function PageHeader(props: {
1817
const { context, page, ancestors } = props;
1918
const { revision, linker } = context;
2019

21-
const markdownResult = await getMarkdownForPage(context, page.path);
22-
2320
if (!page.layout.title && !page.layout.description) {
2421
return null;
2522
}
@@ -46,8 +43,7 @@ export async function PageHeader(props: {
4643
)}
4744
>
4845
<AIActionsDropdown
49-
markdown={markdownResult.data}
50-
markdownPageUrl={context.linker.toPathInSpace(page.path)}
46+
markdownPageUrl={`${context.linker.toPathInSpace(page.path)}.md`}
5147
pageURL={context.linker.toAbsoluteURL(
5248
context.linker.toPathForPage({
5349
pages: context.revision.pages,

0 commit comments

Comments
 (0)