mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Improve display of recommended questions using streaming (#2800)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': minor
|
||||
---
|
||||
|
||||
Improve the display of recommended questions by streaming them.
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user