Skip to content

Commit

Permalink
Improve display of recommended questions using streaming (#2800)
Browse files Browse the repository at this point in the history
  • Loading branch information
SamyPesse authored Jan 31, 2025
1 parent 6e54a06 commit 9eca010
Show file tree
Hide file tree
Showing 12 changed files with 66 additions and 63 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-flies-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'gitbook': minor
---

Improve the display of recommended questions by streaming them.
4 changes: 0 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,6 @@ jobs:
- deploy-v2-vercel
- deploy-v2-cloudflare
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: ./.github/composite/setup-bun
- name: Find GitHub Comment
uses: peter-evans/find-comment@v3
id: fc
Expand Down
9 changes: 5 additions & 4 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"name": "gitbook",
"version": "0.5.0",
"dependencies": {
"@gitbook/api": "^0.89.0",
"@gitbook/api": "^0.90.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
Expand Down Expand Up @@ -117,8 +117,9 @@
},
"packages/gitbook-v2": {
"name": "gitbook-v2",
"version": "0.0.0",
"dependencies": {
"@gitbook/api": "^0.89.0",
"@gitbook/api": "^0.90.0",
"next": "canary",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down Expand Up @@ -158,7 +159,7 @@
"name": "@gitbook/react-contentkit",
"version": "0.5.1",
"dependencies": {
"@gitbook/api": "^0.89.0",
"@gitbook/api": "^0.90.0",
"assert-never": "^1.2.1",
"classnames": "^2.5.1",
},
Expand Down Expand Up @@ -565,7 +566,7 @@

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

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

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

Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"next": "canary",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@gitbook/api": "^0.89.0"
"@gitbook/api": "^0.90.0"
},
"devDependencies": {
"@opennextjs/cloudflare": "^0.4.3"
Expand Down
2 changes: 1 addition & 1 deletion packages/gitbook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
},
"dependencies": {
"@gitbook/api": "^0.89.0",
"@gitbook/api": "^0.90.0",
"@gitbook/cache-do": "workspace:*",
"@gitbook/emoji-codepoints": "workspace:*",
"@gitbook/icons": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export default async function PDFHTMLOutput(props: {
<TrademarkLink
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Footer}
placement={SiteInsightsTrademarkPlacement.Pdf}
/>
) : null
}
Expand Down
3 changes: 0 additions & 3 deletions packages/gitbook/src/components/Search/SearchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { SearchState, UpdateSearchState, useSearch } from './useSearch';
import { LoadingPane } from '../primitives/LoadingPane';

interface SearchModalProps {
spaceId: string;
revisionId: string;
spaceTitle: string;
isMultiVariants: boolean;
Expand Down Expand Up @@ -147,7 +146,6 @@ function SearchModalBody(
) {
const {
pointer,
spaceId,
revisionId,
spaceTitle,
withAsk,
Expand Down Expand Up @@ -309,7 +307,6 @@ function SearchModalBody(
<SearchResults
ref={resultsRef}
pointer={pointer}
spaceId={spaceId}
revisionId={revisionId}
global={isMultiVariants && state.global}
query={state.query}
Expand Down
65 changes: 39 additions & 26 deletions packages/gitbook/src/components/Search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ import assertNever from 'assert-never';
import React from 'react';

import { t, useLanguage } from '@/intl/client';
import { iterateStreamResponse } from '@/lib/actions';
import { SiteContentPointer } from '@/lib/api';
import { tcls } from '@/lib/tailwind';

import { SearchPageResultItem } from './SearchPageResultItem';
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
import { SearchSectionResultItem } from './SearchSectionResultItem';
import {
getRecommendedQuestions,
OrderedComputedResult,
searchSiteSpaceContent,
searchAllSiteContent,
streamRecommendedQuestions,
} from './server-actions';
import { useTrackEvent } from '../Insights';
import { Loading } from '../primitives';
Expand All @@ -31,6 +32,12 @@ type ResultType =
| { type: 'question'; id: string; query: string }
| { type: 'recommended-question'; id: string; question: string };

/**
* We cache the recommended questions globally to avoid calling the API multiple times
* when re-opening the search modal.
*/
let cachedRecommendedQuestions: null | ResultType[] = null;

/**
* Fetch the results of the keyboard navigable elements to display for a query:
* - Recommended questions if no query is provided.
Expand All @@ -41,7 +48,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
props: {
children?: React.ReactNode;
query: string;
spaceId: string;
revisionId: string;
global: boolean;
withAsk: boolean;
Expand All @@ -50,7 +56,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
},
ref: React.Ref<SearchResultsRef>,
) {
const { children, query, pointer, spaceId, revisionId, withAsk, global, onSwitchToAsk } = props;
const { children, query, pointer, revisionId, withAsk, global, onSwitchToAsk } = props;

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

React.useEffect(() => {
if (!query) {
Expand All @@ -69,42 +74,50 @@ export const SearchResults = React.forwardRef(function SearchResults(
return;
}

if (suggestedQuestionsRef.current) {
setResultsState({ results: suggestedQuestionsRef.current, fetching: false });
if (cachedRecommendedQuestions) {
setResultsState({ results: cachedRecommendedQuestions, fetching: false });
return;
}

let cancelled = false;

setResultsState({ results: [], fetching: true });
getRecommendedQuestions(spaceId).then((questions) => {
if (!questions) {
if (!cancelled) {
setResultsState({ results: [], fetching: false });
}
captureException(
new Error(`corrupt-cache: getRecommendedQuestions is ${questions}`),
);
return;
}

const results = questions.map((question) => ({
type: 'recommended-question',
id: question,
question: question,
})) satisfies ResultType[];

suggestedQuestionsRef.current = results;
// We currently have a bug where the same question can be returned multiple times.
// This is a workaround to avoid that.
const questions = new Set<string>();
const recommendedQuestions: ResultType[] = [];

const timeout = setTimeout(async () => {
if (cancelled) {
return;
}

setResultsState({ results, fetching: false });
});
const response = streamRecommendedQuestions(pointer.organizationId, pointer.siteId);
const stream = iterateStreamResponse(response);

for await (const { question } of stream) {
if (questions.has(question)) {
continue;
}

questions.add(question);
recommendedQuestions.push({
type: 'recommended-question',
id: question,
question,
});
cachedRecommendedQuestions = recommendedQuestions;

if (!cancelled) {
setResultsState({ results: [...recommendedQuestions], fetching: false });
}
}
}, 100);

return () => {
cancelled = true;
clearTimeout(timeout);
};
} else {
setResultsState((prev) => ({ results: prev.results, fetching: true }));
Expand Down Expand Up @@ -142,7 +155,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
clearTimeout(timeout);
};
}
}, [query, global, pointer, spaceId, revisionId, withAsk, trackEvent]);
}, [query, global, pointer, revisionId, withAsk, trackEvent]);

const results: ResultType[] = React.useMemo(() => {
if (!withAsk) {
Expand Down
18 changes: 13 additions & 5 deletions packages/gitbook/src/components/Search/server-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,20 @@ export const streamAskQuestion = streamResponse(async function* (
});

/**
* List suggested questions for a space.
* Stream a list of suggested questions for the site.
*/
export async function getRecommendedQuestions(spaceId: string): Promise<string[]> {
const data = await api.getRecommendedQuestionsInSpace(spaceId);
return data.questions;
}
export const streamRecommendedQuestions = streamResponse(async function* (
organizationId: string,
siteId: string,
) {
const apiCtx = await api.api();
const stream = apiCtx.client.orgs.streamRecommendedQuestionsInSite(organizationId, siteId);

for await (const chunk of stream) {
console.log('got question', chunk);
yield chunk;
}
});

async function transformAnswer(
answer: SearchAIAnswer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,6 @@ export async function SpaceLayout(props: {

<React.Suspense fallback={null}>
<SearchModal
spaceId={contentTarget.spaceId}
revisionId={contentTarget.revisionId}
spaceTitle={customization.title ?? space.title}
withAsk={customization.aiSearch.enabled}
Expand Down
16 changes: 0 additions & 16 deletions packages/gitbook/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1101,22 +1101,6 @@ export const searchSiteContent = cache({
},
});

/**
* Get a list of recommended questions in a space.
*/
export const getRecommendedQuestionsInSpace = cache({
name: 'api.getRecommendedQuestionsInSpace',
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
get: async (spaceId: string, options: CacheFunctionOptions) => {
const apiCtx = await api();
const response = await apiCtx.client.spaces.getRecommendedQuestionsInSpace(spaceId, {
...noCacheFetchOptions,
signal: options.signal,
});
return cacheResponse(response);
},
});

/**
* Render an integration contentkit UI
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/react-contentkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"classnames": "^2.5.1",
"@gitbook/api": "^0.89.0",
"@gitbook/api": "^0.90.0",
"assert-never": "^1.2.1"
},
"peerDependencies": {
Expand Down

0 comments on commit 9eca010

Please sign in to comment.