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:
parent
f7be2a65fc
commit
2b4779ad06
11 changed files with 126 additions and 21 deletions
|
|
@ -1426,6 +1426,7 @@ assistive tech users */
|
|||
justify-content: center;
|
||||
background-color: #fff;
|
||||
color: var(--docsearch-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DocSearch-Menu {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
13
packages/docsearch-react/src/types/SuggestedQuestions.ts
Normal file
13
packages/docsearch-react/src/types/SuggestedQuestions.ts
Normal 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>;
|
||||
|
|
@ -4,3 +4,4 @@ export * from './DocSearchTheme';
|
|||
export * from './InternalDocSearchHit';
|
||||
export * from './KeyboardShortcuts';
|
||||
export * from './StoredDocSearchHit';
|
||||
export * from './SuggestedQuestions';
|
||||
|
|
|
|||
44
packages/docsearch-react/src/useSuggestedQuestions.ts
Normal file
44
packages/docsearch-react/src/useSuggestedQuestions.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue