diff --git a/packages/docsearch-css/src/modal.css b/packages/docsearch-css/src/modal.css index 0063bdc5..b6abf07f 100644 --- a/packages/docsearch-css/src/modal.css +++ b/packages/docsearch-css/src/modal.css @@ -1426,6 +1426,7 @@ assistive tech users */ justify-content: center; background-color: #fff; color: var(--docsearch-text-color); + cursor: pointer; } .DocSearch-Menu { diff --git a/packages/docsearch-react/src/DocSearch.tsx b/packages/docsearch-react/src/DocSearch.tsx index 544bd42e..83339327 100644 --- a/packages/docsearch-react/src/DocSearch.tsx +++ b/packages/docsearch-react/src/DocSearch.tsx @@ -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 { diff --git a/packages/docsearch-react/src/DocSearchModal.tsx b/packages/docsearch-react/src/DocSearchModal.tsx index 90ddb0c8..b107a443 100644 --- a/packages/docsearch-react/src/DocSearchModal.tsx +++ b/packages/docsearch-react/src/DocSearchModal.tsx @@ -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('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 diff --git a/packages/docsearch-react/src/NewConversationScreen.tsx b/packages/docsearch-react/src/NewConversationScreen.tsx index bf26b8dd..157b80d3 100644 --- a/packages/docsearch-react/src/NewConversationScreen.tsx +++ b/packages/docsearch-react/src/NewConversationScreen.tsx @@ -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} diff --git a/packages/docsearch-react/src/ScreenState.tsx b/packages/docsearch-react/src/ScreenState.tsx index 1b6659fb..9d27e979 100644 --- a/packages/docsearch-react/src/ScreenState.tsx +++ b/packages/docsearch-react/src/ScreenState.tsx @@ -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 onFeedback?: (messageId: string, thumbs: 0 | 1) => Promise; 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 ( { - props.selectAskAiQuestion(true, query); - }} + selectSuggestedQuestion={props.selectSuggestedQuestion} + suggestedQuestions={props.suggestedQuestions} /> ); } diff --git a/packages/docsearch-react/src/constants.ts b/packages/docsearch-react/src/constants.ts index ea47d6b1..246bf538 100644 --- a/packages/docsearch-react/src/constants.ts +++ b/packages/docsearch-react/src/constants.ts @@ -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'; diff --git a/packages/docsearch-react/src/types/SuggestedQuestions.ts b/packages/docsearch-react/src/types/SuggestedQuestions.ts new file mode 100644 index 00000000..8189eff1 --- /dev/null +++ b/packages/docsearch-react/src/types/SuggestedQuestions.ts @@ -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; diff --git a/packages/docsearch-react/src/types/index.ts b/packages/docsearch-react/src/types/index.ts index a50d953e..eae7071e 100644 --- a/packages/docsearch-react/src/types/index.ts +++ b/packages/docsearch-react/src/types/index.ts @@ -4,3 +4,4 @@ export * from './DocSearchTheme'; export * from './InternalDocSearchHit'; export * from './KeyboardShortcuts'; export * from './StoredDocSearchHit'; +export * from './SuggestedQuestions'; diff --git a/packages/docsearch-react/src/useSuggestedQuestions.ts b/packages/docsearch-react/src/useSuggestedQuestions.ts new file mode 100644 index 00000000..703938ec --- /dev/null +++ b/packages/docsearch-react/src/useSuggestedQuestions.ts @@ -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([]); + + useEffect(() => { + const getSuggestedQuestions = async (): Promise => { + const { results } = await searchClient.search({ + requests: [ + { + indexName: SUGGESTED_QUETIONS_INDEX_NAME, + filters: `state:published AND assistantId:${assistantId}`, + hitsPerPage: 3, + }, + ], + }); + + const result = results[0] as SearchResponse; + + setSuggestedQuestions(result.hits); + }; + + if (suggestedQuestionsEnabled && assistantId && assistantId !== '') { + getSuggestedQuestions(); + } + }, [suggestedQuestionsEnabled, assistantId, searchClient]); + + return suggestedQuestions; +}; diff --git a/packages/website/docs/api.mdx b/packages/website/docs/api.mdx index 82d5f4d1..02d901de 100644 --- a/packages/website/docs/api.mdx +++ b/packages/website/docs/api.mdx @@ -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, }} /> ``` diff --git a/packages/website/docs/docsearch.mdx b/packages/website/docs/docsearch.mdx index 1922ccd8..b754fca5 100644 --- a/packages/website/docs/docsearch.mdx +++ b/packages/website/docs/docsearch.mdx @@ -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