feat(v5): UI and DX improvements (#2949)
* feat: Rename assistantId to agentId * feat: Allow reading default facet values from index searchParameters * feat: Remove indexName prop from Sidepanel, cleanup documentation pages * feat: Move appId and apiKey up into @docsearch/core * feat: Add back nested grouping of search results * add changeset * revert changes to example demo * fix: e2e tests
This commit is contained in:
parent
5eac1fd6bb
commit
4f6b5b1b88
52 changed files with 781 additions and 283 deletions
15
.changeset/brave-things-join.md
Normal file
15
.changeset/brave-things-join.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
"@docsearch/docusaurus-adapter": patch
|
||||
"@docsearch/sidepanel": patch
|
||||
"@docsearch/modal": patch
|
||||
"@docsearch/react": patch
|
||||
"@docsearch/core": patch
|
||||
---
|
||||
|
||||
feat(v5): UI and DX updates
|
||||
|
||||
- Rename the Ask AI assistantId option to agentId (adapter theme.SearchModal.askAi.assistantId → agentId)
|
||||
- appId and apiKey moved up into @docsearch/core, so they're configured once and shared
|
||||
- Removed the indexName prop from the Sidepanel
|
||||
- Facet defaults can now be read from the index searchParameters
|
||||
- Restored nested grouping of search results
|
||||
|
|
@ -117,7 +117,6 @@
|
|||
"new-cap": ["error"],
|
||||
"no-array-constructor": ["error"],
|
||||
"no-bitwise": ["error"],
|
||||
"no-continue": ["error"],
|
||||
"no-lonely-if": ["error"],
|
||||
"no-nested-ternary": ["error"],
|
||||
"no-unneeded-ternary": ["error"],
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const minimalDocSearchConfig = {
|
|||
} satisfies DocSearchInput;
|
||||
|
||||
const minimalAskAiConfig = {
|
||||
assistantId: 'my-assistant-id',
|
||||
agentId: 'my-assistant-id',
|
||||
} satisfies NonNullable<DocSearchInput>['askAi'];
|
||||
|
||||
const askAiConfigWithIndices = {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ declare module '@docsearch/docusaurus-adapter' {
|
|||
};
|
||||
|
||||
export type AskAiConfig = {
|
||||
assistantId: DocSearchAskAi['assistantId'];
|
||||
agentId: DocSearchAskAi['agentId'];
|
||||
suggestedQuestions?: DocSearchAskAi['suggestedQuestions'];
|
||||
searchParameters?: AgentStudioSearchParameters;
|
||||
indices?: AgentStudioIndices[];
|
||||
|
|
|
|||
|
|
@ -79,6 +79,8 @@ type AdapterDocSearchProps = Omit<
|
|||
| 'onAskAiToggle'
|
||||
| 'onClose'
|
||||
> & {
|
||||
appId: DocSearchProps['appId'];
|
||||
apiKey: DocSearchProps['apiKey'];
|
||||
askAi?: AskAiOptions;
|
||||
contextualSearch?: boolean;
|
||||
externalUrlRegex?: string;
|
||||
|
|
@ -422,10 +424,9 @@ function DocSearch({
|
|||
{...panelOptions}
|
||||
variant={panelOptions.variant ?? 'inline'}
|
||||
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
|
||||
assistantId={sidePanelAskAi.assistantId}
|
||||
agentId={sidePanelAskAi.agentId}
|
||||
apiKey={sidePanelAskAi.apiKey}
|
||||
appId={sidePanelAskAi.appId}
|
||||
indexName={sidePanelAskAi.indexName}
|
||||
searchParameters={sidePanelAskAi.searchParameters}
|
||||
indices={panelOptions.indices ?? sidePanelAskAi.indices}
|
||||
suggestedQuestions={
|
||||
|
|
@ -454,6 +455,8 @@ export default function SearchBar(
|
|||
|
||||
return (
|
||||
<DocSearchProvider
|
||||
appId={docSearchProps.appId}
|
||||
apiKey={docSearchProps.apiKey}
|
||||
initialQuery={docSearchProps.initialQuery}
|
||||
keyboardShortcuts={docSearchProps.keyboardShortcuts}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ const AskAiPromptSuggestionsSchema = Joi.object({
|
|||
}).unknown(false);
|
||||
|
||||
const AskAiSchema = Joi.object({
|
||||
assistantId: Joi.string().required(),
|
||||
agentId: Joi.string().required(),
|
||||
suggestedQuestions: Joi.boolean().optional(),
|
||||
searchParameters: Joi.object()
|
||||
.pattern(Joi.string(), SearchParametersSchema)
|
||||
|
|
@ -261,7 +261,7 @@ function assertNoRemovedKeys(themeConfig: ThemeConfig): void {
|
|||
const askAi = docsearchRecord.askAi;
|
||||
if (typeof askAi === 'string') {
|
||||
throw new Error(
|
||||
'`themeConfig.docsearch.askAi` must be an object with `assistantId`.'
|
||||
'`themeConfig.docsearch.askAi` must be an object with `agentId`.'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ test.describe('a11y > Modal', () => {
|
|||
await expect(docSearch.hits).toBeVisible();
|
||||
|
||||
const scanResults = await axe()
|
||||
.include('#docsearch-hits_docsearch-list')
|
||||
.include('#docsearch-hits_docsearch_0-list')
|
||||
.analyze();
|
||||
|
||||
await testInfo.attach('a11y-scan-results-modal-search-results', {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export class DocSearchPage {
|
|||
this.hits = page.locator('.DocSearch-Hits').first();
|
||||
this.clearButton = page.locator('.DocSearch-Clear');
|
||||
this.firstHit = page
|
||||
.locator('#docsearch-hits_docsearch-list .DocSearch-Hit a')
|
||||
.locator('#docsearch-hits_docsearch_0-list .DocSearch-Hit a')
|
||||
.first();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ test.describe('Search', () => {
|
|||
await docSearch.typeQueryMatching();
|
||||
await expect(docSearch.hits).toBeVisible();
|
||||
const firstHitOption = page
|
||||
.locator('#docsearch-hits_docsearch-list')
|
||||
.locator('#docsearch-hits_docsearch_0-list')
|
||||
.getByRole('option')
|
||||
.first();
|
||||
const firstHitOptionId = await firstHitOption.getAttribute('id');
|
||||
|
|
|
|||
|
|
@ -36,10 +36,9 @@ let sidepanelInstance: SidepanelInstance | undefined = undefined;
|
|||
|
||||
sidepanelInstance = sidepanel({
|
||||
container: '#docsearch-sidepanel',
|
||||
indexName: 'docsearch',
|
||||
appId: 'PMZUYBQDAK',
|
||||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
agentId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
onReady: () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[demo-js] sidepanel onReady()');
|
||||
|
|
@ -73,9 +72,7 @@ docsearchInstance = docsearch({
|
|||
indices: ['docsearch'],
|
||||
appId: 'PMZUYBQDAK',
|
||||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||
askAi: {
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
},
|
||||
askAi: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
interceptAskAiEvent: (initialMessage) => {
|
||||
docsearchInstance?.close();
|
||||
sidepanelInstance.open(initialMessage);
|
||||
|
|
|
|||
4
examples/demo-react/src/constants.ts
Normal file
4
examples/demo-react/src/constants.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const APP_ID = 'PMZUYBQDAK';
|
||||
export const API_KEY = '24b09689d5b4223813d9b8e48563c8f6';
|
||||
export const AGENT_ID = 'ccdec697-e3fe-465b-a1c3-657e7bf18aef';
|
||||
export const SEARCH_INDEX_NAME = 'docsearch';
|
||||
|
|
@ -3,6 +3,7 @@ import { DocSearchAI, type ToolCalls } from '@docsearch/react';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
const customTools: ToolCalls = {
|
||||
printConsoleMessage: {
|
||||
|
|
@ -29,11 +30,11 @@ export default function BasicAskAI({
|
|||
}): JSX.Element {
|
||||
return (
|
||||
<DocSearchAI
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
indices={[SEARCH_INDEX_NAME]}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
askAi={{
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
agentId: AGENT_ID,
|
||||
suggestedQuestions: true,
|
||||
tools: customTools,
|
||||
promptSuggestions: {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ import { DocSearch } from '@docsearch/react';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
export default function Basic({ theme }: { theme: DemoTheme }): JSX.Element {
|
||||
return (
|
||||
<DocSearch
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
indices={[SEARCH_INDEX_NAME]}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
translations={{ button: { buttonText: 'Keyword search' } }}
|
||||
insights={true}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DocSearchButton, DocSearchAskAiModal } from '@docsearch/modal';
|
|||
import { type JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
export default function Composable({
|
||||
theme,
|
||||
|
|
@ -11,16 +12,9 @@ export default function Composable({
|
|||
theme: DemoTheme;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<DocSearch theme={theme}>
|
||||
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||
<DocSearchButton translations={{ buttonText: 'Composable API' }} />
|
||||
<DocSearchAskAiModal
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
askAi={{
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
}}
|
||||
/>
|
||||
<DocSearchAskAiModal askAi={AGENT_ID} indices={[SEARCH_INDEX_NAME]} />
|
||||
</DocSearch>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DocSearch } from '@docsearch/react';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
export default function DefaultExperience({
|
||||
theme,
|
||||
|
|
@ -11,9 +12,9 @@ export default function DefaultExperience({
|
|||
}): JSX.Element {
|
||||
return (
|
||||
<DocSearch
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
indices={[SEARCH_INDEX_NAME]}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { useCallback, useRef, useState, type JSX } from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
let DocSearchModal: typeof DocSearchAskAiModalType | null = null;
|
||||
|
||||
|
|
@ -94,12 +95,10 @@ function DocSearch({ theme }: { theme: DemoTheme }): JSX.Element {
|
|||
searchContainer.current &&
|
||||
createPortal(
|
||||
<DocSearchModal
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
askAi={{
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
}}
|
||||
indices={[SEARCH_INDEX_NAME]}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
askAi={AGENT_ID}
|
||||
initialScrollY={window.scrollY}
|
||||
initialQuery={initialQuery}
|
||||
isAskAiActive={isAskAiActive}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { Sidepanel, SidepanelButton } from '@docsearch/sidepanel';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { AGENT_ID, API_KEY, APP_ID } from '../constants';
|
||||
|
||||
export default function BasicHybrid({
|
||||
theme,
|
||||
|
|
@ -12,24 +13,12 @@ export default function BasicHybrid({
|
|||
theme: DemoTheme;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<DocSearch theme={theme}>
|
||||
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||
<DocSearchButton />
|
||||
<DocSearchAskAiModal
|
||||
indices={['docsearch']}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
askAi={{
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
}}
|
||||
/>
|
||||
<DocSearchAskAiModal askAi={AGENT_ID} indices={['docsearch']} />
|
||||
|
||||
<SidepanelButton />
|
||||
<Sidepanel
|
||||
indexName="docsearch-markdown"
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
||||
/>
|
||||
<Sidepanel agentId={AGENT_ID} />
|
||||
</DocSearch>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { DocSearch } from '@docsearch/react';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||
|
||||
export default function MultiIndex({
|
||||
theme,
|
||||
|
|
@ -13,7 +14,7 @@ export default function MultiIndex({
|
|||
<DocSearch
|
||||
indices={[
|
||||
{
|
||||
name: 'docsearch',
|
||||
name: SEARCH_INDEX_NAME,
|
||||
},
|
||||
{
|
||||
name: 'tailwindcss',
|
||||
|
|
@ -22,8 +23,8 @@ export default function MultiIndex({
|
|||
name: 'kubernetes',
|
||||
},
|
||||
]}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
translations={{ button: { buttonText: 'Multi index search' } }}
|
||||
insights={true}
|
||||
theme={theme}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { SidepanelButton, Sidepanel } from '@docsearch/sidepanel';
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import type { DemoTheme } from '../App';
|
||||
import { AGENT_ID, API_KEY, APP_ID } from '../constants';
|
||||
|
||||
export default function SidepanelExample({
|
||||
theme,
|
||||
|
|
@ -11,13 +12,10 @@ export default function SidepanelExample({
|
|||
theme: DemoTheme;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<DocSearch theme={theme}>
|
||||
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||
<SidepanelButton variant="inline" />
|
||||
<Sidepanel
|
||||
indexName="docsearch"
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
||||
agentId={AGENT_ID}
|
||||
variant="floating"
|
||||
tools={{
|
||||
printConsoleMessage: {
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ export default function WTransformItems({
|
|||
]}
|
||||
appId="PMZUYBQDAK"
|
||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||
askAi={{
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
}}
|
||||
askAi="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
||||
insights={true}
|
||||
transformItems={(items) => {
|
||||
return items.map((item: any) => ({
|
||||
|
|
|
|||
|
|
@ -48,6 +48,10 @@ export interface DocSearchRef {
|
|||
}
|
||||
|
||||
export interface DocSearchContext {
|
||||
/** Default Algolia application ID for child DocSearch/Sidepanel components. */
|
||||
appId?: string;
|
||||
/** Default Algolia API key for child DocSearch/Sidepanel components. */
|
||||
apiKey?: string;
|
||||
docsearchState: DocSearchState;
|
||||
setDocsearchState: (newState: DocSearchState) => void;
|
||||
searchButtonRef: React.RefObject<HTMLButtonElement | null>;
|
||||
|
|
@ -79,6 +83,10 @@ export interface DocSearchCallbacks {
|
|||
|
||||
export interface DocSearchProps extends DocSearchCallbacks {
|
||||
children: Array<JSX.Element | null> | JSX.Element | React.ReactNode | null;
|
||||
/** Default Algolia application id for descendant DocSearch views. */
|
||||
appId?: string;
|
||||
/** Default Algolia API key for descendant DocSearch views. */
|
||||
apiKey?: string;
|
||||
theme?: DocSearchTheme;
|
||||
initialQuery?: string;
|
||||
keyboardShortcuts?: KeyboardShortcuts;
|
||||
|
|
@ -90,6 +98,8 @@ Context.displayName = 'DocSearchContext';
|
|||
function DocSearchInner(
|
||||
{
|
||||
children,
|
||||
appId,
|
||||
apiKey,
|
||||
theme,
|
||||
onReady,
|
||||
onOpen,
|
||||
|
|
@ -267,6 +277,8 @@ function DocSearchInner(
|
|||
|
||||
const value: DocSearchContext = React.useMemo(
|
||||
() => ({
|
||||
appId,
|
||||
apiKey,
|
||||
docsearchState,
|
||||
setDocsearchState,
|
||||
searchButtonRef,
|
||||
|
|
@ -282,6 +294,8 @@ function DocSearchInner(
|
|||
isHybridModeSupported,
|
||||
}),
|
||||
[
|
||||
appId,
|
||||
apiKey,
|
||||
docsearchState,
|
||||
searchButtonRef,
|
||||
initialQuery,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,18 @@ describe('@docsearch/core', () => {
|
|||
expect(screen.getByText('State: ready')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('provides credential defaults to children', () => {
|
||||
const { result } = renderCustomHook({
|
||||
appId: 'app-id',
|
||||
apiKey: 'api-key',
|
||||
});
|
||||
|
||||
expect(result.current).toMatchObject({
|
||||
appId: 'app-id',
|
||||
apiKey: 'api-key',
|
||||
});
|
||||
});
|
||||
|
||||
it('updates state from children', async () => {
|
||||
const Comp = (): JSX.Element => {
|
||||
const { docsearchState, openModal } = useDocSearch();
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { createPortal } from 'react-dom';
|
|||
|
||||
export type DocSearchAskAiModalProps = Omit<
|
||||
ReactDocSearchAskAiModalProps,
|
||||
| 'appId'
|
||||
| 'apiKey'
|
||||
| 'initialScrollY'
|
||||
| 'isAskAiActive'
|
||||
| 'isHybridModeSupported'
|
||||
|
|
@ -14,12 +16,15 @@ export type DocSearchAskAiModalProps = Omit<
|
|||
| 'onAskAiToggle'
|
||||
| 'onClose'
|
||||
| 'theme'
|
||||
>;
|
||||
> &
|
||||
Partial<Pick<ReactDocSearchAskAiModalProps, 'appId' | 'apiKey'>>;
|
||||
|
||||
export function DocSearchAskAiModal(
|
||||
props: DocSearchAskAiModalProps
|
||||
): JSX.Element | null {
|
||||
const {
|
||||
appId: providerAppId,
|
||||
apiKey: providerApiKey,
|
||||
isModalActive,
|
||||
onAskAiToggle,
|
||||
closeModal,
|
||||
|
|
@ -29,6 +34,15 @@ export function DocSearchAskAiModal(
|
|||
isHybridModeSupported,
|
||||
} = useDocSearch();
|
||||
|
||||
const appId = props.appId ?? providerAppId;
|
||||
const apiKey = props.apiKey ?? providerApiKey;
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error(
|
||||
'`DocSearchAskAiModal` requires `appId` and `apiKey` props or values configured on the `DocSearch` provider.'
|
||||
);
|
||||
}
|
||||
|
||||
const containerElement = React.useMemo(
|
||||
() => props.portalContainer ?? document.body,
|
||||
[props.portalContainer]
|
||||
|
|
@ -43,6 +57,8 @@ export function DocSearchAskAiModal(
|
|||
const modalProps: ReactDocSearchAskAiModalProps = React.useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
appId,
|
||||
apiKey,
|
||||
isAskAiActive,
|
||||
initialQuery: props.initialQuery ?? initialQuery,
|
||||
initialScrollY: initialScroll,
|
||||
|
|
@ -52,6 +68,8 @@ export function DocSearchAskAiModal(
|
|||
}),
|
||||
[
|
||||
props,
|
||||
appId,
|
||||
apiKey,
|
||||
isAskAiActive,
|
||||
initialQuery,
|
||||
initialScroll,
|
||||
|
|
|
|||
|
|
@ -7,12 +7,33 @@ import { createPortal } from 'react-dom';
|
|||
|
||||
export type DocSearchModalProps = Omit<
|
||||
ReactDocSearchModalProps,
|
||||
'initialScrollY' | 'keyboardShortcuts' | 'onClose' | 'theme'
|
||||
>;
|
||||
| 'appId'
|
||||
| 'apiKey'
|
||||
| 'initialScrollY'
|
||||
| 'keyboardShortcuts'
|
||||
| 'onClose'
|
||||
| 'theme'
|
||||
> &
|
||||
Partial<Pick<ReactDocSearchModalProps, 'appId' | 'apiKey'>>;
|
||||
|
||||
export function DocSearchModal(props: DocSearchModalProps): JSX.Element | null {
|
||||
const { isModalActive, closeModal, initialQuery, registerView } =
|
||||
useDocSearch();
|
||||
const {
|
||||
appId: providerAppId,
|
||||
apiKey: providerApiKey,
|
||||
isModalActive,
|
||||
closeModal,
|
||||
initialQuery,
|
||||
registerView,
|
||||
} = useDocSearch();
|
||||
|
||||
const appId = props.appId ?? providerAppId;
|
||||
const apiKey = props.apiKey ?? providerApiKey;
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error(
|
||||
'`DocSearchModal` requires `appId` and `apiKey` props or values configured on the `DocSearch` provider.'
|
||||
);
|
||||
}
|
||||
|
||||
const containerElement = React.useMemo(
|
||||
() => props.portalContainer ?? document.body,
|
||||
|
|
@ -28,11 +49,13 @@ export function DocSearchModal(props: DocSearchModalProps): JSX.Element | null {
|
|||
const modalProps: ReactDocSearchModalProps = React.useMemo(
|
||||
() => ({
|
||||
...props,
|
||||
appId,
|
||||
apiKey,
|
||||
initialQuery: props.initialQuery ?? initialQuery,
|
||||
initialScrollY: initialScroll,
|
||||
onClose: closeModal,
|
||||
}),
|
||||
[props, initialQuery, initialScroll, closeModal]
|
||||
[props, appId, apiKey, initialQuery, initialScroll, closeModal]
|
||||
);
|
||||
|
||||
return isModalActive
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ const DEFAULT_PROPS: DocSearchModalProps = {
|
|||
},
|
||||
};
|
||||
|
||||
const PROVIDER_DEFAULT_PROPS: DocSearchModalProps = {
|
||||
indices: [INDEX_NAME],
|
||||
transformSearchClient: DEFAULT_PROPS.transformSearchClient,
|
||||
};
|
||||
|
||||
const renderComponent = (
|
||||
props: DocSearchModalProps = DEFAULT_PROPS,
|
||||
docsearchProps: Omit<DocSearchProps, 'children'> = {}
|
||||
|
|
@ -67,6 +72,22 @@ describe('@docsearch/modal', () => {
|
|||
renderComponent();
|
||||
});
|
||||
|
||||
it('uses credentials configured on the provider', () => {
|
||||
renderComponent(PROVIDER_DEFAULT_PROPS, {
|
||||
appId: APP_ID,
|
||||
apiKey: API_KEY,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(document, {
|
||||
key: 'k',
|
||||
ctrlKey: true,
|
||||
});
|
||||
});
|
||||
|
||||
expect(screen.getByText('Search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens modal on keyboard shortcut', () => {
|
||||
renderComponent();
|
||||
|
||||
|
|
|
|||
|
|
@ -157,11 +157,11 @@ export interface DocSearchProps {
|
|||
}
|
||||
|
||||
function DocSearchComponent(
|
||||
props: DocSearchProps,
|
||||
{ appId, apiKey, ...props }: DocSearchProps,
|
||||
ref: React.ForwardedRef<DocSearchRef>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<DocSearchProvider {...props} ref={ref}>
|
||||
<DocSearchProvider {...props} appId={appId} apiKey={apiKey} ref={ref}>
|
||||
<DocSearchInner {...props} />
|
||||
</DocSearchProvider>
|
||||
);
|
||||
|
|
@ -169,7 +169,9 @@ function DocSearchComponent(
|
|||
|
||||
export const DocSearch = React.forwardRef(DocSearchComponent);
|
||||
|
||||
export function DocSearchInner(props: DocSearchProps): JSX.Element {
|
||||
export function DocSearchInner(
|
||||
props: Omit<DocSearchProps, 'appId' | 'apiKey'>
|
||||
): JSX.Element {
|
||||
const {
|
||||
searchButtonRef,
|
||||
keyboardShortcuts,
|
||||
|
|
@ -177,8 +179,14 @@ export function DocSearchInner(props: DocSearchProps): JSX.Element {
|
|||
initialQuery,
|
||||
openModal,
|
||||
closeModal,
|
||||
appId,
|
||||
apiKey,
|
||||
} = useDocSearch();
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error('`DocSearch` requires `appId` and `apiKey` props.');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocSearchButton
|
||||
|
|
@ -191,6 +199,8 @@ export function DocSearchInner(props: DocSearchProps): JSX.Element {
|
|||
createPortal(
|
||||
<DocSearchModal
|
||||
{...props}
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
initialScrollY={window.scrollY}
|
||||
initialQuery={initialQuery}
|
||||
translations={props?.translations?.modal}
|
||||
|
|
|
|||
|
|
@ -154,11 +154,6 @@ export interface PromptSuggestions {
|
|||
}
|
||||
|
||||
export interface DocSearchAskAi {
|
||||
/**
|
||||
* The index name to use for the Ask AI feature. Your assistant will search
|
||||
* for relevant documents. If not provided, the root 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.
|
||||
|
|
@ -169,8 +164,8 @@ export interface DocSearchAskAi {
|
|||
* 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;
|
||||
/** The Agent Studio Agent ID to use for the Ask AI feature. */
|
||||
agentId: string;
|
||||
/**
|
||||
* Enables displaying suggested questions on Ask AI's new conversation screen.
|
||||
*
|
||||
|
|
@ -236,11 +231,16 @@ export interface DocSearchAIProps extends DocSearchProps {
|
|||
}
|
||||
|
||||
function DocSearchAIComponent(
|
||||
props: DocSearchAIProps,
|
||||
{ appId, apiKey, ...props }: DocSearchAIProps,
|
||||
ref: React.ForwardedRef<DocSearchRef>
|
||||
): JSX.Element {
|
||||
return (
|
||||
<DocSearchProvider {...props} ref={ref}>
|
||||
<DocSearchProvider
|
||||
{...props}
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
ref={ref}
|
||||
>
|
||||
<DocSearchAIInner {...props} />
|
||||
</DocSearchProvider>
|
||||
);
|
||||
|
|
@ -248,7 +248,9 @@ function DocSearchAIComponent(
|
|||
|
||||
export const DocSearchAI = React.forwardRef(DocSearchAIComponent);
|
||||
|
||||
export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element {
|
||||
export function DocSearchAIInner(
|
||||
props: Omit<DocSearchAIProps, 'appId' | 'apiKey'>
|
||||
): JSX.Element {
|
||||
const {
|
||||
searchButtonRef,
|
||||
keyboardShortcuts,
|
||||
|
|
@ -259,8 +261,14 @@ export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element {
|
|||
openModal,
|
||||
closeModal,
|
||||
isHybridModeSupported,
|
||||
appId,
|
||||
apiKey,
|
||||
} = useDocSearch();
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error('`DocSearchAI` requires `appId` and `apiKey` props.');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocSearchButton
|
||||
|
|
@ -273,6 +281,8 @@ export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element {
|
|||
createPortal(
|
||||
<DocSearchAskAiModal
|
||||
{...props}
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
initialScrollY={window.scrollY}
|
||||
initialQuery={initialQuery}
|
||||
translations={props?.translations?.modal}
|
||||
|
|
|
|||
|
|
@ -157,12 +157,12 @@ export function DocSearchAskAiModal({
|
|||
|
||||
const askAiConfig = typeof askAi === 'object' ? askAi : null;
|
||||
const askAiConfigurationId = askAiConfig
|
||||
? askAiConfig.assistantId
|
||||
? askAiConfig.agentId
|
||||
: (askAi as string);
|
||||
const askAiSearchParameters = askAiConfig?.searchParameters;
|
||||
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
|
||||
const suggestedQuestions = useSuggestedQuestions({
|
||||
assistantId: askAiConfigurationId,
|
||||
agentId: askAiConfigurationId,
|
||||
searchClient,
|
||||
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
|
||||
});
|
||||
|
|
@ -224,10 +224,9 @@ export function DocSearchAskAiModal({
|
|||
startNewConversation,
|
||||
restoreConversation,
|
||||
} = useAskAi({
|
||||
assistantId: askAiConfigurationId,
|
||||
agentId: askAiConfigurationId,
|
||||
apiKey: askAiConfig?.apiKey || apiKey,
|
||||
appId: askAiConfig?.appId || appId,
|
||||
indexName: askAiConfig?.indexName || defaultIndexName,
|
||||
searchParameters: askAiSearchParameters,
|
||||
tools,
|
||||
memory: askAiConfig?.memory,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { ResultsTranslations } from './Results';
|
|||
import { Results } from './Results';
|
||||
import type { ScreenStateProps } from './ScreenState';
|
||||
import type { InternalDocSearchHit } from './types';
|
||||
import { removeHighlightTags } from './utils';
|
||||
|
||||
export type ResultsScreenTranslations = Partial<{
|
||||
askAiPlaceholder: string;
|
||||
|
|
@ -27,19 +28,6 @@ export function ResultsScreen({
|
|||
resultBadgeKey,
|
||||
...props
|
||||
}: ResultsScreenProps): JSX.Element {
|
||||
const { resultsSectionTitle = 'Results' } = translations;
|
||||
|
||||
const renderIcon = React.useCallback(
|
||||
({ item }: { item: InternalDocSearchHit }) => {
|
||||
return (
|
||||
<div className="DocSearch-Hit-icon">
|
||||
<SourceIcon type={item.type} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderAction = React.useCallback(() => {
|
||||
return (
|
||||
<div className="DocSearch-Hit-action">
|
||||
|
|
@ -70,14 +58,40 @@ export function ResultsScreen({
|
|||
return null;
|
||||
}
|
||||
|
||||
const title = removeHighlightTags(collection.items[0]);
|
||||
|
||||
return (
|
||||
<Results
|
||||
{...props}
|
||||
key={collection.source.sourceId}
|
||||
translations={translations}
|
||||
title={resultsSectionTitle}
|
||||
title={title}
|
||||
collection={collection}
|
||||
renderIcon={renderIcon}
|
||||
renderIcon={({ item, index }) => (
|
||||
<>
|
||||
{item.__docsearch_parent && (
|
||||
<svg className="DocSearch-Hit-Tree" viewBox="0 0 24 54">
|
||||
<g
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{item.__docsearch_parent !==
|
||||
collection.items[index + 1]?.__docsearch_parent ? (
|
||||
<path d="M8 6v21M20 27H8.3" />
|
||||
) : (
|
||||
<path d="M8 6v42M20 27H8.3" />
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
<div className="DocSearch-Hit-icon">
|
||||
<SourceIcon type={item.type} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
renderAction={renderAction}
|
||||
renderResultBadge={renderResultBadge}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -37,17 +37,12 @@ export type SidepanelSearchParameters = {
|
|||
};
|
||||
|
||||
export type DocSearchSidepanelProps = DocSearchCallbacks & {
|
||||
/** The assistant ID to use for the ask AI feature. */
|
||||
assistantId: string;
|
||||
/** Public api key with search permissions for the index. */
|
||||
/** The Agent Studio agent ID to use for Ask AI. */
|
||||
agentId: string;
|
||||
/** Search only API key. */
|
||||
apiKey: string;
|
||||
/** Algolia application id used by the search client. */
|
||||
/** Algolia application ID that hosts the Agent Studio agent. */
|
||||
appId: string;
|
||||
/**
|
||||
* The index name to use for the ask AI feature. Your assistant will search
|
||||
* this index for relevant documents.
|
||||
*/
|
||||
indexName: string;
|
||||
/**
|
||||
* Configuration for keyboard shortcuts. Allows enabling/disabling specific
|
||||
* shortcuts.
|
||||
|
|
@ -103,6 +98,8 @@ function DocSearchSidepanelComponent(
|
|||
): JSX.Element {
|
||||
return (
|
||||
<DocSearch
|
||||
appId={props.appId}
|
||||
apiKey={props.apiKey}
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
theme={theme}
|
||||
ref={ref}
|
||||
|
|
@ -130,8 +127,16 @@ function DocSearchSidepanelComp({
|
|||
keyboardShortcuts,
|
||||
registerView,
|
||||
initialAskAiMessage,
|
||||
appId,
|
||||
apiKey,
|
||||
} = useDocSearch();
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error(
|
||||
'`Sidepanel` requires `appId` and `apiKey` props or values configured on the `DocSearch` provider.'
|
||||
);
|
||||
}
|
||||
|
||||
const toggleSidepanelState = React.useCallback(() => {
|
||||
setDocsearchState(docsearchState === 'sidepanel' ? 'ready' : 'sidepanel');
|
||||
}, [docsearchState, setDocsearchState]);
|
||||
|
|
|
|||
|
|
@ -134,10 +134,9 @@ function SidepanelInner(
|
|||
isOpen = false,
|
||||
onOpen,
|
||||
onClose,
|
||||
assistantId,
|
||||
agentId,
|
||||
apiKey,
|
||||
appId,
|
||||
indexName,
|
||||
variant = 'floating',
|
||||
searchParameters,
|
||||
pushSelector,
|
||||
|
|
@ -194,8 +193,7 @@ function SidepanelInner(
|
|||
restoreConversation,
|
||||
} = useAskAi({
|
||||
appId,
|
||||
indexName,
|
||||
assistantId,
|
||||
agentId,
|
||||
apiKey,
|
||||
searchParameters,
|
||||
tools,
|
||||
|
|
@ -204,7 +202,7 @@ function SidepanelInner(
|
|||
});
|
||||
|
||||
const suggestedQuestions = useSuggestedQuestions({
|
||||
assistantId,
|
||||
agentId,
|
||||
suggestedQuestionsEnabled,
|
||||
searchClient,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -445,7 +445,7 @@ describe('api', () => {
|
|||
},
|
||||
}}
|
||||
askAi={{
|
||||
assistantId: '123',
|
||||
agentId: '123',
|
||||
promptSuggestions: {
|
||||
indexName: 'prompt-suggestions',
|
||||
},
|
||||
|
|
|
|||
126
packages/docsearch-react/src/__tests__/resultsScreen.test.tsx
Normal file
126
packages/docsearch-react/src/__tests__/resultsScreen.test.tsx
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import { cleanup, render } from '@testing-library/react';
|
||||
import React, { type JSX } from 'react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
import { ResultsScreen } from '../ResultsScreen';
|
||||
import type { InternalDocSearchHit } from '../types';
|
||||
|
||||
function Hit({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
function createHit(
|
||||
objectID: string,
|
||||
lvl0: string,
|
||||
parent: InternalDocSearchHit | null = null
|
||||
): InternalDocSearchHit {
|
||||
const hit: InternalDocSearchHit = {
|
||||
objectID,
|
||||
content: null,
|
||||
url: `/${objectID}`,
|
||||
url_without_anchor: `/${objectID}`,
|
||||
type: parent ? 'content' : 'lvl1',
|
||||
anchor: null,
|
||||
hierarchy: {
|
||||
lvl0,
|
||||
lvl1: 'Installation',
|
||||
lvl2: null,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
_highlightResult: {
|
||||
content: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
hierarchy: {
|
||||
lvl0: { value: lvl0, matchLevel: 'none', matchedWords: [] },
|
||||
lvl1: { value: 'Installation', matchLevel: 'none', matchedWords: [] },
|
||||
lvl2: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl3: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl4: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl5: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl6: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
},
|
||||
hierarchy_camel: [],
|
||||
},
|
||||
_snippetResult: {
|
||||
content: { value: '', matchLevel: 'none' },
|
||||
hierarchy: {
|
||||
lvl0: { value: lvl0, matchLevel: 'none', matchedWords: [] },
|
||||
lvl1: { value: 'Installation', matchLevel: 'none', matchedWords: [] },
|
||||
lvl2: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl3: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl4: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl5: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl6: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
},
|
||||
hierarchy_camel: [],
|
||||
},
|
||||
__docsearch_parent: parent,
|
||||
};
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('ResultsScreen', () => {
|
||||
it('renders lvl0 headings and tree connectors for grouped child hits', () => {
|
||||
const parent = createHit('guide-install', 'Guides');
|
||||
const firstChild = createHit('guide-install-1', 'Guides', parent);
|
||||
const lastChild = createHit('guide-install-2', 'Guides', parent);
|
||||
const reference = createHit('reference-install', 'Reference');
|
||||
|
||||
const props = {
|
||||
state: {
|
||||
activeItemId: null,
|
||||
collections: [
|
||||
{
|
||||
source: { sourceId: 'hits_docs_0' },
|
||||
items: [parent, firstChild, lastChild],
|
||||
},
|
||||
{
|
||||
source: { sourceId: 'hits_docs_1' },
|
||||
items: [reference],
|
||||
},
|
||||
],
|
||||
completion: null,
|
||||
context: {},
|
||||
isOpen: true,
|
||||
query: 'install',
|
||||
status: 'idle',
|
||||
},
|
||||
getItemProps: vi.fn(() => ({})),
|
||||
getListProps: vi.fn(() => ({})),
|
||||
hitComponent: Hit,
|
||||
indexName: 'docs',
|
||||
inputRef: React.createRef<HTMLInputElement>(),
|
||||
onItemClick: vi.fn(),
|
||||
refresh: vi.fn(),
|
||||
setQuery: vi.fn(),
|
||||
recentSearches: { add: vi.fn(), remove: vi.fn() },
|
||||
favoriteSearches: { add: vi.fn(), remove: vi.fn() },
|
||||
conversations: { add: vi.fn(), remove: vi.fn() },
|
||||
disableUserPersonalization: false,
|
||||
hasCollections: true,
|
||||
} as any;
|
||||
|
||||
render(<ResultsScreen {...props} />);
|
||||
|
||||
expect(document.querySelector('.DocSearch-Hit-source')).toHaveTextContent(
|
||||
'Guides'
|
||||
);
|
||||
expect(
|
||||
document.querySelectorAll('.DocSearch-Hit-source')[1]
|
||||
).toHaveTextContent('Reference');
|
||||
|
||||
const connectorPaths = Array.from(
|
||||
document.querySelectorAll('.DocSearch-Hit-Tree path')
|
||||
).map((path) => path.getAttribute('d'));
|
||||
|
||||
expect(connectorPaths).toEqual(['M8 6v42M20 27H8.3', 'M8 6v21M20 27H8.3']);
|
||||
});
|
||||
});
|
||||
|
|
@ -157,8 +157,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {
|
||||
customAction: {
|
||||
onToolCall,
|
||||
|
|
@ -205,8 +204,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {
|
||||
customAction: {
|
||||
onToolCall,
|
||||
|
|
@ -233,10 +231,9 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
tools: {},
|
||||
agentId: 'assistant-id',
|
||||
memory: { userToken: 'secure-user-token' },
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -250,8 +247,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -266,8 +262,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -284,8 +279,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
searchParameters,
|
||||
})
|
||||
|
|
@ -308,8 +302,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
indices,
|
||||
})
|
||||
|
|
@ -335,8 +328,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
searchParameters,
|
||||
indices,
|
||||
|
|
@ -353,8 +345,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
indices: [],
|
||||
})
|
||||
|
|
@ -369,8 +360,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -383,8 +373,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -408,8 +397,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -434,8 +422,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -463,8 +450,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -492,8 +478,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -506,8 +491,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -541,8 +525,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
tools: {},
|
||||
})
|
||||
);
|
||||
|
|
@ -578,8 +561,7 @@ describe('useAskAi', () => {
|
|||
useAskAi({
|
||||
apiKey: 'api-key',
|
||||
appId: 'app-id',
|
||||
assistantId: 'assistant-id',
|
||||
indexName: 'index-name',
|
||||
agentId: 'assistant-id',
|
||||
searchParameters,
|
||||
indices,
|
||||
tools: {},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|||
import type { AIMessage } from '../types/AskiAi';
|
||||
import { extractLinksFromMessage } from '../utils/ai';
|
||||
import { createFacetFilters } from '../utils/createDocSearchSources';
|
||||
import { getFacetLabel, normalizeFacets } from '../utils/facets';
|
||||
import {
|
||||
deriveDefaultSelectedFacetsFromIndex,
|
||||
getFacetLabel,
|
||||
normalizeFacets,
|
||||
} from '../utils/facets';
|
||||
import {
|
||||
createObjectStorage,
|
||||
createStorage,
|
||||
|
|
@ -66,6 +70,34 @@ describe('utils', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('derives default selections from index facet filters', () => {
|
||||
expect(
|
||||
deriveDefaultSelectedFacetsFromIndex([
|
||||
{ name: 'docs' },
|
||||
{
|
||||
name: 'blog',
|
||||
searchParameters: {
|
||||
facetFilters: [
|
||||
'language:en',
|
||||
'version:v2',
|
||||
'invalid',
|
||||
'empty:',
|
||||
':value',
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'api',
|
||||
searchParameters: { facetFilters: ['language:fr'] },
|
||||
},
|
||||
{
|
||||
name: 'guides',
|
||||
searchParameters: { facetFilters: 'format:guide' as never },
|
||||
},
|
||||
])
|
||||
).toEqual({ language: 'fr', version: 'v2', format: 'guide' });
|
||||
});
|
||||
|
||||
it('returns configured facetFilters when no dynamic facets are selected', () => {
|
||||
expect(createFacetFilters(['language:en'], {})).toEqual(['language:en']);
|
||||
});
|
||||
|
|
@ -79,10 +111,21 @@ describe('utils', () => {
|
|||
).toEqual(['docusaurus_tag:default', 'language:en', 'version:v2']);
|
||||
});
|
||||
|
||||
it('ignores empty dynamic facet selections', () => {
|
||||
it('overrides configured filters with dynamic selections for the same facet', () => {
|
||||
expect(
|
||||
createFacetFilters(undefined, { language: '', version: 'v2' })
|
||||
).toEqual(['version:v2']);
|
||||
createFacetFilters(['language:en', 'version:v2'], {
|
||||
language: 'fr',
|
||||
})
|
||||
).toEqual(['version:v2', 'language:fr']);
|
||||
});
|
||||
|
||||
it('removes configured filters for cleared facet selections', () => {
|
||||
expect(
|
||||
createFacetFilters(['language:en', 'version:v2'], {
|
||||
language: '',
|
||||
type: 'guide',
|
||||
})
|
||||
).toEqual(['version:v2', 'type:guide']);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { type JSX } from 'react';
|
|||
import type { DocSearchFacet } from '../DocSearch';
|
||||
import { ChevronIcon } from '../icons';
|
||||
import { capitalize } from '../utils';
|
||||
import type { FacetSelections } from '../utils/createDocSearchSources';
|
||||
import { getFacetLabel } from '../utils/facets';
|
||||
|
||||
import { Chip } from './ui/Chip';
|
||||
|
|
@ -132,7 +133,7 @@ const SelectedFacetChip = React.memo(function SelectedFacetChip({
|
|||
value,
|
||||
dismissAriaLabel,
|
||||
onDismiss,
|
||||
}: SelectedFacetChipProps): JSX.Element {
|
||||
}: SelectedFacetChipProps): JSX.Element | null {
|
||||
const handleDismissFacet = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onDismiss(facetKey, e);
|
||||
|
|
@ -153,7 +154,7 @@ const SelectedFacetChip = React.memo(function SelectedFacetChip({
|
|||
|
||||
interface FacetBarProps {
|
||||
facets: FacetBarFacet[];
|
||||
selections: Record<string, string>;
|
||||
selections: FacetSelections;
|
||||
onSelectionChange: (facet: string, value: string) => void;
|
||||
clearSelections: () => void;
|
||||
translations?: FacetBarTranslations;
|
||||
|
|
@ -174,9 +175,15 @@ export const FacetBar = React.memo(function FacetBar({
|
|||
selectedFacetsAriaLabel = 'Selected search filters',
|
||||
clearFacetAriaLabel = 'Clear filter:',
|
||||
} = translations;
|
||||
const visibleFacetKeys = React.useMemo(
|
||||
() => new Set(facets.map((f) => f.key)),
|
||||
[facets]
|
||||
);
|
||||
const selectionsToDisplay = React.useMemo(() => {
|
||||
return Object.entries(selections).filter(([_, value]) => Boolean(value));
|
||||
}, [selections]);
|
||||
return Object.entries(selections).filter(
|
||||
([key, value]) => Boolean(value) && visibleFacetKeys.has(key)
|
||||
);
|
||||
}, [selections, visibleFacetKeys]);
|
||||
|
||||
const triggerRefs = React.useRef(new Map<string, HTMLButtonElement>());
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ describe('useDocSearchFacets', () => {
|
|||
result.current.clearFacetSelections();
|
||||
});
|
||||
|
||||
expect(result.current.facetSelections).toEqual({});
|
||||
expect(result.current.facetSelectionsRef.current).toEqual({});
|
||||
expect(result.current.facetSelections).toEqual({ language: '' });
|
||||
expect(result.current.facetSelectionsRef.current).toEqual({ language: '' });
|
||||
expect(onSelectionsChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import type { DocSearchFacet, DocSearchIndex } from '../DocSearch';
|
|||
import { useFacetValues } from '../useFacetValues';
|
||||
import type { useSearchClient } from '../useSearchClient';
|
||||
import type { FacetSelections } from '../utils/createDocSearchSources';
|
||||
import { normalizeFacets } from '../utils/facets';
|
||||
import {
|
||||
deriveDefaultSelectedFacetsFromIndex,
|
||||
normalizeFacets,
|
||||
} from '../utils/facets';
|
||||
|
||||
export interface UseDocSearchFacetsProps {
|
||||
facets?: DocSearchFacet[];
|
||||
|
|
@ -38,18 +41,18 @@ export function useDocSearchFacets({
|
|||
() => normalizeFacets(facets),
|
||||
[facets]
|
||||
);
|
||||
const normalizedFacetsRef = React.useRef(normalizedFacets);
|
||||
const facetValues = useFacetValues({
|
||||
facets: normalizedFacets,
|
||||
indexes,
|
||||
searchClient,
|
||||
});
|
||||
const [facetSelections, setFacetSelections] = React.useState<FacetSelections>(
|
||||
{}
|
||||
() => deriveDefaultSelectedFacetsFromIndex(indexes)
|
||||
);
|
||||
const facetSelectionsRef = React.useRef(facetSelections);
|
||||
|
||||
const onSelectionsChangeRef = React.useRef(onSelectionsChange);
|
||||
onSelectionsChangeRef.current = onSelectionsChange;
|
||||
|
||||
const visibleFacets = React.useMemo(
|
||||
() =>
|
||||
|
|
@ -59,6 +62,11 @@ export function useDocSearchFacets({
|
|||
[facetValues, normalizedFacets]
|
||||
);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
normalizedFacetsRef.current = normalizedFacets;
|
||||
onSelectionsChangeRef.current = onSelectionsChange;
|
||||
});
|
||||
|
||||
const applySelections = React.useCallback((next: FacetSelections): void => {
|
||||
facetSelectionsRef.current = next;
|
||||
setFacetSelections(next);
|
||||
|
|
@ -67,15 +75,13 @@ export function useDocSearchFacets({
|
|||
|
||||
const handleFacetSelectionChange = React.useCallback(
|
||||
(facet: string, value: string): void => {
|
||||
if (facetSelectionsRef.current[facet] === value) return;
|
||||
const existing = facetSelectionsRef.current[facet];
|
||||
|
||||
if (existing === value) return;
|
||||
|
||||
const next = { ...facetSelectionsRef.current };
|
||||
|
||||
if (value === '') {
|
||||
delete next[facet];
|
||||
} else {
|
||||
next[facet] = value;
|
||||
}
|
||||
next[facet] = value;
|
||||
|
||||
applySelections(next);
|
||||
},
|
||||
|
|
@ -84,7 +90,15 @@ export function useDocSearchFacets({
|
|||
|
||||
const clearFacetSelections = React.useCallback(() => {
|
||||
if (Object.keys(facetSelectionsRef.current).length === 0) return;
|
||||
applySelections({});
|
||||
const next = {};
|
||||
|
||||
for (const facet of normalizedFacetsRef.current) {
|
||||
if (facetSelectionsRef.current[facet.key]) {
|
||||
next[facet.key] = '';
|
||||
}
|
||||
}
|
||||
|
||||
applySelections(next);
|
||||
}, [applySelections]);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -30,10 +30,9 @@ import type {
|
|||
type UseChat = UseChatHelpers<AIMessage>;
|
||||
|
||||
type UseAskAiParams = {
|
||||
assistantId: string;
|
||||
agentId: string;
|
||||
apiKey: string;
|
||||
appId: string;
|
||||
indexName: string;
|
||||
searchParameters?: AgentStudioSearchParameters;
|
||||
tools: ToolCalls;
|
||||
memory?: Memory;
|
||||
|
|
@ -71,7 +70,7 @@ type UseAskAi = (params: UseAskAiParams) => UseAskAiReturn;
|
|||
|
||||
type AgentStudioTransportParams = Pick<
|
||||
UseAskAiParams,
|
||||
'apiKey' | 'appId' | 'assistantId'
|
||||
'apiKey' | 'appId' | 'agentId'
|
||||
> & {
|
||||
searchParameters?: AgentStudioSearchParameters;
|
||||
userToken?: string;
|
||||
|
|
@ -81,7 +80,7 @@ type AgentStudioTransportParams = Pick<
|
|||
const getAgentStudioTransport = ({
|
||||
appId,
|
||||
apiKey,
|
||||
assistantId,
|
||||
agentId,
|
||||
searchParameters,
|
||||
userToken,
|
||||
indices,
|
||||
|
|
@ -100,7 +99,7 @@ const getAgentStudioTransport = ({
|
|||
}
|
||||
|
||||
return new DefaultChatTransport({
|
||||
api: `${agentStudioBaseUrl(appId)}/agents/${assistantId}/completions?stream=true&compatibilityMode=ai-sdk-5`,
|
||||
api: `${agentStudioBaseUrl(appId)}/agents/${agentId}/completions?stream=true&compatibilityMode=ai-sdk-5`,
|
||||
headers: {
|
||||
'x-algolia-application-id': appId,
|
||||
'x-algolia-api-key': apiKey,
|
||||
|
|
@ -126,7 +125,7 @@ const getAgentStudioTransport = ({
|
|||
};
|
||||
|
||||
export const useAskAi: UseAskAi = ({
|
||||
assistantId,
|
||||
agentId,
|
||||
apiKey,
|
||||
appId,
|
||||
tools = EMPTY_TOOLS,
|
||||
|
|
@ -141,12 +140,12 @@ export const useAskAi: UseAskAi = ({
|
|||
getAgentStudioTransport({
|
||||
apiKey,
|
||||
appId,
|
||||
assistantId,
|
||||
agentId,
|
||||
searchParameters,
|
||||
userToken: memory?.userToken,
|
||||
indices,
|
||||
}),
|
||||
[apiKey, appId, assistantId, searchParameters, memory?.userToken, indices]
|
||||
[apiKey, appId, agentId, searchParameters, memory?.userToken, indices]
|
||||
);
|
||||
|
||||
// Store transport in a ref since it is dependent on unstable dependencies:
|
||||
|
|
@ -224,10 +223,10 @@ export const useAskAi: UseAskAi = ({
|
|||
|
||||
const sendFeedback = useCallback<OnAskAiFeedback>(
|
||||
async (messageId, { thumbs, tags, notes }): Promise<void> => {
|
||||
if (!assistantId) return;
|
||||
if (!agentId) return;
|
||||
|
||||
const res = await postAgentStudioFeedback({
|
||||
agentId: assistantId,
|
||||
agentId,
|
||||
vote: thumbs,
|
||||
messageId,
|
||||
appId,
|
||||
|
|
@ -244,7 +243,7 @@ export const useAskAi: UseAskAi = ({
|
|||
{ tags, notes }
|
||||
);
|
||||
},
|
||||
[assistantId, appId, apiKey, conversations]
|
||||
[agentId, appId, apiKey, conversations]
|
||||
);
|
||||
|
||||
const onStopStreaming = useCallback(async (): Promise<void> => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ export function useFacetValues({
|
|||
const facetKeys = stableFacetKeys ? stableFacetKeys.split(',') : [];
|
||||
|
||||
if (facetKeys.length === 0 || indexes.length === 0) {
|
||||
setFacetValues({});
|
||||
return () => {
|
||||
isMounted = false;
|
||||
};
|
||||
|
|
@ -41,7 +40,6 @@ export function useFacetValues({
|
|||
searchClient
|
||||
.search<DocSearchHit>({
|
||||
requests: indexes.map((index) => ({
|
||||
...index.searchParameters,
|
||||
indexName: index.name,
|
||||
query: '',
|
||||
hitsPerPage: 0,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ import type {
|
|||
} from '.';
|
||||
|
||||
type UseSuggestedQuestionsProps = {
|
||||
assistantId: string | null;
|
||||
agentId: string | null;
|
||||
searchClient: DocSearchTransformClient;
|
||||
suggestedQuestionsEnabled?: boolean;
|
||||
};
|
||||
|
||||
export const useSuggestedQuestions = ({
|
||||
assistantId,
|
||||
agentId,
|
||||
searchClient,
|
||||
suggestedQuestionsEnabled = false,
|
||||
}: UseSuggestedQuestionsProps): SuggestedQuestionHit[] => {
|
||||
|
|
@ -30,7 +30,7 @@ export const useSuggestedQuestions = ({
|
|||
requests: [
|
||||
{
|
||||
indexName: SUGGESTED_QUETIONS_INDEX_NAME,
|
||||
filters: `state:published AND assistantId:${assistantId}`,
|
||||
filters: `state:published AND assistantId:${agentId}`,
|
||||
hitsPerPage: 3,
|
||||
},
|
||||
],
|
||||
|
|
@ -41,10 +41,10 @@ export const useSuggestedQuestions = ({
|
|||
setSuggestedQuestions(result.hits);
|
||||
};
|
||||
|
||||
if (suggestedQuestionsEnabled && assistantId && assistantId !== '') {
|
||||
if (suggestedQuestionsEnabled && agentId && agentId !== '') {
|
||||
getSuggestedQuestions();
|
||||
}
|
||||
}, [suggestedQuestionsEnabled, assistantId, searchClient]);
|
||||
}, [suggestedQuestionsEnabled, agentId, searchClient]);
|
||||
|
||||
return suggestedQuestions;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DocSearchHit, InternalDocSearchHit } from '../../types';
|
||||
import { buildQuerySources } from '../createDocSearchSources';
|
||||
|
||||
function createHit({
|
||||
objectID,
|
||||
lvl0,
|
||||
lvl1,
|
||||
type,
|
||||
}: {
|
||||
objectID: string;
|
||||
lvl0: string;
|
||||
lvl1: string;
|
||||
type: DocSearchHit['type'];
|
||||
}): DocSearchHit {
|
||||
const hit: DocSearchHit = {
|
||||
objectID,
|
||||
content: null,
|
||||
url: `/${objectID}`,
|
||||
url_without_anchor: `/${objectID}`,
|
||||
type,
|
||||
anchor: null,
|
||||
hierarchy: {
|
||||
lvl0,
|
||||
lvl1,
|
||||
lvl2: null,
|
||||
lvl3: null,
|
||||
lvl4: null,
|
||||
lvl5: null,
|
||||
lvl6: null,
|
||||
},
|
||||
_highlightResult: {
|
||||
content: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
hierarchy: {
|
||||
lvl0: { value: lvl0, matchLevel: 'none', matchedWords: [] },
|
||||
lvl1: { value: lvl1, matchLevel: 'none', matchedWords: [] },
|
||||
lvl2: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl3: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl4: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl5: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl6: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
},
|
||||
hierarchy_camel: [],
|
||||
},
|
||||
_snippetResult: {
|
||||
content: { value: '', matchLevel: 'none' },
|
||||
hierarchy: {
|
||||
lvl0: { value: lvl0, matchLevel: 'none', matchedWords: [] },
|
||||
lvl1: { value: lvl1, matchLevel: 'none', matchedWords: [] },
|
||||
lvl2: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl3: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl4: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl5: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
lvl6: { value: '', matchLevel: 'none', matchedWords: [] },
|
||||
},
|
||||
hierarchy_camel: [],
|
||||
},
|
||||
};
|
||||
|
||||
return hit;
|
||||
}
|
||||
|
||||
describe('buildQuerySources', () => {
|
||||
it('creates a source per lvl0 group and scopes parents to that group', async () => {
|
||||
const sources = await buildQuerySources({
|
||||
query: 'install',
|
||||
state: { context: { searchSuggestions: [] } },
|
||||
setContext: vi.fn(),
|
||||
setStatus: vi.fn(),
|
||||
searchClient: {
|
||||
search: vi.fn().mockResolvedValue({
|
||||
results: [
|
||||
{
|
||||
index: 'docs',
|
||||
nbHits: 4,
|
||||
hits: [
|
||||
createHit({
|
||||
objectID: 'guide-install',
|
||||
lvl0: 'Guides',
|
||||
lvl1: 'Installation',
|
||||
type: 'lvl1',
|
||||
}),
|
||||
createHit({
|
||||
objectID: 'guide-install-content',
|
||||
lvl0: 'Guides',
|
||||
lvl1: 'Installation',
|
||||
type: 'content',
|
||||
}),
|
||||
createHit({
|
||||
objectID: 'reference-install',
|
||||
lvl0: 'Reference',
|
||||
lvl1: 'Installation',
|
||||
type: 'lvl1',
|
||||
}),
|
||||
createHit({
|
||||
objectID: 'reference-install-content',
|
||||
lvl0: 'Reference',
|
||||
lvl1: 'Installation',
|
||||
type: 'content',
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
} as any,
|
||||
indexes: [{ name: 'docs' }],
|
||||
snippetLength: { current: 15 },
|
||||
insights: false,
|
||||
saveRecentSearch: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
facetSelections: { current: {} },
|
||||
});
|
||||
|
||||
expect(sources.map((source) => source.sourceId)).toEqual([
|
||||
'hits_docs_0',
|
||||
'hits_docs_1',
|
||||
]);
|
||||
|
||||
const [guideSource, referenceSource] = sources;
|
||||
const guideItems = (await guideSource.getItems(
|
||||
{} as never
|
||||
)) as InternalDocSearchHit[];
|
||||
const referenceItems = (await referenceSource.getItems(
|
||||
{} as never
|
||||
)) as InternalDocSearchHit[];
|
||||
|
||||
expect(guideItems.map((item) => item.objectID)).toEqual([
|
||||
'guide-install',
|
||||
'guide-install-content',
|
||||
]);
|
||||
expect(referenceItems.map((item) => item.objectID)).toEqual([
|
||||
'reference-install',
|
||||
'reference-install-content',
|
||||
]);
|
||||
expect(guideItems[1].__docsearch_parent?.objectID).toBe('guide-install');
|
||||
expect(referenceItems[1].__docsearch_parent?.objectID).toBe(
|
||||
'reference-install'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,11 @@ import type {
|
|||
AutocompleteSource,
|
||||
AutocompleteState,
|
||||
} from '@algolia/autocomplete-core';
|
||||
import type { SearchParamsObject, SearchResponse } from 'algoliasearch/lite';
|
||||
import type {
|
||||
FacetFilters,
|
||||
SearchParamsObject,
|
||||
SearchResponse,
|
||||
} from 'algoliasearch/lite';
|
||||
import type React from 'react';
|
||||
|
||||
import type { DocSearchIndex, DocSearchProps } from '../DocSearch';
|
||||
|
|
@ -34,19 +38,43 @@ export function createFacetFilters(
|
|||
searchParametersFacetFilters: SearchParamsObject['facetFilters'],
|
||||
facetSelections: FacetSelections
|
||||
): SearchParamsObject['facetFilters'] {
|
||||
const dynamicFacetFilters = Object.entries(facetSelections)
|
||||
.filter(([, value]) => value)
|
||||
.map(([facet, value]) => `${facet}:${value}`);
|
||||
const selections = Object.entries(facetSelections);
|
||||
const selectedFacets = new Set(selections.map(([facet]) => facet));
|
||||
const dynamicFacetFilters: string[] = [];
|
||||
|
||||
if (dynamicFacetFilters.length === 0) {
|
||||
for (const [facet, selection] of selections) {
|
||||
if (selection !== '') {
|
||||
dynamicFacetFilters.push(`${facet}:${selection}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFacets.size === 0) {
|
||||
return searchParametersFacetFilters;
|
||||
}
|
||||
|
||||
if (!searchParametersFacetFilters) {
|
||||
return dynamicFacetFilters;
|
||||
let configuredFacetFilters: FacetFilters = [];
|
||||
|
||||
if (
|
||||
searchParametersFacetFilters &&
|
||||
Array.isArray(searchParametersFacetFilters)
|
||||
) {
|
||||
configuredFacetFilters = searchParametersFacetFilters;
|
||||
} else if (searchParametersFacetFilters) {
|
||||
configuredFacetFilters = [searchParametersFacetFilters];
|
||||
}
|
||||
|
||||
return [...searchParametersFacetFilters, ...dynamicFacetFilters];
|
||||
const remainingFacetFilters = configuredFacetFilters.filter((facetFilter) => {
|
||||
if (typeof facetFilter !== 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const separatorIndex = facetFilter.indexOf(':');
|
||||
const facet = facetFilter.slice(0, separatorIndex);
|
||||
|
||||
return separatorIndex <= 0 || !selectedFacets.has(facet);
|
||||
});
|
||||
|
||||
return [...remainingFacetFilters, ...dynamicFacetFilters];
|
||||
}
|
||||
|
||||
export function buildNoQuerySources({
|
||||
|
|
@ -221,10 +249,8 @@ export async function buildQuerySources({
|
|||
};
|
||||
}
|
||||
|
||||
const items = Object.values(sources).flat();
|
||||
|
||||
return {
|
||||
sourceId: `hits_${result.index}`,
|
||||
return Object.values<DocSearchHit[]>(sources).map((items, index) => ({
|
||||
sourceId: `hits_${result.index}_${index}`,
|
||||
onSelect({ item, event }): void {
|
||||
saveRecentSearch(item);
|
||||
if (!isModifierEvent(event)) {
|
||||
|
|
@ -234,36 +260,34 @@ export async function buildQuerySources({
|
|||
getItemUrl({ item }): string {
|
||||
return item.url;
|
||||
},
|
||||
getItems() {
|
||||
getItems(): InternalDocSearchHit[] {
|
||||
return Object.values(
|
||||
groupBy(items, (item) => item.hierarchy.lvl1, maxResultsPerGroup)
|
||||
)
|
||||
.map((groupedHits) =>
|
||||
groupedHits
|
||||
.map((item) => {
|
||||
let parent: InternalDocSearchHit | null = null;
|
||||
groupedHits.map((item) => {
|
||||
let parent: InternalDocSearchHit | null = null;
|
||||
|
||||
const potentialParent = groupedHits.find(
|
||||
(siblingItem) =>
|
||||
siblingItem.type === 'lvl1' &&
|
||||
siblingItem.hierarchy.lvl1 === item.hierarchy.lvl1
|
||||
) as InternalDocSearchHit | undefined;
|
||||
const potentialParent = groupedHits.find(
|
||||
(siblingItem) =>
|
||||
siblingItem.type === 'lvl1' &&
|
||||
siblingItem.hierarchy.lvl1 === item.hierarchy.lvl1
|
||||
) as InternalDocSearchHit | undefined;
|
||||
|
||||
if (item.type !== 'lvl1' && potentialParent) {
|
||||
parent = potentialParent;
|
||||
}
|
||||
if (item.type !== 'lvl1' && potentialParent) {
|
||||
parent = potentialParent;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
__docsearch_parent: parent,
|
||||
...insightsParams,
|
||||
};
|
||||
})
|
||||
.flat()
|
||||
return {
|
||||
...item,
|
||||
__docsearch_parent: parent,
|
||||
...insightsParams,
|
||||
};
|
||||
})
|
||||
)
|
||||
.flat();
|
||||
},
|
||||
};
|
||||
}));
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).name === 'RetryError') {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { DocSearchFacet } from '../DocSearch';
|
||||
import type { DocSearchFacet, DocSearchIndex } from '../DocSearch';
|
||||
|
||||
import type { FacetSelections } from './createDocSearchSources';
|
||||
|
||||
export const MAX_FACETS = 5;
|
||||
|
||||
|
|
@ -40,3 +42,36 @@ export function getFacetLabel(facet: DocSearchFacet): string {
|
|||
.replace(/[._-]+/g, ' ')
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
export function deriveDefaultSelectedFacetsFromIndex(
|
||||
indices: DocSearchIndex[]
|
||||
): FacetSelections {
|
||||
const defaultFacets: FacetSelections = {};
|
||||
|
||||
for (const index of indices) {
|
||||
const facetFilters = index.searchParameters?.facetFilters;
|
||||
|
||||
for (const facetFilter of Array.isArray(facetFilters)
|
||||
? facetFilters
|
||||
: [facetFilters]) {
|
||||
if (typeof facetFilter !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const separatorIndex = facetFilter.indexOf(':');
|
||||
|
||||
if (separatorIndex <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = facetFilter.slice(0, separatorIndex);
|
||||
const value = facetFilter.slice(separatorIndex + 1);
|
||||
|
||||
if (key && value) {
|
||||
defaultFacets[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultFacets;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,9 +36,8 @@ function App() {
|
|||
<SidepanelButton />
|
||||
<Sidepanel
|
||||
appId="YOUR_APP_ID"
|
||||
indexName="YOUR_INDEX_NAME"
|
||||
apiKey="YOUR_SEARCH_API_KEY"
|
||||
assistantId="YOUR_ASK_AI_ASSISTANT_ID"
|
||||
agentId="YOUR_ASK_AI_ASSISTANT_ID"
|
||||
/>
|
||||
</DocSearch>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,11 @@ import type { JSX } from 'react';
|
|||
import { createPortal } from 'react-dom';
|
||||
|
||||
export type SidepanelProps = DocSearchSidepanelProps['panel'] &
|
||||
Omit<DocSearchSidepanelProps, 'button' | 'panel' | 'theme'> &
|
||||
Omit<
|
||||
DocSearchSidepanelProps,
|
||||
'appId' | 'apiKey' | 'button' | 'panel' | 'theme'
|
||||
> &
|
||||
Partial<Pick<DocSearchSidepanelProps, 'appId' | 'apiKey'>> &
|
||||
SidepanelSearchParameters;
|
||||
|
||||
export function Sidepanel({
|
||||
|
|
@ -17,6 +21,8 @@ export function Sidepanel({
|
|||
...props
|
||||
}: SidepanelProps): JSX.Element {
|
||||
const {
|
||||
appId: providerAppId,
|
||||
apiKey: providerApiKey,
|
||||
docsearchState,
|
||||
setDocsearchState,
|
||||
keyboardShortcuts,
|
||||
|
|
@ -24,6 +30,15 @@ export function Sidepanel({
|
|||
initialAskAiMessage,
|
||||
} = useDocSearch();
|
||||
|
||||
const appId = props.appId ?? providerAppId;
|
||||
const apiKey = props.apiKey ?? providerApiKey;
|
||||
|
||||
if (!appId || !apiKey) {
|
||||
throw new Error(
|
||||
'`Sidepanel` requires `appId` and `apiKey` props or values configured on the `DocSearch` provider.'
|
||||
);
|
||||
}
|
||||
|
||||
const handleOpen = React.useCallback((): void => {
|
||||
setDocsearchState('sidepanel');
|
||||
}, [setDocsearchState]);
|
||||
|
|
@ -49,12 +64,16 @@ export function Sidepanel({
|
|||
keyboardShortcuts,
|
||||
initialMessage: initialAskAiMessage,
|
||||
...props,
|
||||
appId,
|
||||
apiKey,
|
||||
}),
|
||||
[
|
||||
docsearchState,
|
||||
handleOpen,
|
||||
handleClose,
|
||||
props,
|
||||
appId,
|
||||
apiKey,
|
||||
keyboardShortcuts,
|
||||
initialAskAiMessage,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ bun add @docsearch/core@^5.0.0-beta @docsearch/modal@^5.0.0-beta @docsearch/css@
|
|||
|
||||
## Add keyword search
|
||||
|
||||
Wrap the button and keyword modal in one `DocSearch` provider. Pass a public search-only API key. Prefer `indices` over the deprecated `indexName` and `searchParameters` props.
|
||||
Wrap the button and keyword modal in one `DocSearch` provider. Pass a public search-only API key to the provider; its descendants use these credentials by default.
|
||||
|
||||
```tsx title="KeywordSearch.tsx"
|
||||
import { DocSearch } from '@docsearch/core';
|
||||
|
|
@ -85,14 +85,14 @@ export function KeywordSearch({
|
|||
indexName,
|
||||
}: KeywordSearchProps): JSX.Element {
|
||||
return (
|
||||
<DocSearch>
|
||||
<DocSearch appId={appId} apiKey={apiKey}>
|
||||
<DocSearchButton
|
||||
translations={{
|
||||
buttonText: 'Search docs',
|
||||
buttonAriaLabel: 'Search documentation',
|
||||
}}
|
||||
/>
|
||||
<DocSearchModal appId={appId} apiKey={apiKey} indices={[indexName]} />
|
||||
<DocSearchModal indices={[indexName]} />
|
||||
</DocSearch>
|
||||
);
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ The provider opens the modal when a user selects the button, presses <kbd>Ctrl</
|
|||
|
||||
## Add keyword search and Ask AI
|
||||
|
||||
Replace `DocSearchModal` with `DocSearchAskAiModal`. Create the assistant in [Agent Studio](/docs/agent-studio/getting-started), then pass its ID through `askAi`.
|
||||
Replace `DocSearchModal` with `DocSearchAskAiModal`. Create the assistant in [Agent Studio](/docs/agent-studio/getting-started), then configure its ID on the provider.
|
||||
|
||||
```tsx title="SearchWithAskAi.tsx"
|
||||
import { DocSearch } from '@docsearch/core';
|
||||
|
|
@ -115,30 +115,28 @@ interface SearchWithAskAiProps {
|
|||
appId: string;
|
||||
apiKey: string;
|
||||
indexName: string;
|
||||
assistantId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function SearchWithAskAi({
|
||||
appId,
|
||||
apiKey,
|
||||
indexName,
|
||||
assistantId,
|
||||
agentId,
|
||||
}: SearchWithAskAiProps): JSX.Element {
|
||||
return (
|
||||
<DocSearch>
|
||||
<DocSearch appId={appId} apiKey={apiKey}>
|
||||
<DocSearchButton />
|
||||
<DocSearchAskAiModal
|
||||
appId={appId}
|
||||
apiKey={apiKey}
|
||||
indices={[indexName]}
|
||||
askAi={{ assistantId }}
|
||||
askAi={agentId}
|
||||
/>
|
||||
</DocSearch>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The `askAi` prop also accepts an assistant ID string. Use the object form when you need options such as `indices`, `searchParameters`, `suggestedQuestions`, `promptSuggestions`, `tools`, or `memory`. See the [React package reference](/docs/packages/react/api-reference) for those option types.
|
||||
The `askAi` prop accepts an assistant ID string or an object with `agentId`. Use the object form when you need options such as `indices`, `searchParameters`, `suggestedQuestions`, `promptSuggestions`, `tools`, or `memory`. Set `appId` and `apiKey` on an individual modal or Sidepanel only when they must override the provider's defaults.
|
||||
|
||||
## Understand the shared state
|
||||
|
||||
|
|
@ -200,7 +198,7 @@ interface ControlledSearchProps {
|
|||
appId: string;
|
||||
apiKey: string;
|
||||
indexName: string;
|
||||
assistantId: string;
|
||||
agentId: string;
|
||||
onReady?: () => void;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
|
|
@ -231,7 +229,7 @@ export function ControlledSearch(props: ControlledSearchProps): JSX.Element {
|
|||
appId={props.appId}
|
||||
apiKey={props.apiKey}
|
||||
indices={[props.indexName]}
|
||||
askAi={{ assistantId: props.assistantId }}
|
||||
askAi={props.agentId}
|
||||
/>
|
||||
</DocSearch>
|
||||
);
|
||||
|
|
@ -339,7 +337,7 @@ interface LazySearchProps {
|
|||
appId: string;
|
||||
apiKey: string;
|
||||
indexName: string;
|
||||
assistantId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export function LazySearch(props: LazySearchProps): JSX.Element {
|
||||
|
|
@ -354,7 +352,7 @@ export function LazySearch(props: LazySearchProps): JSX.Element {
|
|||
appId={props.appId}
|
||||
apiKey={props.apiKey}
|
||||
indices={[props.indexName]}
|
||||
askAi={{ assistantId: props.assistantId }}
|
||||
askAi={props.agentId}
|
||||
/>
|
||||
</DocSearch>
|
||||
);
|
||||
|
|
@ -401,12 +399,12 @@ Add `@docsearch/css/dist/_askai.css` when you use `DocSearchAskAiModal`. Bundler
|
|||
|
||||
| API | Required configuration | Provider-managed behavior |
|
||||
| --- | --- | --- |
|
||||
| `DocSearch` | `children` | State, theme, initial query, shortcuts, focus restoration, lifecycle callbacks, and `DocSearchRef` |
|
||||
| `DocSearch` | `children` | State, credentials defaults, theme, initial query, shortcuts, focus restoration, lifecycle callbacks, and `DocSearchRef` |
|
||||
| `DocSearchButton` | None | Button ref, theme, shortcuts, and opening keyword search |
|
||||
| `DocSearchModal` | `appId`, `apiKey`, and at least one `indices` entry or deprecated `indexName` | Open state, close action, initial scroll position, initial query, theme, and shortcuts |
|
||||
| `DocSearchModal` | At least one `indices` entry or deprecated `indexName`; `appId` and `apiKey` must be set here or on `DocSearch` | Open state, close action, initial scroll position, initial query, theme, and shortcuts |
|
||||
| `DocSearchAskAiModal` | The keyword modal configuration plus `askAi` | Keyword modal behavior, Ask AI state, Ask AI transitions, and hybrid detection |
|
||||
| `useDocSearch` | A parent `DocSearch` provider | Reads the context and throws when used outside the provider |
|
||||
|
||||
`DocSearchButton` accepts native React button props and `translations`. The connected wrapper doesn't accept `theme` or `keyboardShortcuts`; set those on `DocSearch`.
|
||||
|
||||
Both connected modal wrappers accept the corresponding low-level modal options, except the state and lifecycle fields supplied by the provider. Consult the [modal API](/docs/packages/modal/api) before adding options.
|
||||
Both connected modal wrappers accept the corresponding low-level modal options, except the state and lifecycle fields supplied by the provider. Explicit `appId` and `apiKey` props override their respective provider values. Consult the [modal API](/docs/packages/modal/api) before adding options.
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ V5 adds `askAi.indices`, `askAi.tools`, `askAi.memory`, and `askAi.promptSuggest
|
|||
|
||||
## 4. Move keyword configuration to `indices`
|
||||
|
||||
`indexName` and root `searchParameters` still work in v5, but both are deprecated.
|
||||
`indexName` and root `searchParameters` both have been removed in `v5`.
|
||||
|
||||
```diff title="app.js"
|
||||
docsearch({
|
||||
|
|
@ -223,7 +223,7 @@ V5 adds `askAi.indices`, `askAi.tools`, `askAi.memory`, and `askAi.promptSuggest
|
|||
});
|
||||
```
|
||||
|
||||
Use one item per index. DocSearch queries them in array order. If you temporarily pass both `indexName` and `indices`, it queries `indexName` first, so remove the old option to avoid duplicate requests.
|
||||
Use one item per index. DocSearch queries them in array order.
|
||||
|
||||
## 5. Update result customization
|
||||
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Public API key with search permission.
|
|||
|
||||
> `type: Array<string | DocSearchIndex>` | **optional**
|
||||
|
||||
Indices to search in display order. Provide `indices` or the deprecated [`indexName`](#indexname).
|
||||
Indices to search in display order.
|
||||
|
||||
```tsx title="Search.tsx"
|
||||
<DocSearch
|
||||
|
|
@ -61,12 +61,6 @@ Indices to search in display order. Provide `indices` or the deprecated [`indexN
|
|||
/>
|
||||
```
|
||||
|
||||
### `indexName`
|
||||
|
||||
> `type: string` | **optional** | **deprecated**
|
||||
|
||||
Single index to search. Use [`indices`](#indices) instead. If you pass both options, DocSearch queries `indexName` first.
|
||||
|
||||
### `facets`
|
||||
|
||||
> `type: DocSearchFacet[]` | **optional**
|
||||
|
|
@ -87,12 +81,6 @@ Theme written to `document.documentElement.dataset.theme`. By default, DocSearch
|
|||
|
||||
Search input placeholder. The active experience supplies the default.
|
||||
|
||||
### `searchParameters`
|
||||
|
||||
> `type: SearchParamsObject` | **optional** | **deprecated**
|
||||
|
||||
Search parameters for `indexName`. Set `searchParameters` on an [`indices`](#indices) item instead.
|
||||
|
||||
### `maxResultsPerGroup`
|
||||
|
||||
> `type: number` | **optional**
|
||||
|
|
@ -250,11 +238,11 @@ Primitive values render as text and arrays of primitives render as a comma-separ
|
|||
|
||||
Agent Studio assistant ID or configuration. This prop is required by `DocSearchAI`.
|
||||
|
||||
#### `assistantId`
|
||||
#### `agentId`
|
||||
|
||||
> `type: string` | **required**
|
||||
|
||||
Agent Studio assistant ID.
|
||||
Agent Studio agent ID.
|
||||
|
||||
#### `appId`
|
||||
|
||||
|
|
@ -268,12 +256,6 @@ Application ID for Agent Studio. Defaults to the root `appId`.
|
|||
|
||||
API key for Agent Studio. Defaults to the root `apiKey`.
|
||||
|
||||
#### `indexName`
|
||||
|
||||
> `type: string` | **optional**
|
||||
|
||||
Index for Agent Studio. Defaults to the first normalized keyword index.
|
||||
|
||||
#### `suggestedQuestions`
|
||||
|
||||
> `type: boolean` | **optional**
|
||||
|
|
@ -289,7 +271,7 @@ Search parameters keyed by index name. Each value supports `filters`, `attribute
|
|||
```tsx title="Search.tsx"
|
||||
<DocSearchAI
|
||||
askAi={{
|
||||
assistantId: 'YOUR_ASSISTANT_ID',
|
||||
agentId: 'YOUR_AGENT_ID',
|
||||
searchParameters: {
|
||||
docs: {
|
||||
filters: 'language:en',
|
||||
|
|
@ -392,7 +374,6 @@ Opens a registered Sidepanel. Does nothing when no Sidepanel is registered.
|
|||
- `react`, `react-dom`, and `@types/react` support versions `>=16.8.0 <20.0.0`.
|
||||
- `search-insights` supports versions `>=1 <3` and is optional.
|
||||
- The browser build targets ES2017.
|
||||
- `indexName` and root `searchParameters` remain supported but are deprecated. Move to `indices`.
|
||||
- `UseDocSearchKeyboardEventsProps.onInput` and `searchButtonRef` remain accepted for compatibility but are deprecated.
|
||||
- `DocSearch` is keyword-only in v5. Use `DocSearchAI` for Ask AI.
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default {
|
|||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||
indices: [{ name: 'docsearch' }],
|
||||
askAi: {
|
||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
agentId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||
},
|
||||
sidePanel: true,
|
||||
contextualSearch: true,
|
||||
|
|
|
|||
|
|
@ -80,17 +80,26 @@ export default function DemoApp() {
|
|||
|
||||
HTMLElement.prototype.focus = function focusNoScroll(options) {
|
||||
if (!userEngaged) return undefined;
|
||||
return nativeFocus.call(this, { preventScroll: true, ...(options ?? {}) });
|
||||
return nativeFocus.call(this, {
|
||||
preventScroll: true,
|
||||
...(options ?? {}),
|
||||
});
|
||||
};
|
||||
|
||||
Element.prototype.scrollIntoView = function containedScrollIntoView(arg) {
|
||||
const behavior = typeof arg === 'object' && arg?.behavior ? arg.behavior : 'auto';
|
||||
const behavior =
|
||||
typeof arg === 'object' && arg?.behavior ? arg.behavior : 'auto';
|
||||
let ancestor = this.parentElement;
|
||||
while (ancestor) {
|
||||
const overflowY = getComputedStyle(ancestor).overflowY;
|
||||
if ((overflowY === 'auto' || overflowY === 'scroll') && ancestor.scrollHeight > ancestor.clientHeight) {
|
||||
if (
|
||||
(overflowY === 'auto' || overflowY === 'scroll') &&
|
||||
ancestor.scrollHeight > ancestor.clientHeight
|
||||
) {
|
||||
const top =
|
||||
this.getBoundingClientRect().top - ancestor.getBoundingClientRect().top + ancestor.scrollTop;
|
||||
this.getBoundingClientRect().top -
|
||||
ancestor.getBoundingClientRect().top +
|
||||
ancestor.scrollTop;
|
||||
ancestor.scrollTo({ top, behavior });
|
||||
return;
|
||||
}
|
||||
|
|
@ -164,10 +173,9 @@ export default function DemoApp() {
|
|||
<DocSearchSidepanel
|
||||
ref={sidepanelRef}
|
||||
theme={theme}
|
||||
indexName={INDEX_NAME}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
assistantId={ASSISTANT_ID}
|
||||
agentId={ASSISTANT_ID}
|
||||
button={{ variant: 'inline' }}
|
||||
panel={{ suggestedQuestions: true }}
|
||||
/>
|
||||
|
|
@ -186,7 +194,7 @@ export default function DemoApp() {
|
|||
indices={[INDEX_NAME]}
|
||||
appId={APP_ID}
|
||||
apiKey={API_KEY}
|
||||
askAi={{ assistantId: ASSISTANT_ID }}
|
||||
askAi={ASSISTANT_ID}
|
||||
navigator={navigator}
|
||||
/>
|
||||
</DocSearch>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ npm install @docsearch/react@4 @docsearch/css@4
|
|||
import { DocSearch } from '@docsearch/react';
|
||||
import '@docsearch/css';
|
||||
|
||||
<DocSearch appId="YOUR_APP_ID" indexName="YOUR_INDEX" apiKey="YOUR_SEARCH_KEY" />
|
||||
<DocSearch appId="YOUR_APP_ID" indices=["YOUR_INDEX"] apiKey="YOUR_SEARCH_KEY" />
|
||||
\`\`\`
|
||||
|
||||
### Docusaurus (recommended)
|
||||
|
|
@ -47,7 +47,7 @@ themeConfig: {
|
|||
docsearch: {
|
||||
appId: 'YOUR_APP_ID',
|
||||
apiKey: 'YOUR_SEARCH_KEY',
|
||||
indexName: 'YOUR_INDEX',
|
||||
indices: ['YOUR_INDEX'],
|
||||
},
|
||||
},
|
||||
\`\`\`
|
||||
|
|
|
|||
Loading…
Reference in a new issue