Improve display of recommended questions using streaming (#2800)

This commit is contained in:
Samy Pessé
2025-01-31 23:13:40 +01:00
committed by GitHub
parent 6e54a06d9c
commit 9eca010f94
12 changed files with 66 additions and 63 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': minor
---
Improve the display of recommended questions by streaming them.
-4
View File
@@ -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
+5 -4
View File
@@ -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:*",
@@ -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",
@@ -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",
},
@@ -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"],
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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:*",
@@ -150,7 +150,7 @@ export default async function PDFHTMLOutput(props: {
<TrademarkLink
space={space}
customization={customization}
placement={SiteInsightsTrademarkPlacement.Footer}
placement={SiteInsightsTrademarkPlacement.Pdf}
/>
) : null
}
@@ -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;
@@ -147,7 +146,6 @@ function SearchModalBody(
) {
const {
pointer,
spaceId,
revisionId,
spaceTitle,
withAsk,
@@ -309,7 +307,6 @@ function SearchModalBody(
<SearchResults
ref={resultsRef}
pointer={pointer}
spaceId={spaceId}
revisionId={revisionId}
global={isMultiVariants && state.global}
query={state.query}
@@ -5,6 +5,7 @@ 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';
@@ -12,10 +13,10 @@ 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';
@@ -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.
@@ -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;
@@ -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();
@@ -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) {
@@ -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 }));
@@ -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) {
@@ -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,
@@ -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}
-16
View File
@@ -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
*/
+1 -1
View File
@@ -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": {