Skip to content

Commit 9eca010

Browse files
authored
Improve display of recommended questions using streaming (#2800)
1 parent 6e54a06 commit 9eca010

File tree

12 files changed

+66
-63
lines changed

12 files changed

+66
-63
lines changed

.changeset/pretty-flies-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': minor
3+
---
4+
5+
Improve the display of recommended questions by streaming them.

.github/workflows/ci.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,10 +192,6 @@ jobs:
192192
- deploy-v2-vercel
193193
- deploy-v2-cloudflare
194194
steps:
195-
- name: Checkout
196-
uses: actions/checkout@v4
197-
- name: Setup Bun
198-
uses: ./.github/composite/setup-bun
199195
- name: Find GitHub Comment
200196
uses: peter-evans/find-comment@v3
201197
id: fc

bun.lock

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"name": "gitbook",
3434
"version": "0.5.0",
3535
"dependencies": {
36-
"@gitbook/api": "^0.89.0",
36+
"@gitbook/api": "^0.90.0",
3737
"@gitbook/cache-do": "workspace:*",
3838
"@gitbook/emoji-codepoints": "workspace:*",
3939
"@gitbook/icons": "workspace:*",
@@ -117,8 +117,9 @@
117117
},
118118
"packages/gitbook-v2": {
119119
"name": "gitbook-v2",
120+
"version": "0.0.0",
120121
"dependencies": {
121-
"@gitbook/api": "^0.89.0",
122+
"@gitbook/api": "^0.90.0",
122123
"next": "canary",
123124
"react": "^19.0.0",
124125
"react-dom": "^19.0.0",
@@ -158,7 +159,7 @@
158159
"name": "@gitbook/react-contentkit",
159160
"version": "0.5.1",
160161
"dependencies": {
161-
"@gitbook/api": "^0.89.0",
162+
"@gitbook/api": "^0.90.0",
162163
"assert-never": "^1.2.1",
163164
"classnames": "^2.5.1",
164165
},
@@ -565,7 +566,7 @@
565566

566567
"@fortawesome/fontawesome-svg-core": ["@fortawesome/[email protected]", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
567568

568-
"@gitbook/api": ["@gitbook/api@0.89.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-htzPY5OrFrZz29ShhB535e/j0Z6BBRDmdc97qfprJKzUE4zTRv2L20ZZ0dYeSgNFTkSiVhsuv0sfd6njWfNk7w=="],
569+
"@gitbook/api": ["@gitbook/api@0.90.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-sLJj0JsC189a1PZ3a1LhtLDl0w7wkIBcWkhfoKNaz4gwoWg3cBBRt9wSqyK4nbshp0muRF1qFO3wA9vp+7LSdQ=="],
569570

570571
"@gitbook/cache-do": ["@gitbook/cache-do@workspace:packages/cache-do"],
571572

packages/gitbook-v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"next": "canary",
66
"react": "^19.0.0",
77
"react-dom": "^19.0.0",
8-
"@gitbook/api": "^0.89.0"
8+
"@gitbook/api": "^0.90.0"
99
},
1010
"devDependencies": {
1111
"@opennextjs/cloudflare": "^0.4.3"

packages/gitbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
1717
},
1818
"dependencies": {
19-
"@gitbook/api": "^0.89.0",
19+
"@gitbook/api": "^0.90.0",
2020
"@gitbook/cache-do": "workspace:*",
2121
"@gitbook/emoji-codepoints": "workspace:*",
2222
"@gitbook/icons": "workspace:*",

packages/gitbook/src/app/middleware/(space)/~gitbook/pdf/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export default async function PDFHTMLOutput(props: {
150150
<TrademarkLink
151151
space={space}
152152
customization={customization}
153-
placement={SiteInsightsTrademarkPlacement.Footer}
153+
placement={SiteInsightsTrademarkPlacement.Pdf}
154154
/>
155155
) : null
156156
}

packages/gitbook/src/components/Search/SearchModal.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { SearchState, UpdateSearchState, useSearch } from './useSearch';
1818
import { LoadingPane } from '../primitives/LoadingPane';
1919

2020
interface SearchModalProps {
21-
spaceId: string;
2221
revisionId: string;
2322
spaceTitle: string;
2423
isMultiVariants: boolean;
@@ -147,7 +146,6 @@ function SearchModalBody(
147146
) {
148147
const {
149148
pointer,
150-
spaceId,
151149
revisionId,
152150
spaceTitle,
153151
withAsk,
@@ -309,7 +307,6 @@ function SearchModalBody(
309307
<SearchResults
310308
ref={resultsRef}
311309
pointer={pointer}
312-
spaceId={spaceId}
313310
revisionId={revisionId}
314311
global={isMultiVariants && state.global}
315312
query={state.query}

packages/gitbook/src/components/Search/SearchResults.tsx

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@ import assertNever from 'assert-never';
55
import React from 'react';
66

77
import { t, useLanguage } from '@/intl/client';
8+
import { iterateStreamResponse } from '@/lib/actions';
89
import { SiteContentPointer } from '@/lib/api';
910
import { tcls } from '@/lib/tailwind';
1011

1112
import { SearchPageResultItem } from './SearchPageResultItem';
1213
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
1314
import { SearchSectionResultItem } from './SearchSectionResultItem';
1415
import {
15-
getRecommendedQuestions,
1616
OrderedComputedResult,
1717
searchSiteSpaceContent,
1818
searchAllSiteContent,
19+
streamRecommendedQuestions,
1920
} from './server-actions';
2021
import { useTrackEvent } from '../Insights';
2122
import { Loading } from '../primitives';
@@ -31,6 +32,12 @@ type ResultType =
3132
| { type: 'question'; id: string; query: string }
3233
| { type: 'recommended-question'; id: string; question: string };
3334

35+
/**
36+
* We cache the recommended questions globally to avoid calling the API multiple times
37+
* when re-opening the search modal.
38+
*/
39+
let cachedRecommendedQuestions: null | ResultType[] = null;
40+
3441
/**
3542
* Fetch the results of the keyboard navigable elements to display for a query:
3643
* - Recommended questions if no query is provided.
@@ -41,7 +48,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
4148
props: {
4249
children?: React.ReactNode;
4350
query: string;
44-
spaceId: string;
4551
revisionId: string;
4652
global: boolean;
4753
withAsk: boolean;
@@ -50,7 +56,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
5056
},
5157
ref: React.Ref<SearchResultsRef>,
5258
) {
53-
const { children, query, pointer, spaceId, revisionId, withAsk, global, onSwitchToAsk } = props;
59+
const { children, query, pointer, revisionId, withAsk, global, onSwitchToAsk } = props;
5460

5561
const language = useLanguage();
5662
const trackEvent = useTrackEvent();
@@ -60,7 +66,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
6066
}>({ results: [], fetching: true });
6167
const [cursor, setCursor] = React.useState<number | null>(null);
6268
const refs = React.useRef<(null | HTMLAnchorElement)[]>([]);
63-
const suggestedQuestionsRef = React.useRef<null | ResultType[]>(null);
6469

6570
React.useEffect(() => {
6671
if (!query) {
@@ -69,42 +74,50 @@ export const SearchResults = React.forwardRef(function SearchResults(
6974
return;
7075
}
7176

72-
if (suggestedQuestionsRef.current) {
73-
setResultsState({ results: suggestedQuestionsRef.current, fetching: false });
77+
if (cachedRecommendedQuestions) {
78+
setResultsState({ results: cachedRecommendedQuestions, fetching: false });
7479
return;
7580
}
7681

7782
let cancelled = false;
7883

7984
setResultsState({ results: [], fetching: true });
80-
getRecommendedQuestions(spaceId).then((questions) => {
81-
if (!questions) {
82-
if (!cancelled) {
83-
setResultsState({ results: [], fetching: false });
84-
}
85-
captureException(
86-
new Error(`corrupt-cache: getRecommendedQuestions is ${questions}`),
87-
);
88-
return;
89-
}
9085

91-
const results = questions.map((question) => ({
92-
type: 'recommended-question',
93-
id: question,
94-
question: question,
95-
})) satisfies ResultType[];
96-
97-
suggestedQuestionsRef.current = results;
86+
// We currently have a bug where the same question can be returned multiple times.
87+
// This is a workaround to avoid that.
88+
const questions = new Set<string>();
89+
const recommendedQuestions: ResultType[] = [];
9890

91+
const timeout = setTimeout(async () => {
9992
if (cancelled) {
10093
return;
10194
}
10295

103-
setResultsState({ results, fetching: false });
104-
});
96+
const response = streamRecommendedQuestions(pointer.organizationId, pointer.siteId);
97+
const stream = iterateStreamResponse(response);
98+
99+
for await (const { question } of stream) {
100+
if (questions.has(question)) {
101+
continue;
102+
}
103+
104+
questions.add(question);
105+
recommendedQuestions.push({
106+
type: 'recommended-question',
107+
id: question,
108+
question,
109+
});
110+
cachedRecommendedQuestions = recommendedQuestions;
111+
112+
if (!cancelled) {
113+
setResultsState({ results: [...recommendedQuestions], fetching: false });
114+
}
115+
}
116+
}, 100);
105117

106118
return () => {
107119
cancelled = true;
120+
clearTimeout(timeout);
108121
};
109122
} else {
110123
setResultsState((prev) => ({ results: prev.results, fetching: true }));
@@ -142,7 +155,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
142155
clearTimeout(timeout);
143156
};
144157
}
145-
}, [query, global, pointer, spaceId, revisionId, withAsk, trackEvent]);
158+
}, [query, global, pointer, revisionId, withAsk, trackEvent]);
146159

147160
const results: ResultType[] = React.useMemo(() => {
148161
if (!withAsk) {

packages/gitbook/src/components/Search/server-actions.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,20 @@ export const streamAskQuestion = streamResponse(async function* (
227227
});
228228

229229
/**
230-
* List suggested questions for a space.
230+
* Stream a list of suggested questions for the site.
231231
*/
232-
export async function getRecommendedQuestions(spaceId: string): Promise<string[]> {
233-
const data = await api.getRecommendedQuestionsInSpace(spaceId);
234-
return data.questions;
235-
}
232+
export const streamRecommendedQuestions = streamResponse(async function* (
233+
organizationId: string,
234+
siteId: string,
235+
) {
236+
const apiCtx = await api.api();
237+
const stream = apiCtx.client.orgs.streamRecommendedQuestionsInSite(organizationId, siteId);
238+
239+
for await (const chunk of stream) {
240+
console.log('got question', chunk);
241+
yield chunk;
242+
}
243+
});
236244

237245
async function transformAnswer(
238246
answer: SearchAIAnswer,

packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ export async function SpaceLayout(props: {
183183

184184
<React.Suspense fallback={null}>
185185
<SearchModal
186-
spaceId={contentTarget.spaceId}
187186
revisionId={contentTarget.revisionId}
188187
spaceTitle={customization.title ?? space.title}
189188
withAsk={customization.aiSearch.enabled}

0 commit comments

Comments
 (0)