mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Add recent search (and chat) queries (#4225)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import * as zustand from 'zustand';
|
||||
|
||||
import { useCurrentContent } from '@/components/hooks';
|
||||
import { useLanguage } from '@/intl/client';
|
||||
import { tString } from '@/intl/translate';
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import assertNever from 'assert-never';
|
||||
import * as React from 'react';
|
||||
import { getInsightsSession, useTrackEvent } from '../Insights';
|
||||
import { useSetSearchState } from '../Search';
|
||||
import { addRecentSearchQuery } from '../Search/recent-queries';
|
||||
import type { AnyAIControl } from './controls';
|
||||
import { ConfirmControlDef, ConfirmControlOutputSchema } from './controls/ConfirmControl';
|
||||
import { type AIChatReference, serializeReferences } from './references';
|
||||
@@ -187,6 +189,7 @@ export function AIChatProvider(props: {
|
||||
const messageContextRef = useAIMessageContextRef();
|
||||
const trackEvent = useTrackEvent();
|
||||
const setSearchState = useSetSearchState();
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const language = useLanguage();
|
||||
|
||||
// Event listeners storage
|
||||
@@ -491,6 +494,10 @@ export function AIChatProvider(props: {
|
||||
|
||||
// For first message, update the ask parameter in URL
|
||||
if (messages.length === 0) {
|
||||
if (siteSpaceId) {
|
||||
addRecentSearchQuery(siteSpaceId, input.message, 'ask');
|
||||
}
|
||||
|
||||
setSearchState((prev) => ({
|
||||
ask: input.message,
|
||||
query: prev?.query ?? null,
|
||||
@@ -537,7 +544,7 @@ export function AIChatProvider(props: {
|
||||
|
||||
streamResponse({ message: wireMessage, userQuery: input.message });
|
||||
},
|
||||
[setSearchState, trackEvent, streamResponse]
|
||||
[setSearchState, siteSpaceId, trackEvent, streamResponse]
|
||||
);
|
||||
|
||||
// Clear the conversation and reset ask parameter
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCurrentContent } from '@/components/hooks';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import type { AIChatController } from '../AI';
|
||||
import { useRecentSearchQueries } from '../Search/recent-queries';
|
||||
import { Button } from '../primitives';
|
||||
|
||||
export default function AIChatSuggestedQuestions(props: {
|
||||
@@ -7,16 +9,31 @@ export default function AIChatSuggestedQuestions(props: {
|
||||
suggestions?: string[];
|
||||
}) {
|
||||
const language = useLanguage();
|
||||
const { chatController, suggestions: _suggestions } = props;
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const recentQueries = useRecentSearchQueries(siteSpaceId ?? '');
|
||||
const { chatController, suggestions: configuredSuggestions } = props;
|
||||
|
||||
const suggestions =
|
||||
_suggestions && _suggestions.length > 0
|
||||
? _suggestions
|
||||
: [
|
||||
tString(language, 'ai_chat_suggested_questions_about_this_page'),
|
||||
tString(language, 'ai_chat_suggested_questions_read_next'),
|
||||
tString(language, 'ai_chat_suggested_questions_example'),
|
||||
];
|
||||
const defaultSuggestions = [
|
||||
tString(language, 'ai_chat_suggested_questions_about_this_page'),
|
||||
tString(language, 'ai_chat_suggested_questions_read_next'),
|
||||
tString(language, 'ai_chat_suggested_questions_example'),
|
||||
];
|
||||
const baseSuggestions =
|
||||
configuredSuggestions && configuredSuggestions.length > 0
|
||||
? configuredSuggestions
|
||||
: defaultSuggestions;
|
||||
|
||||
const suggestions = [
|
||||
...recentQueries.filter((entry) => entry.action === 'ask').map((entry) => entry.query),
|
||||
...baseSuggestions,
|
||||
].reduce<string[]>((acc, suggestion) => {
|
||||
if (acc.includes(suggestion)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc.push(suggestion);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { useCurrentContent } from '@/components/hooks';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
|
||||
import { SearchResultItem } from './SearchResultItem';
|
||||
import { addRecentSearchQuery } from './recent-queries';
|
||||
import { useSearchLink } from './useSearch';
|
||||
|
||||
/**
|
||||
@@ -19,6 +21,7 @@ export function SearchAskBar(props: {
|
||||
}) {
|
||||
const { query, assistant, active = false, withShortcut = false, onSelect } = props;
|
||||
const language = useLanguage();
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const getSearchLinkProps = useSearchLink();
|
||||
|
||||
const linkProps = getSearchLinkProps(
|
||||
@@ -28,6 +31,9 @@ export function SearchAskBar(props: {
|
||||
open: assistant.mode === 'search',
|
||||
},
|
||||
() => {
|
||||
if (assistant.mode === 'search' && siteSpaceId) {
|
||||
addRecentSearchQuery(siteSpaceId, query, 'ask');
|
||||
}
|
||||
onSelect?.();
|
||||
assistant.open(query);
|
||||
}
|
||||
|
||||
@@ -1,43 +1,57 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { Assistant } from '@/components/AI';
|
||||
import { useCurrentContent } from '@/components/hooks';
|
||||
import { tString, useLanguage } from '@/intl/client';
|
||||
import { SearchResultItem } from './SearchResultItem';
|
||||
import { addRecentSearchQuery } from './recent-queries';
|
||||
import { useSearchLink } from './useSearch';
|
||||
|
||||
export const SearchQuestionResultItem = React.forwardRef(function SearchQuestionResultItem(
|
||||
props: {
|
||||
question: string;
|
||||
action: 'ask' | 'search';
|
||||
active: boolean;
|
||||
recommended?: boolean;
|
||||
assistant: Assistant;
|
||||
style?: React.CSSProperties;
|
||||
},
|
||||
ref: React.Ref<HTMLAnchorElement>
|
||||
) {
|
||||
const { question, recommended = false, active, assistant, style, ...rest } = props;
|
||||
const { question, action, active, assistant, ...rest } = props;
|
||||
const language = useLanguage();
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const getLinkProp = useSearchLink();
|
||||
const shouldAsk = action === 'ask';
|
||||
|
||||
return (
|
||||
<SearchResultItem
|
||||
size="small"
|
||||
action={tString(language, 'ask', '')}
|
||||
action={tString(language, shouldAsk ? 'ask' : 'search')}
|
||||
ref={ref}
|
||||
data-testid="search-recommended-question"
|
||||
scroll={false}
|
||||
{...getLinkProp(
|
||||
{
|
||||
ask: question,
|
||||
query: null,
|
||||
open: assistant.mode === 'search',
|
||||
},
|
||||
() => {
|
||||
assistant.open(question);
|
||||
}
|
||||
shouldAsk
|
||||
? {
|
||||
ask: question,
|
||||
query: null,
|
||||
open: assistant.mode === 'search',
|
||||
}
|
||||
: {
|
||||
ask: null,
|
||||
query: question,
|
||||
open: true,
|
||||
},
|
||||
shouldAsk
|
||||
? () => {
|
||||
if (assistant.mode === 'search' && siteSpaceId) {
|
||||
addRecentSearchQuery(siteSpaceId, question, 'ask');
|
||||
}
|
||||
assistant.open(question);
|
||||
}
|
||||
: undefined
|
||||
)}
|
||||
active={active}
|
||||
leadingIcon="search"
|
||||
leadingIcon={shouldAsk ? assistant.icon : 'search'}
|
||||
{...rest}
|
||||
>
|
||||
{question}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
|
||||
import { useAI } from '@/components/AI';
|
||||
import { useCurrentContent } from '@/components/hooks';
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -13,18 +14,13 @@ import { SearchPageResultItem } from './SearchPageResultItem';
|
||||
import { SearchQuestionResultItem } from './SearchQuestionResultItem';
|
||||
import { SearchRecordResultItem } from './SearchRecordResultItem';
|
||||
import { SearchResultItem } from './SearchResultItem';
|
||||
import type { OrderedComputedResult } from './search-types';
|
||||
import type { LocalPageResult } from './useLocalSearchResults';
|
||||
import { addRecentSearchQuery } from './recent-queries';
|
||||
import type { ResultType } from './useSearchResults';
|
||||
|
||||
export interface SearchResultsRef {
|
||||
select(): boolean;
|
||||
}
|
||||
|
||||
type ResultType =
|
||||
| OrderedComputedResult
|
||||
| LocalPageResult
|
||||
| { type: 'recommended-question'; id: string; question: string };
|
||||
|
||||
function getResultKey(item: ResultType): string {
|
||||
switch (item.type) {
|
||||
case 'local-page':
|
||||
@@ -61,6 +57,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
const { children, id, query, results, fetching, cursor, error, onResultSelect } = props;
|
||||
|
||||
const language = useLanguage();
|
||||
const { siteSpaceId } = useCurrentContent();
|
||||
const shouldAnimateResults = !query || fetching;
|
||||
const previousCursor = React.useRef<number | null>(cursor);
|
||||
const seenResultKeys = React.useRef(new Set<string>());
|
||||
@@ -183,11 +180,24 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
const itemKey = getResultKey(item);
|
||||
const shouldAnimateItem =
|
||||
shouldAnimateResults || !seenResultKeys.current.has(itemKey);
|
||||
const handleResultSelect = () => {
|
||||
if (
|
||||
query &&
|
||||
siteSpaceId &&
|
||||
(item.type === 'local-page' ||
|
||||
item.type === 'page' ||
|
||||
item.type === 'record')
|
||||
) {
|
||||
addRecentSearchQuery(siteSpaceId, query, 'search');
|
||||
}
|
||||
|
||||
onResultSelect?.();
|
||||
};
|
||||
const resultItemProps = {
|
||||
'aria-posinset': index + 1,
|
||||
'aria-setsize': results.length,
|
||||
id: `${id}-${index}`,
|
||||
onClickCapture: () => onResultSelect?.(),
|
||||
onClickCapture: handleResultSelect,
|
||||
};
|
||||
switch (item.type) {
|
||||
case 'local-page':
|
||||
@@ -253,12 +263,9 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
refs.current[index] = ref;
|
||||
}}
|
||||
question={item.question}
|
||||
action={item.action}
|
||||
active={index === cursor}
|
||||
assistant={primaryAssistant}
|
||||
recommended
|
||||
style={{
|
||||
animationDelay: `${index * 25}ms,${100 + index * 25}ms`,
|
||||
}}
|
||||
{...resultItemProps}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { RecentSearchQueryEntry } from './recent-queries';
|
||||
|
||||
export type RecommendedQuestionResult = {
|
||||
type: 'recommended-question';
|
||||
id: string;
|
||||
question: string;
|
||||
action: 'ask' | 'search';
|
||||
};
|
||||
|
||||
export function createRecommendedQuestionResult(
|
||||
id: string,
|
||||
question: string,
|
||||
action: 'ask' | 'search' = 'ask'
|
||||
): RecommendedQuestionResult {
|
||||
return {
|
||||
type: 'recommended-question',
|
||||
id,
|
||||
question,
|
||||
action,
|
||||
};
|
||||
}
|
||||
|
||||
export function getEmptySearchResults(props: {
|
||||
withAI: boolean;
|
||||
recentQueries: RecentSearchQueryEntry[];
|
||||
recommendedQuestions: RecommendedQuestionResult[];
|
||||
}): RecommendedQuestionResult[] {
|
||||
const { withAI, recentQueries, recommendedQuestions } = props;
|
||||
|
||||
if (!withAI) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenQuestions = new Set<string>();
|
||||
const results: RecommendedQuestionResult[] = [];
|
||||
|
||||
for (const recentQuery of recentQueries) {
|
||||
if (seenQuestions.has(recentQuery.query)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenQuestions.add(recentQuery.query);
|
||||
results.push(
|
||||
createRecommendedQuestionResult(
|
||||
`recent-query-${recentQuery.action}-${recentQuery.query}`,
|
||||
recentQuery.query,
|
||||
recentQuery.action
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (const question of recommendedQuestions) {
|
||||
if (seenQuestions.has(question.question)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenQuestions.add(question.question);
|
||||
results.push(question);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getLocalStorageItem, setLocalStorageItem } from '@/lib/browser';
|
||||
|
||||
const STORAGE_KEY = '@gitbook/searchRecentQueries';
|
||||
const MAX_RECENT_QUERIES = 5;
|
||||
const EMPTY_RECENT_QUERIES: RecentSearchQueryEntry[] = [];
|
||||
|
||||
export type RecentSearchQueryAction = 'ask' | 'search';
|
||||
|
||||
export type RecentSearchQueryEntry = {
|
||||
query: string;
|
||||
action: RecentSearchQueryAction;
|
||||
};
|
||||
|
||||
type RecentQueriesState = Record<string, RecentSearchQueryEntry[]>;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
const EMPTY_RECENT_QUERIES_STATE: RecentQueriesState = {};
|
||||
|
||||
let globalRecentQueriesState = EMPTY_RECENT_QUERIES_STATE;
|
||||
let hasStorageListener = false;
|
||||
let hasLoadedRecentQueriesState = false;
|
||||
|
||||
function parseRecentSearchQueryEntry(value: unknown): RecentSearchQueryEntry | null {
|
||||
if (
|
||||
!value ||
|
||||
typeof value !== 'object' ||
|
||||
!('query' in value) ||
|
||||
typeof value.query !== 'string' ||
|
||||
!('action' in value) ||
|
||||
(value.action !== 'ask' && value.action !== 'search')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = value.query.trim();
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
query,
|
||||
action: value.action,
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeQueries(value: unknown): RecentSearchQueryEntry[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return EMPTY_RECENT_QUERIES;
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
|
||||
return value.reduce<RecentSearchQueryEntry[]>((acc, entry) => {
|
||||
const normalizedEntry = parseRecentSearchQueryEntry(entry);
|
||||
if (!normalizedEntry || seen.has(normalizedEntry.query)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
seen.add(normalizedEntry.query);
|
||||
if (acc.length < MAX_RECENT_QUERIES) {
|
||||
acc.push(normalizedEntry);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function sanitizeState(value: unknown): RecentQueriesState {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([siteSpaceId, queries]) => {
|
||||
if (typeof siteSpaceId !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sanitized = sanitizeQueries(queries);
|
||||
return sanitized.length > 0 ? [[siteSpaceId, sanitized]] : [];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function readRecentQueriesState(): RecentQueriesState {
|
||||
return sanitizeState(getLocalStorageItem<unknown>(STORAGE_KEY, {}));
|
||||
}
|
||||
|
||||
function ensureRecentQueriesStateLoaded() {
|
||||
if (typeof window === 'undefined' || hasLoadedRecentQueriesState) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalRecentQueriesState = readRecentQueriesState();
|
||||
hasLoadedRecentQueriesState = true;
|
||||
}
|
||||
|
||||
function emitChange() {
|
||||
listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
function ensureStorageListener() {
|
||||
if (typeof window === 'undefined' || hasStorageListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRecentQueriesStateLoaded();
|
||||
|
||||
window.addEventListener('storage', () => {
|
||||
globalRecentQueriesState = readRecentQueriesState();
|
||||
hasLoadedRecentQueriesState = true;
|
||||
emitChange();
|
||||
});
|
||||
hasStorageListener = true;
|
||||
}
|
||||
|
||||
function writeRecentQueriesState(nextState: RecentQueriesState) {
|
||||
globalRecentQueriesState = nextState;
|
||||
setLocalStorageItem(STORAGE_KEY, nextState);
|
||||
emitChange();
|
||||
}
|
||||
|
||||
export function addRecentSearchQuery(
|
||||
siteSpaceId: string,
|
||||
query: string,
|
||||
action: RecentSearchQueryAction
|
||||
) {
|
||||
const normalizedQuery = query.trim();
|
||||
if (!siteSpaceId || !normalizedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureRecentQueriesStateLoaded();
|
||||
|
||||
const nextQueries: RecentSearchQueryEntry[] = [
|
||||
{
|
||||
query: normalizedQuery,
|
||||
action,
|
||||
},
|
||||
...(globalRecentQueriesState[siteSpaceId] ?? []).filter(
|
||||
(existingQuery) => existingQuery.query !== normalizedQuery
|
||||
),
|
||||
].slice(0, MAX_RECENT_QUERIES);
|
||||
|
||||
writeRecentQueriesState({
|
||||
...globalRecentQueriesState,
|
||||
[siteSpaceId]: nextQueries,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRecentSearchQueries(siteSpaceId: string): RecentSearchQueryEntry[] {
|
||||
const subscribe = React.useCallback((listener: () => void) => {
|
||||
ensureStorageListener();
|
||||
listeners.add(listener);
|
||||
listener();
|
||||
return () => listeners.delete(listener);
|
||||
}, []);
|
||||
|
||||
const getSnapshot = React.useCallback(
|
||||
() => globalRecentQueriesState[siteSpaceId] ?? EMPTY_RECENT_QUERIES,
|
||||
[siteSpaceId]
|
||||
);
|
||||
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, () => EMPTY_RECENT_QUERIES);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useAI } from '../AI';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { useBodyLoaded } from '../primitives';
|
||||
import type { SearchResultsRef } from './SearchResults';
|
||||
import { addRecentSearchQuery } from './recent-queries';
|
||||
import type { SearchBaseProps } from './search-props';
|
||||
import { useSearchState, useSetSearchState } from './useSearch';
|
||||
import { useSearchResults } from './useSearchResults';
|
||||
@@ -220,6 +221,10 @@ export function useSearchController(props: SearchBaseProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (assistant.mode === 'search') {
|
||||
addRecentSearchQuery(siteSpace.id, normalizedQuery, 'ask');
|
||||
}
|
||||
|
||||
abort();
|
||||
assistant.open(normalizedQuery);
|
||||
setSearchState({
|
||||
@@ -229,7 +234,7 @@ export function useSearchController(props: SearchBaseProps) {
|
||||
open: assistant.mode === 'search',
|
||||
});
|
||||
},
|
||||
[abort, assistants, normalizedQuery, setSearchState, state?.scope]
|
||||
[abort, assistants, normalizedQuery, setSearchState, siteSpace.id, state?.scope]
|
||||
);
|
||||
|
||||
const askCount = normalizedQuery && !showAsk ? assistants.length : 0;
|
||||
|
||||
@@ -3,6 +3,11 @@ import React from 'react';
|
||||
|
||||
import { assert } from 'ts-essentials';
|
||||
|
||||
import {
|
||||
type RecommendedQuestionResult,
|
||||
createRecommendedQuestionResult,
|
||||
getEmptySearchResults,
|
||||
} from './empty-search-results';
|
||||
import type { OrderedComputedResult } from './search-types';
|
||||
import { streamRecommendedQuestions } from './server-actions';
|
||||
|
||||
@@ -10,6 +15,7 @@ import { useAI } from '@/components/AI';
|
||||
import assertNever from 'assert-never';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { computeFilterSiteSpaceIds } from './filter';
|
||||
import { useRecentSearchQueries } from './recent-queries';
|
||||
import { type MergedPageResult, reciprocalRankFusion } from './reciprocalRankFusion';
|
||||
import { type LocalPageResult, useLocalSearchResults } from './useLocalSearchResults';
|
||||
import type { SearchScope } from './useSearch';
|
||||
@@ -18,7 +24,7 @@ export type ResultType =
|
||||
| OrderedComputedResult
|
||||
| LocalPageResult
|
||||
| MergedPageResult
|
||||
| { type: 'recommended-question'; id: string; question: string };
|
||||
| RecommendedQuestionResult;
|
||||
|
||||
export type { LocalPageResult, MergedPageResult };
|
||||
|
||||
@@ -31,7 +37,7 @@ export type { LocalPageResult, MergedPageResult };
|
||||
* have different recommended questions for different spaces of the same site.
|
||||
* It should not be used outside of an useEffect.
|
||||
*/
|
||||
const cachedRecommendedQuestions: Map<string, ResultType[]> = new Map();
|
||||
const cachedRecommendedQuestions: Map<string, RecommendedQuestionResult[]> = new Map();
|
||||
|
||||
export function useSearchResults(props: {
|
||||
asEmbeddable?: boolean;
|
||||
@@ -91,6 +97,7 @@ export function useSearchResults(props: {
|
||||
|
||||
const { assistants } = useAI();
|
||||
const withAI = assistants.length > 0;
|
||||
const recentQueries = useRecentSearchQueries(siteSpaceId);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (disabled) {
|
||||
@@ -120,7 +127,7 @@ export function useSearchResults(props: {
|
||||
// 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 recommendedQuestions: RecommendedQuestionResult[] = [];
|
||||
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
suggestions.forEach((question) => {
|
||||
@@ -147,11 +154,7 @@ export function useSearchResults(props: {
|
||||
}
|
||||
|
||||
questions.add(question);
|
||||
recommendedQuestions.push({
|
||||
type: 'recommended-question',
|
||||
id: question,
|
||||
question,
|
||||
});
|
||||
recommendedQuestions.push(createRecommendedQuestionResult(question, question));
|
||||
cachedRecommendedQuestions.set(siteSpaceId, recommendedQuestions);
|
||||
|
||||
if (!cancelled) {
|
||||
@@ -267,24 +270,24 @@ export function useSearchResults(props: {
|
||||
// Re-runs immediately whenever either result set changes.
|
||||
const results = React.useMemo<ResultType[]>(() => {
|
||||
if (!query) {
|
||||
// No query: show recommended questions (AI-only path) or nothing.
|
||||
if (withAI && cachedRecommendedQuestions.has(siteSpaceId)) {
|
||||
return cachedRecommendedQuestions.get(siteSpaceId) ?? [];
|
||||
}
|
||||
if (suggestions && suggestions.length > 0) {
|
||||
return suggestions.map((question, index) => ({
|
||||
type: 'recommended-question' as const,
|
||||
id: `recommended-question-${index}`,
|
||||
question,
|
||||
}));
|
||||
}
|
||||
return [];
|
||||
const recommendedQuestions =
|
||||
cachedRecommendedQuestions.get(siteSpaceId) ??
|
||||
suggestions?.map((question, index) =>
|
||||
createRecommendedQuestionResult(`recommended-question-${index}`, question)
|
||||
) ??
|
||||
[];
|
||||
|
||||
return getEmptySearchResults({
|
||||
withAI,
|
||||
recentQueries,
|
||||
recommendedQuestions,
|
||||
});
|
||||
}
|
||||
|
||||
const merged = reciprocalRankFusion(localResults, remoteState.results, query);
|
||||
|
||||
return merged;
|
||||
}, [localResults, remoteState.results, query, withAI, siteSpaceId, suggestions]);
|
||||
}, [localResults, remoteState.results, query, withAI, siteSpaceId, suggestions, recentQueries]);
|
||||
|
||||
return {
|
||||
results,
|
||||
|
||||
Reference in New Issue
Block a user