1
0
Fork 0

feat(suggested-questions): Add suggested questions (#2787)

* feat(suggested-questions): Add suggested questions

* fix: build

* add docs, fix react-example

* add some more documentation
This commit is contained in:
Paul Jankowski 2025-10-28 14:31:44 -04:00 committed by GitHub
parent f7be2a65fc
commit 2b4779ad06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 126 additions and 21 deletions

View file

@ -1426,6 +1426,7 @@ assistive tech users */
justify-content: center;
background-color: #fff;
color: var(--docsearch-text-color);
cursor: pointer;
}
.DocSearch-Menu {

View file

@ -61,6 +61,12 @@ export type DocSearchAskAi = {
* The search parameters to use for the ask AI feature.
*/
searchParameters?: AskAiSearchParameters;
/**
* Enables displaying suggested questions on Ask AI's new conversation screen.
*
* @default false
*/
suggestedQuestions?: boolean;
};
export interface DocSearchIndex {

View file

@ -6,6 +6,7 @@ import {
type AutocompleteState,
} from '@algolia/autocomplete-core';
import { useTheme } from '@docsearch/core/useTheme';
import type { ChatRequestOptions } from 'ai';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import type { SearchResponse } from 'algoliasearch/lite';
import React, { type JSX } from 'react';
@ -22,9 +23,17 @@ import { ScreenState } from './ScreenState';
import type { SearchBoxTranslations } from './SearchBox';
import { SearchBox } from './SearchBox';
import { createStoredConversations, createStoredSearches } from './stored-searches';
import type { DocSearchHit, DocSearchState, InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
import type {
DocSearchHit,
DocSearchState,
InternalDocSearchHit,
StoredAskAiState,
StoredDocSearchHit,
SuggestedQuestionHit,
} from './types';
import type { AIMessage, AskAiState } from './types/AskiAi';
import { useSearchClient } from './useSearchClient';
import { useSuggestedQuestions } from './useSuggestedQuestions';
import { useTouchEvents } from './useTouchEvents';
import { useTrapFocus } from './useTrapFocus';
import { groupBy, identity, noop, removeHighlightTags, isModifierEvent, scrollTo as scrollToUtils } from './utils';
@ -341,6 +350,11 @@ export function DocSearchModal({
const askAiConfigurationId = typeof askAi === 'string' ? askAi : askAiConfig?.assistantId || null;
const askAiSearchParameters = askAiConfig?.searchParameters;
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
const suggestedQuestions = useSuggestedQuestions({
assistantId: askAiConfigurationId,
searchClient,
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
});
// Format the `indexes` to be used until `indexName` and `searchParameters` props are fully removed.
const indexes: DocSearchIndex[] = [];
@ -517,24 +531,35 @@ export function DocSearchModal({
>(undefined);
const handleSelectAskAiQuestion = React.useCallback(
(toggle: boolean, query: string) => {
(toggle: boolean, query: string, suggestedQuestion: SuggestedQuestionHit | undefined = undefined) => {
if (toggle && askAiState === 'new-conversation') {
// We're starting a new conversation, clear out current messages
setMessages([]);
setAskAiState('initial');
}
const messageOptions: ChatRequestOptions = {};
if (suggestedQuestion) {
messageOptions.body = {
suggestedQuestionId: suggestedQuestion.objectID,
};
}
onAskAiToggle(toggle);
setStoppedStream(false);
sendMessage({
role: 'user',
parts: [
{
type: 'text',
text: query,
},
],
});
sendMessage(
{
role: 'user',
parts: [
{
type: 'text',
text: query,
},
],
},
messageOptions,
);
if (dropdownRef.current) {
// some test environments (like jsdom) don't implement element.scrollTo
@ -809,6 +834,10 @@ export function DocSearchModal({
setAskAiState('conversation-history');
};
const selectSuggestedQuestion = (suggestedQuestion: SuggestedQuestionHit): void => {
handleSelectAskAiQuestion(true, suggestedQuestion.question, suggestedQuestion);
};
// hide the dropdown on idle and no collections
let showDocsearchDropdown = true;
const hasCollections = state.collections.some((collection) => collection.items.length > 0);
@ -885,6 +914,8 @@ export function DocSearchModal({
hasCollections={hasCollections}
askAiState={askAiState}
selectAskAiQuestion={handleSelectAskAiQuestion}
suggestedQuestions={suggestedQuestions}
selectSuggestedQuestion={selectSuggestedQuestion}
onAskAiToggle={onAskAiToggle}
onItemClick={(item, event) => {
// if the item is askAI toggle the screen

View file

@ -1,16 +1,15 @@
import type { Hit } from 'algoliasearch/lite';
import React, { type JSX } from 'react';
import type { SuggestedQuestionHit } from './types';
export type NewConversationTranslations = Partial<{
newConversationTitle: string;
newConversationDescription: string;
}>;
type SuggestedQuestion = Hit<{ question: string }>;
interface NewConversationProps {
suggestedQuestions?: SuggestedQuestion[];
selectSuggestedQuestion: (query: string) => void;
suggestedQuestions?: SuggestedQuestionHit[];
selectSuggestedQuestion: (question: SuggestedQuestionHit) => void;
translations?: NewConversationTranslations;
}
@ -35,7 +34,7 @@ export function NewConversationScreen({
key={question.objectID}
type="button"
className="DocSearch-NewConversationScreen-SuggestedQuestion"
onClick={() => selectSuggestedQuestion(question.question)}
onClick={() => selectSuggestedQuestion(question)}
>
{question.question}
</button>

View file

@ -17,7 +17,7 @@ import { ResultsScreen } from './ResultsScreen';
import type { StartScreenTranslations } from './StartScreen';
import { StartScreen } from './StartScreen';
import type { StoredSearchPlugin } from './stored-searches';
import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit } from './types';
import type { InternalDocSearchHit, StoredAskAiState, StoredDocSearchHit, SuggestedQuestionHit } from './types';
import type { AIMessage, AskAiState } from './types/AskiAi';
export type ScreenStateTranslations = Partial<{
@ -54,6 +54,8 @@ export interface ScreenStateProps<TItem extends BaseItem>
onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise<void>;
askAiState: AskAiState;
selectAskAiQuestion: (toggle: boolean, query: string) => void;
suggestedQuestions: SuggestedQuestionHit[];
selectSuggestedQuestion: (question: SuggestedQuestionHit) => void;
}
export const ScreenState = React.memo(
@ -66,9 +68,8 @@ export const ScreenState = React.memo(
return (
<NewConversationScreen
translations={translations?.newConversation}
selectSuggestedQuestion={(query: string) => {
props.selectAskAiQuestion(true, query);
}}
selectSuggestedQuestion={props.selectSuggestedQuestion}
suggestedQuestions={props.suggestedQuestions}
/>
);
}

View file

@ -1,3 +1,4 @@
export const MAX_QUERY_SIZE = 512;
export const ASK_AI_API_URL = 'https://askai.algolia.com/chat';
export const USE_ASK_AI_TOKEN = true;
export const SUGGESTED_QUETIONS_INDEX_NAME = 'algolia_ask_ai_suggested_questions';

View file

@ -0,0 +1,13 @@
import type { Hit } from 'algoliasearch/lite';
export type SuggestedQuestion = {
appId: string;
assistantId: string;
question: string;
locale?: string;
state: 'published';
source: string;
order: number;
};
export type SuggestedQuestionHit = Hit<SuggestedQuestion>;

View file

@ -4,3 +4,4 @@ export * from './DocSearchTheme';
export * from './InternalDocSearchHit';
export * from './KeyboardShortcuts';
export * from './StoredDocSearchHit';
export * from './SuggestedQuestions';

View file

@ -0,0 +1,44 @@
import type { SearchResponse } from 'algoliasearch/lite';
import { useEffect, useState } from 'react';
import { SUGGESTED_QUETIONS_INDEX_NAME } from './constants';
import type { DocSearchTransformClient, SuggestedQuestion, SuggestedQuestionHit } from '.';
type UseSuggestedQuestionsProps = {
assistantId: string | null;
searchClient: DocSearchTransformClient;
suggestedQuestionsEnabled?: boolean;
};
export const useSuggestedQuestions = ({
assistantId,
searchClient,
suggestedQuestionsEnabled = false,
}: UseSuggestedQuestionsProps): SuggestedQuestionHit[] => {
const [suggestedQuestions, setSuggestedQuestions] = useState<SuggestedQuestionHit[]>([]);
useEffect(() => {
const getSuggestedQuestions = async (): Promise<void> => {
const { results } = await searchClient.search<SuggestedQuestion>({
requests: [
{
indexName: SUGGESTED_QUETIONS_INDEX_NAME,
filters: `state:published AND assistantId:${assistantId}`,
hitsPerPage: 3,
},
],
});
const result = results[0] as SearchResponse<SuggestedQuestion>;
setSuggestedQuestions(result.hits);
};
if (suggestedQuestionsEnabled && assistantId && assistantId !== '') {
getSuggestedQuestions();
}
}, [suggestedQuestionsEnabled, assistantId, searchClient]);
return suggestedQuestions;
};

View file

@ -196,6 +196,9 @@ docsearch({
// Deduplication
distinct: true,
},
// Enables/disables showing suggested questions on Ask AI's new conversation screen
suggestedQuestions: true,
},
// ...
});
@ -234,6 +237,9 @@ in case you want to use different credentials for askAi
// Deduplication
distinct: true,
},
// Enables/disables showing suggested questions on Ask AI's new conversation screen
suggestedQuestions: true,
}}
/>
```

View file

@ -215,6 +215,7 @@ docsearch({
searchParameters: {
facetFilters: ['language:en', 'version:1.0.0'], // Optional: filter Ask AI context
},
suggestedQuestions: true // Optional: enable loading suggested questions on the Ask AI new conversation screen
},
});
```
@ -224,6 +225,7 @@ docsearch({
- Use the string form for a simple setup.
- Use the object form to customize which index, credentials, or filters Ask AI uses.
- The suggested questions feature is controlled on the [Dashboard](https://dashboard.algolia.com) in the Ask AI section.
### Filtering search results