* feat: UX tweaks for Modal * Prepare new conversation screen to accept suggested questions * Add conversation history screen * Reset basic askai config * Reset askai api url * Document new translations * Remove unused CSS * Add translation for stopped streaming text * Add rest of translations * Bump allowed bundle size * Bump allowed bundle size
259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
import type { AutocompleteOptions, AutocompleteState } from '@algolia/autocomplete-core';
|
|
import type { LiteClient, SearchParamsObject } from 'algoliasearch/lite';
|
|
import React, { type JSX } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { DocSearchButton } from './DocSearchButton';
|
|
import { DocSearchModal } from './DocSearchModal';
|
|
import type {
|
|
DocSearchHit,
|
|
DocSearchTheme,
|
|
InternalDocSearchHit,
|
|
KeyboardShortcuts,
|
|
StoredDocSearchHit,
|
|
} from './types';
|
|
import type { AskAiState } from './types/AskiAi';
|
|
import { useDocSearchKeyboardEvents } from './useDocSearchKeyboardEvents';
|
|
import { useTheme } from './useTheme';
|
|
|
|
import type { ButtonTranslations, ModalTranslations } from '.';
|
|
|
|
export type DocSearchTranslations = Partial<{
|
|
button: ButtonTranslations;
|
|
modal: ModalTranslations;
|
|
}>;
|
|
|
|
// The interface that describes the minimal implementation required for the algoliasearch client, when using the [`transformSearchClient`](https://docsearch.algolia.com/docs/api/#transformsearchclient) option.
|
|
export type DocSearchTransformClient = {
|
|
search: LiteClient['search'];
|
|
addAlgoliaAgent: LiteClient['addAlgoliaAgent'];
|
|
transporter: Pick<LiteClient['transporter'], 'algoliaAgent'>;
|
|
};
|
|
|
|
export type DocSearchAskAi = {
|
|
/**
|
|
* The index name to use for the ask AI feature. Your assistant will search this index for relevant documents.
|
|
* If not provided, the index name will be used.
|
|
*/
|
|
indexName?: string;
|
|
/**
|
|
* The API key to use for the ask AI feature. Your assistant will use this API key to search the index.
|
|
* If not provided, the API key will be used.
|
|
*/
|
|
apiKey?: string;
|
|
/**
|
|
* The app ID to use for the ask AI feature. Your assistant will use this app ID to search the index.
|
|
* If not provided, the app ID will be used.
|
|
*/
|
|
appId?: string;
|
|
/**
|
|
* The assistant ID to use for the ask AI feature.
|
|
*/
|
|
assistantId: string | null;
|
|
/**
|
|
* The search parameters to use for the ask AI feature.
|
|
*/
|
|
searchParameters?: {
|
|
facetFilters?: SearchParamsObject['facetFilters'];
|
|
};
|
|
};
|
|
|
|
export interface DocSearchIndex {
|
|
name: string;
|
|
searchParameters?: SearchParamsObject;
|
|
}
|
|
|
|
export interface DocSearchProps {
|
|
/**
|
|
* Algolia application id used by the search client.
|
|
*/
|
|
appId: string;
|
|
/**
|
|
* Public api key with search permissions for the index.
|
|
*/
|
|
apiKey: string;
|
|
/**
|
|
* Name of the algolia index to query.
|
|
*
|
|
* @deprecated `indexName` will be removed in a future version. Please use `indices` property going forward.
|
|
*/
|
|
indexName?: string;
|
|
/**
|
|
* List of indices and _optional_ searchParameters to be used for search.
|
|
*
|
|
* @see {@link https://docsearch.algolia.com/docs/api#indices}
|
|
*/
|
|
indices?: Array<DocSearchIndex | string>;
|
|
/**
|
|
* Configuration or assistant id to enable ask ai mode. Pass a string assistant id or a full config object.
|
|
*/
|
|
askAi?: DocSearchAskAi | string;
|
|
/**
|
|
* Theme overrides applied to the modal and related components.
|
|
*/
|
|
theme?: DocSearchTheme;
|
|
/**
|
|
* Placeholder text for the search input.
|
|
*/
|
|
placeholder?: string;
|
|
/**
|
|
* Additional algolia search parameters to merge into each query.
|
|
*
|
|
* @deprecated `searchParameters` will be removed in a future version. Please use `indices` property going forward.
|
|
*/
|
|
searchParameters?: SearchParamsObject;
|
|
/**
|
|
* Maximum number of hits to display per source/group.
|
|
*/
|
|
maxResultsPerGroup?: number;
|
|
/**
|
|
* Hook to post-process hits before rendering.
|
|
*/
|
|
transformItems?: (items: DocSearchHit[]) => DocSearchHit[];
|
|
/**
|
|
* Custom component to render an individual hit.
|
|
*/
|
|
hitComponent?: (props: { hit: InternalDocSearchHit | StoredDocSearchHit; children: React.ReactNode }) => JSX.Element;
|
|
/**
|
|
* Custom component rendered at the bottom of the results panel.
|
|
*/
|
|
resultsFooterComponent?: (props: { state: AutocompleteState<InternalDocSearchHit> }) => JSX.Element | null;
|
|
/**
|
|
* Hook to wrap or modify the algolia search client.
|
|
*/
|
|
transformSearchClient?: (searchClient: DocSearchTransformClient) => DocSearchTransformClient;
|
|
/**
|
|
* Disable storage and usage of recent and favorite searches.
|
|
*/
|
|
disableUserPersonalization?: boolean;
|
|
/**
|
|
* Query string to prefill when opening the modal.
|
|
*/
|
|
initialQuery?: string;
|
|
/**
|
|
* Custom navigator for controlling link navigation.
|
|
*/
|
|
navigator?: AutocompleteOptions<InternalDocSearchHit>['navigator'];
|
|
/**
|
|
* Localized strings for the button and modal ui.
|
|
*/
|
|
translations?: DocSearchTranslations;
|
|
/**
|
|
* Builds a url to report missing results for a given query.
|
|
*/
|
|
getMissingResultsUrl?: ({ query }: { query: string }) => string;
|
|
/**
|
|
* Insights client integration options to send analytics events.
|
|
*/
|
|
insights?: AutocompleteOptions<InternalDocSearchHit>['insights'];
|
|
/**
|
|
* The container element where the modal should be portaled to. Defaults to document.body.
|
|
*/
|
|
portalContainer?: DocumentFragment | Element;
|
|
/**
|
|
* Limit of how many recent searches should be saved/displayed..
|
|
*
|
|
* @default 7
|
|
*/
|
|
recentSearchesLimit?: number;
|
|
/**
|
|
* Limit of how many recent searches should be saved/displayed when there are favorited searches..
|
|
*
|
|
* @default 4
|
|
*/
|
|
recentSearchesWithFavoritesLimit?: number;
|
|
/**
|
|
* Configuration for keyboard shortcuts. Allows enabling/disabling specific shortcuts.
|
|
*/
|
|
keyboardShortcuts?: KeyboardShortcuts;
|
|
}
|
|
|
|
export function DocSearch(props: DocSearchProps): JSX.Element {
|
|
const searchButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
const [initialQuery, setInitialQuery] = React.useState<string | undefined>(props?.initialQuery || undefined);
|
|
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
|
|
const isAskAiActive = askAiState !== 'initial';
|
|
|
|
let currentPlaceholder =
|
|
props?.translations?.modal?.searchBox?.placeholderText || props?.placeholder || 'Search docs';
|
|
|
|
// check if the instance is configured to handle ask ai
|
|
const canHandleAskAi = Boolean(props?.askAi);
|
|
|
|
if (canHandleAskAi) {
|
|
currentPlaceholder = props?.translations?.modal?.searchBox?.placeholderText || 'Search docs or ask AI a question';
|
|
}
|
|
|
|
if (isAskAiActive) {
|
|
currentPlaceholder = props?.translations?.modal?.searchBox?.placeholderTextAskAi || 'Ask another question...';
|
|
}
|
|
|
|
const onAskAiToggle = React.useCallback(
|
|
(askAitoggle: boolean) => {
|
|
setAskAiState(askAitoggle ? 'conversation' : 'initial');
|
|
},
|
|
[setAskAiState],
|
|
);
|
|
|
|
const onOpen = React.useCallback(() => {
|
|
setIsOpen(true);
|
|
}, [setIsOpen]);
|
|
|
|
const onClose = React.useCallback(() => {
|
|
setIsOpen(false);
|
|
setInitialQuery(props?.initialQuery);
|
|
if (isAskAiActive) {
|
|
setAskAiState('initial');
|
|
}
|
|
}, [setIsOpen, props.initialQuery, isAskAiActive, setAskAiState]);
|
|
|
|
const onInput = React.useCallback(
|
|
(event: KeyboardEvent) => {
|
|
setIsOpen(true);
|
|
setInitialQuery(event.key);
|
|
},
|
|
[setIsOpen, setInitialQuery],
|
|
);
|
|
|
|
useDocSearchKeyboardEvents({
|
|
isOpen,
|
|
onOpen,
|
|
onClose,
|
|
onInput,
|
|
isAskAiActive,
|
|
onAskAiToggle,
|
|
searchButtonRef,
|
|
keyboardShortcuts: props.keyboardShortcuts,
|
|
});
|
|
useTheme({ theme: props.theme });
|
|
|
|
return (
|
|
<>
|
|
<DocSearchButton
|
|
ref={searchButtonRef}
|
|
translations={props?.translations?.button}
|
|
keyboardShortcuts={props.keyboardShortcuts}
|
|
onClick={onOpen}
|
|
/>
|
|
|
|
{isOpen &&
|
|
createPortal(
|
|
<DocSearchModal
|
|
{...props}
|
|
placeholder={currentPlaceholder}
|
|
initialScrollY={window.scrollY}
|
|
initialQuery={initialQuery}
|
|
translations={props?.translations?.modal}
|
|
isAskAiActive={isAskAiActive}
|
|
canHandleAskAi={canHandleAskAi}
|
|
askAiState={askAiState}
|
|
setAskAiState={setAskAiState}
|
|
onAskAiToggle={onAskAiToggle}
|
|
onClose={onClose}
|
|
/>,
|
|
props.portalContainer ?? document.body,
|
|
)}
|
|
</>
|
|
);
|
|
}
|