From 629fce7902f9a574166b44090b8dafb734920eda Mon Sep 17 00:00:00 2001 From: Paul Jankowski <8bittitan@gmail.com> Date: Thu, 16 Jul 2026 11:03:50 -0400 Subject: [PATCH] feat(askai): Move askai related props under root askai (#2919) * feat(askai): Move askai related props under root askai * fix: playwright test case * fix(docusaurus): validate Ask AI options --- .../src/__tests__/validateThemeConfig.test.ts | 144 ++++++++++++++++++ .../src/client/useAlgoliaAskAi.ts | 8 +- .../src/theme-search-algolia.d.ts | 24 ++- .../src/theme/SearchBar/index.tsx | 16 +- .../src/theme/SearchBar/styles.css | 5 + .../src/validateThemeConfig.ts | 89 +++++++---- e2e/search.spec.ts | 21 ++- .../demo-react/src/examples/basic-askai.tsx | 44 +++--- packages/docsearch-react/src/DocSearchAI.tsx | 93 +++++------ .../src/DocSearchAskAiModal.tsx | 9 +- packages/docsearch-react/src/Sidepanel.tsx | 5 + .../src/Sidepanel/Sidepanel.tsx | 4 +- .../src/__tests__/api.test.tsx | 7 +- packages/website/docs/docusaurus-adapter.mdx | 27 ++++ packages/website/docusaurus.config.mjs | 3 + 15 files changed, 382 insertions(+), 117 deletions(-) diff --git a/adapters/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.ts b/adapters/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.ts index 905f096c..0eedd649 100644 --- a/adapters/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.ts +++ b/adapters/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.ts @@ -287,6 +287,58 @@ describe('validateThemeConfig', () => { }); }); + it('accepts sidePanel Agent Studio indices', () => { + const docsearch: DocSearchInput = { + ...minimalDocSearchConfig, + askAi: minimalAskAiConfig, + sidePanel: { + indices: [ + { + index: 'sidepanel-markdown-index', + description: 'Documentation content for the side panel.', + }, + ], + }, + }; + + expect(testValidateThemeConfig(docsearch)).toEqual({ + docsearch: { + ...DEFAULT_CONFIG, + ...docsearch, + }, + }); + }); + + it('rejects empty sidePanel Agent Studio indices', () => { + const docsearch = { + ...minimalDocSearchConfig, + askAi: minimalAskAiConfig, + sidePanel: { + indices: [], + }, + } as unknown as DocSearchInput; + + expectThrowMessage( + () => testValidateThemeConfig(docsearch), + '"docsearch.sidePanel.indices" must contain at least 1 items', + ); + }); + + it('rejects incomplete sidePanel Agent Studio indices', () => { + const docsearch = { + ...minimalDocSearchConfig, + askAi: minimalAskAiConfig, + sidePanel: { + indices: [{ index: 'sidepanel-markdown-index' }], + }, + } as unknown as DocSearchInput; + + expectThrowMessage( + () => testValidateThemeConfig(docsearch), + '"docsearch.sidePanel.indices[0].description" is required', + ); + }); + it('rejects sidePanel without askAi', () => { const docsearch: DocSearchInput = { ...minimalDocSearchConfig, @@ -298,6 +350,98 @@ describe('validateThemeConfig', () => { '`themeConfig.docsearch.sidePanel` requires `themeConfig.docsearch.askAi`.', ); }); + + it('accepts askAi memory object', () => { + const docsearch: DocSearchInput = { + ...minimalDocSearchConfig, + askAi: { + ...minimalAskAiConfig, + memory: { + enabled: true, + userToken: 'b2916249-b172-4ca2-8d0f-663e0f37f85d', + }, + }, + sidePanel: { + variant: 'inline', + side: 'left', + width: 420, + expandedWidth: '60vw', + pushSelector: '#__docusaurus', + hideButton: true, + keyboardShortcuts: { + 'Ctrl/Cmd+I': false, + }, + }, + }; + + expect(testValidateThemeConfig(docsearch)).toEqual({ + docsearch: { + ...DEFAULT_CONFIG, + ...docsearch, + }, + }); + }); + + it('accepts askAi promptSuggestions object', () => { + const docsearch: DocSearchInput = { + ...minimalDocSearchConfig, + askAi: { + ...minimalAskAiConfig, + promptSuggestions: { + indexName: 'test-index', + hitsPerPage: 7, + }, + }, + sidePanel: { + variant: 'inline', + side: 'left', + width: 420, + expandedWidth: '60vw', + pushSelector: '#__docusaurus', + hideButton: true, + keyboardShortcuts: { + 'Ctrl/Cmd+I': false, + }, + }, + }; + + expect(testValidateThemeConfig(docsearch)).toEqual({ + docsearch: { + ...DEFAULT_CONFIG, + ...docsearch, + }, + }); + }); + + it('rejects askAi custom tools definitions', () => { + const docsearch = { + ...minimalDocSearchConfig, + askAi: { + ...minimalAskAiConfig, + tools: {}, + }, + } as unknown as DocSearchInput; + + expectThrowMessage( + () => testValidateThemeConfig(docsearch), + '`themeConfig.docsearch.askAi.tools` is not supported because Docusaurus removes function values', + ); + }); + + it('rejects sidePanel custom tools definitions', () => { + const docsearch = { + ...minimalDocSearchConfig, + askAi: minimalAskAiConfig, + sidePanel: { + tools: {}, + }, + } as unknown as DocSearchInput; + + expectThrowMessage( + () => testValidateThemeConfig(docsearch), + '`themeConfig.docsearch.sidePanel.tools` is not supported because Docusaurus removes function values', + ); + }); }); describe('removed config migration errors', () => { diff --git a/adapters/docusaurus-theme-search-algolia/src/client/useAlgoliaAskAi.ts b/adapters/docusaurus-theme-search-algolia/src/client/useAlgoliaAskAi.ts index 7e246d4e..fbfe6d87 100644 --- a/adapters/docusaurus-theme-search-algolia/src/client/useAlgoliaAskAi.ts +++ b/adapters/docusaurus-theme-search-algolia/src/client/useAlgoliaAskAi.ts @@ -14,12 +14,14 @@ import { mergeFacetFilters } from './utils'; import type { AskAiConfig } from '@docsearch/docusaurus-adapter'; +type AskAiOptions = AskAiConfig & Pick; + // The minimal props the hook needs from DocSearch interface DocSearchPropsLite { apiKey: string; appId: string; indices: NonNullable; - askAi?: AskAiConfig; + askAi?: AskAiOptions; } type UseAskAiResult = { @@ -56,9 +58,9 @@ function applyContextualSearchToAgentStudioIndex( // This can't be done at config normalization time because contextual filters // can only be determined at runtime function applyAskAiContextualSearch( - askAi: AskAiConfig | undefined, + askAi: AskAiOptions | undefined, contextualSearchFilters: FacetFilters | undefined, -): AskAiConfig | undefined { +): AskAiOptions | undefined { if (!askAi) { return undefined; } diff --git a/adapters/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts b/adapters/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts index f85cdea4..8c10607d 100644 --- a/adapters/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts +++ b/adapters/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts @@ -11,10 +11,17 @@ declare module '@docsearch/docusaurus-adapter' { AgentStudioSearchParameters, DocSearchAskAi, DocSearchProps, + ToolCalls, } from '@docsearch/react'; import type { SidepanelProps } from '@docsearch/react/sidepanel'; - type DocusaurusSidePanelConfig = boolean | (SidepanelProps & { hideButton?: boolean }); + type DocusaurusSidePanelConfig = boolean | (Omit & { hideButton?: boolean }); + type DocusaurusSearchBarSidePanelProps = + | boolean + | (Omit & { + hideButton?: boolean; + tools?: ToolCalls; + }); type SearchPageFacetConfig = { /** Algolia attribute to build a refinement list from (e.g. `hierarchy.lvl0`). */ @@ -39,8 +46,12 @@ declare module '@docsearch/docusaurus-adapter' { suggestedQuestions?: DocSearchAskAi['suggestedQuestions']; searchParameters?: AgentStudioSearchParameters; indices?: AgentStudioIndices[]; + memory?: DocSearchAskAi['memory']; + promptSuggestions?: DocSearchAskAi['promptSuggestions']; }; + export type DocusaurusSearchBarAskAiProps = AskAiConfig & Pick; + // DocSearch props that Docusaurus exposes directly through props forwarding type DocusaurusDocSearchProps = Pick< DocSearchProps, @@ -76,6 +87,13 @@ declare module '@docsearch/docusaurus-adapter' { }; }; + export type DocusaurusSearchBarProps = Partial< + Omit & { + askAi?: DocusaurusSearchBarAskAiProps; + sidePanel?: DocusaurusSearchBarSidePanelProps; + } + >; + type UserDocSearchConfig = Omit, 'apiKey' | 'appId' | 'askAi' | 'indices'> & { appId: ThemeConfigDocSearch['appId']; apiKey: ThemeConfigDocSearch['apiKey']; @@ -101,7 +119,9 @@ declare module '@theme/SearchPage' { declare module '@theme/SearchBar' { import type { ReactNode } from 'react'; - export default function SearchBar(): ReactNode; + import type { DocusaurusSearchBarProps } from '@docsearch/docusaurus-adapter'; + + export default function SearchBar(props?: DocusaurusSearchBarProps): ReactNode; } declare module '@theme/SearchTranslations' { diff --git a/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx b/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx index c5d818c3..b8775341 100644 --- a/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx +++ b/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx @@ -20,6 +20,7 @@ import type { DocSearchTranslations, InternalDocSearchHit, StoredDocSearchHit, + ToolCalls, } from '@docsearch/react'; import { SidepanelButton } from '@docsearch/sidepanel/button'; import type { Sidepanel as SidepanelType } from '@docsearch/sidepanel/sidepanel'; @@ -43,12 +44,13 @@ import { useSearchResultUrlProcessor, } from '../../client'; -import type { ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter'; +import type { DocusaurusSearchBarAskAiProps, ThemeConfigDocSearch } from '@docsearch/docusaurus-adapter'; type NavigatorNavigateParams = Parameters['navigate']>>[0]; -type SidePanelOptions = Exclude, boolean>; +type SidePanelOptions = Exclude, boolean> & { tools?: ToolCalls }; type SidePanelPanelOptions = Omit; +type AskAiOptions = NonNullable & Pick; type AdapterDocSearchProps = Omit< DocSearchAskAiModalProps, @@ -61,12 +63,12 @@ type AdapterDocSearchProps = Omit< | 'onClose' | 'searchParameters' > & { - askAi?: ThemeConfigDocSearch['askAi']; + askAi?: AskAiOptions; contextualSearch?: boolean; externalUrlRegex?: string; indices: NonNullable; searchPage: ThemeConfigDocSearch['searchPage']; - sidePanel?: ThemeConfigDocSearch['sidePanel']; + sidePanel?: SidePanelOptions | boolean; translations?: DocSearchTranslations; }; @@ -305,7 +307,7 @@ function DocSearch({ translations: props.translations?.modal ?? translations.modal, indices, }; - const panelOptions = getSidePanelPanelOptions(sidePanelOptions); + const panelOptions = getSidePanelPanelOptions(typeof sidePanel === 'object' ? sidePanel : undefined); return ( <> @@ -356,8 +358,10 @@ function DocSearch({ appId={sidePanelAskAi.appId} indexName={sidePanelAskAi.indexName} searchParameters={sidePanelAskAi.searchParameters} - indices={sidePanelAskAi.indices} + indices={panelOptions.indices ?? sidePanelAskAi.indices} suggestedQuestions={panelOptions.suggestedQuestions ?? sidePanelAskAi.suggestedQuestions} + tools={panelOptions.tools ?? sidePanelAskAi.tools} + memory={panelOptions.memory ?? sidePanelAskAi.memory} /> )} diff --git a/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/styles.css b/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/styles.css index 6eb23aa1..d878924e 100644 --- a/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/styles.css +++ b/adapters/docusaurus-theme-search-algolia/src/theme/SearchBar/styles.css @@ -95,6 +95,10 @@ display: none; } +.DocSearch-Sidepanel .DocSearch-AskAiScreen-Query { + margin: 0; +} + .DocSearch-SidepanelButton.inline { background-color: var(--ifm-background-color); } @@ -117,3 +121,4 @@ .DocSearch-Container { z-index: calc(var(--ifm-z-index-fixed) + 1); } + diff --git a/adapters/docusaurus-theme-search-algolia/src/validateThemeConfig.ts b/adapters/docusaurus-theme-search-algolia/src/validateThemeConfig.ts index 4f834f10..15f98688 100644 --- a/adapters/docusaurus-theme-search-algolia/src/validateThemeConfig.ts +++ b/adapters/docusaurus-theme-search-algolia/src/validateThemeConfig.ts @@ -31,37 +31,6 @@ const SearchParametersSchema = Joi.object({ distinct: Joi.alternatives().try(Joi.boolean(), Joi.number(), Joi.string()).optional(), }).unknown(); -const SidePanelKeyboardShortcutsSchema = Joi.object({ - 'Ctrl/Cmd+I': Joi.boolean().optional(), -}).unknown(false); - -const SidePanelSchema = Joi.object({ - keyboardShortcuts: SidePanelKeyboardShortcutsSchema.optional(), - variant: Joi.string().valid('floating', 'inline').optional(), - side: Joi.string().valid('left', 'right').optional(), - width: Joi.alternatives().try(Joi.number(), Joi.string()).optional(), - expandedWidth: Joi.alternatives().try(Joi.number(), Joi.string()).optional(), - pushSelector: Joi.string().optional(), - suggestedQuestions: Joi.boolean().optional(), - translations: Joi.object().optional().unknown(), - hideButton: Joi.boolean().optional(), - portalContainer: Joi.object().optional().unknown(), -}).unknown(false); - -const KeyboardShortcutsSchema = Joi.object({ - 'Ctrl/Cmd+K': Joi.boolean().optional(), - '/': Joi.boolean().optional(), - 'Ctrl/Cmd+I': Joi.boolean().optional(), -}).unknown(false); - -const IndexSchema = Joi.alternatives().try( - Joi.string(), - Joi.object({ - name: Joi.string().required(), - searchParameters: SearchParametersSchema.optional(), - }).unknown(false), -); - const SearchControlTextParamSchema = Joi.object({ exposed: Joi.boolean().required(), default: Joi.string().optional(), @@ -112,11 +81,56 @@ const AskAiIndexSchema = Joi.object({ searchControls: AgentStudioSearchControlsSchema.optional(), }).unknown(false); +const AskAiMemorySchema = Joi.object({ + enabled: Joi.bool().optional().default(false), + userToken: Joi.string().optional(), +}).unknown(false); + +const SidePanelKeyboardShortcutsSchema = Joi.object({ + 'Ctrl/Cmd+I': Joi.boolean().optional(), +}).unknown(false); + +const SidePanelSchema = Joi.object({ + keyboardShortcuts: SidePanelKeyboardShortcutsSchema.optional(), + variant: Joi.string().valid('floating', 'inline').optional(), + side: Joi.string().valid('left', 'right').optional(), + width: Joi.alternatives().try(Joi.number(), Joi.string()).optional(), + expandedWidth: Joi.alternatives().try(Joi.number(), Joi.string()).optional(), + pushSelector: Joi.string().optional(), + suggestedQuestions: Joi.boolean().optional(), + translations: Joi.object().optional().unknown(), + hideButton: Joi.boolean().optional(), + portalContainer: Joi.object().optional().unknown(), + indices: Joi.array().items(AskAiIndexSchema).min(1).optional(), + memory: AskAiMemorySchema.optional(), +}).unknown(false); + +const KeyboardShortcutsSchema = Joi.object({ + 'Ctrl/Cmd+K': Joi.boolean().optional(), + '/': Joi.boolean().optional(), + 'Ctrl/Cmd+I': Joi.boolean().optional(), +}).unknown(false); + +const IndexSchema = Joi.alternatives().try( + Joi.string(), + Joi.object({ + name: Joi.string().required(), + searchParameters: SearchParametersSchema.optional(), + }).unknown(false), +); + +const AskAiPromptSuggestionsSchema = Joi.object({ + indexName: Joi.string().min(1).required(), + hitsPerPage: Joi.number().positive().optional().default(3), +}).unknown(false); + const AskAiSchema = Joi.object({ assistantId: Joi.string().required(), suggestedQuestions: Joi.boolean().optional(), searchParameters: Joi.object().pattern(Joi.string(), SearchParametersSchema).optional(), indices: Joi.array().items(AskAiIndexSchema).min(1).optional(), + memory: AskAiMemorySchema.optional(), + promptSuggestions: AskAiPromptSuggestionsSchema.optional(), }).unknown(false); const SearchPageFacetSchema = Joi.object({ @@ -219,6 +233,13 @@ function assertNoRemovedKeys(themeConfig: ThemeConfig): void { ); } + const sidePanel = docsearchRecord.sidePanel; + if (sidePanel && typeof sidePanel === 'object' && (sidePanel as Record).tools !== undefined) { + throw new Error( + '`themeConfig.docsearch.sidePanel.tools` is not supported because Docusaurus removes function values when serializing theme config. Pass custom tools through a swizzled `@theme/SearchBar` component instead: use `askAi` for the modal and `sidePanel` for the side panel.', + ); + } + const askAi = docsearchRecord.askAi; if (typeof askAi === 'string') { throw new Error('`themeConfig.docsearch.askAi` must be an object with `assistantId`.'); @@ -247,6 +268,12 @@ function assertNoRemovedKeys(themeConfig: ThemeConfig): void { '`themeConfig.docsearch.askAi.sidePanel` was removed. Use `themeConfig.docsearch.sidePanel` instead.', ); } + + if (askAiRecord.tools !== undefined) { + throw new Error( + '`themeConfig.docsearch.askAi.tools` is not supported because Docusaurus removes function values when serializing theme config. Pass custom tools through a swizzled `@theme/SearchBar` component instead: use `askAi` for the modal and `sidePanel` for the side panel.', + ); + } } function ensureSidePanelHasAskAi(themeConfig: ThemeConfig): void { diff --git a/e2e/search.spec.ts b/e2e/search.spec.ts index 7c2fd828..45d982fe 100644 --- a/e2e/search.spec.ts +++ b/e2e/search.spec.ts @@ -1,5 +1,7 @@ import { test, expect } from './fixtures'; +const MAX_KEYBOARD_NAVIGATION_STEPS = 10; + test.describe('Start', () => { test.beforeEach(async ({ docSearch }) => { await docSearch.goto(); @@ -89,9 +91,22 @@ test.describe('Search', () => { await docSearch.typeQueryMatching(); await expect(docSearch.hits).toBeVisible(); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowDown'); - await page.keyboard.press('ArrowUp'); + const firstHitOption = page.locator('#docsearch-hits_docsearch-list').getByRole('option').first(); + const firstHitOptionId = await firstHitOption.getAttribute('id'); + + expect(firstHitOptionId).not.toBeNull(); + + // Prompt suggestions precede document hits, but the number is not stable. + for (let index = 0; index < MAX_KEYBOARD_NAVIGATION_STEPS; index++) { + if ((await docSearch.input.getAttribute('aria-activedescendant')) === firstHitOptionId) { + break; + } + + await page.keyboard.press('ArrowDown'); + } + + await expect(docSearch.input).toHaveAttribute('aria-activedescendant', firstHitOptionId!); + await expect(firstHitOption).toHaveAttribute('aria-selected', 'true'); await page.keyboard.press('Enter'); await expect(page).not.toHaveURL(initialURL, { timeout: 10000 }); diff --git a/examples/demo-react/src/examples/basic-askai.tsx b/examples/demo-react/src/examples/basic-askai.tsx index fe0cd552..887753aa 100644 --- a/examples/demo-react/src/examples/basic-askai.tsx +++ b/examples/demo-react/src/examples/basic-askai.tsx @@ -1,9 +1,27 @@ /* eslint-disable react/react-in-jsx-scope */ -import { DocSearchAI } from '@docsearch/react'; +import { DocSearchAI, type ToolCalls } from '@docsearch/react'; import type { JSX } from 'react'; import type { DemoTheme } from '../App'; +const customTools: ToolCalls = { + printConsoleMessage: { + render({ message: { output } }) { + if (!output) return ''; + + return output as string; + }, + async onToolCall({ input, addToolOutput }) { + // eslint-disable-next-line no-console + console.log((input as any).message); + + await addToolOutput({ + output: 'Check your console for a nice message :)', + }); + }, + }, +}; + export default function BasicAskAI({ theme }: { theme: DemoTheme }): JSX.Element { return ( ); } diff --git a/packages/docsearch-react/src/DocSearchAI.tsx b/packages/docsearch-react/src/DocSearchAI.tsx index 6f04ba9c..8e3d7bf0 100644 --- a/packages/docsearch-react/src/DocSearchAI.tsx +++ b/packages/docsearch-react/src/DocSearchAI.tsx @@ -127,6 +127,34 @@ export interface AgentStudioIndices { searchControls?: AgentStudioSearchControls; } +export interface Memory { + /** + * Determines whether or not to display the memory based tool calls. + * + * @default false + */ + enabled?: boolean; + /** + * The JWT used by the agent to know which user's memory to read. + * + * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/user-authentication + */ + userToken?: string; +} + +export interface PromptSuggestions { + /** + * The name of the index where the prompt suggestions are stored. + */ + indexName: string; + /** + * The number of prompt suggestions that are retrieved and displayed. + * + * @default 3 + */ + hitsPerPage?: number; +} + export interface DocSearchAskAi { /** * The index name to use for the Ask AI feature. Your assistant will search for relevant documents. @@ -167,34 +195,32 @@ export interface DocSearchAskAi { * List of dynamic indices for the Agent Studio search tool to use. */ indices?: AgentStudioIndices[]; -} - -export interface Memory { /** - * Determines whether or not to display the memory based tool calls. + * Use custom tools driven by Agent Studio. * - * @default false - */ - enabled?: boolean; + * For best performance, memoize this object with `useMemo` or define it + * outside the component. Inline object literals will be recreated every + * render but will not affect correctness. + **/ + tools?: ToolCalls; /** - * The JWT used by the agent to know which user's memory to read. + * Configuration for the Agent Studio memory feature. * - * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/user-authentication - */ - userToken?: string; -} - -export interface PromptSuggestions { - /** - * The name of the index where the prompt suggestions are stored. - */ - indexName: string; - /** - * The number of prompt suggestions that are retrieved and displayed. + * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/memory/overview * - * @default 3 + * @example + * { enabled: true, userToken: '{{SERVER_GENERATED_JWT_TOKEN}}' } */ - hitsPerPage?: number; + memory?: Memory; + /** + * Enables and configures prompt suggestions that are displayed during keyword search. + * + * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/integration#prompt-suggestions + * + * @example + * { indexName: 'docsearch-markdown_prompt_suggestions', hitsPerPage: 1 } + */ + promptSuggestions?: PromptSuggestions; } export interface DocSearchAIProps extends DocSearchProps { @@ -209,27 +235,6 @@ export interface DocSearchAIProps extends DocSearchProps { * Useful to route Ask AI into a different UI (e.g. `@docsearch/sidepanel-js`) without flicker. */ interceptAskAiEvent?: (initialMessage: InitialAskAiMessage) => boolean | void; - /** - * Use custom tools driven by Agent Studio. - * - * For best performance, memoize this object with `useMemo` or define it - * outside the component. Inline object literals will be recreated every - * render but will not affect correctness. - **/ - tools?: ToolCalls; - /** - * Configuration for the Agent Studio memory feature. - */ - memory?: Memory; - /** - * Enables and configures prompt suggestions that are displayed during keyword search. - * - * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/integration#prompt-suggestions - * - * @example - * { indexName: 'docsearch-markdown_prompt_suggestions', hitsPerPage: 1 } - */ - promptSuggestions?: PromptSuggestions; } function DocSearchAIComponent(props: DocSearchAIProps, ref: React.ForwardedRef): JSX.Element { @@ -280,3 +285,5 @@ export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element { ); } + +export type { ToolCalls } from './types/AskiAi'; diff --git a/packages/docsearch-react/src/DocSearchAskAiModal.tsx b/packages/docsearch-react/src/DocSearchAskAiModal.tsx index 99d05d87..c5a47af0 100644 --- a/packages/docsearch-react/src/DocSearchAskAiModal.tsx +++ b/packages/docsearch-react/src/DocSearchAskAiModal.tsx @@ -86,8 +86,6 @@ export function DocSearchAskAiModal({ searchParameters, facets, isHybridModeSupported = false, - tools = EMPTY_TOOLS, - promptSuggestions, ...props }: DocSearchAskAiModalProps): JSX.Element { const { @@ -133,7 +131,8 @@ export function DocSearchAskAiModal({ searchClient, suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions, }); - const memoryEnabled = props.memory?.enabled ?? false; + const memoryEnabled = askAiConfig?.memory?.enabled ?? false; + const tools = askAiConfig?.tools ?? EMPTY_TOOLS; const indexes = React.useMemo( () => @@ -193,7 +192,7 @@ export function DocSearchAskAiModal({ indexName: askAiConfig?.indexName || defaultIndexName, searchParameters: askAiSearchParameters, tools, - memory: props.memory, + memory: askAiConfig?.memory, indices: askAiConfig?.indices, }); @@ -376,7 +375,7 @@ export function DocSearchAskAiModal({ ? buildAskAiActionSources({ query, handleSelectAskAiQuestion, - promptSuggestionsOptions: promptSuggestions, + promptSuggestionsOptions: askAiConfig?.promptSuggestions, searchClient, }) : Promise.resolve([]); diff --git a/packages/docsearch-react/src/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel.tsx index 7ce3c406..cdfe9009 100644 --- a/packages/docsearch-react/src/Sidepanel.tsx +++ b/packages/docsearch-react/src/Sidepanel.tsx @@ -71,6 +71,11 @@ export type DocSearchSidepanelProps = DocSearchCallbacks & { tools?: ToolCalls; /** * Configuration for the Agent Studio memory feature. + * + * @see https://www.algolia.com/doc/guides/algolia-ai/agent-studio/how-to/memory/overview + * + * @example + * { enabled: true, userToken: '{{SERVER_GENERATED_JWT_TOKEN}}' } */ memory?: Memory; /** diff --git a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx index 34f6c058..77255d51 100644 --- a/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx +++ b/packages/docsearch-react/src/Sidepanel/Sidepanel.tsx @@ -62,7 +62,7 @@ export type SidepanelTranslations = Partial<{ logo: AlgoliaLogoTranslations; }>; -export type SidepanelProps = { +export type SidepanelProps = Pick & { /** * Variant of the Sidepanel positioning. * @@ -120,7 +120,7 @@ export type SidepanelProps = { keyboardShortcuts?: SidepanelShortcuts; }; -type Props = Omit & +type Props = Omit & SidepanelProps & SidepanelSearchParameters & { isOpen?: boolean; diff --git a/packages/docsearch-react/src/__tests__/api.test.tsx b/packages/docsearch-react/src/__tests__/api.test.tsx index 99913b3f..64d2e69f 100644 --- a/packages/docsearch-react/src/__tests__/api.test.tsx +++ b/packages/docsearch-react/src/__tests__/api.test.tsx @@ -348,7 +348,6 @@ describe('api', () => { render( ({ ...searchClient, search: promptSuggestionsSearch, @@ -361,6 +360,12 @@ describe('api', () => { }, }, }} + askAi={{ + assistantId: '123', + promptSuggestions: { + indexName: 'prompt-suggestions', + }, + }} />, ); diff --git a/packages/website/docs/docusaurus-adapter.mdx b/packages/website/docs/docusaurus-adapter.mdx index 85613274..c9e76644 100644 --- a/packages/website/docs/docusaurus-adapter.mdx +++ b/packages/website/docs/docusaurus-adapter.mdx @@ -53,6 +53,33 @@ Use `themeConfig.docsearch` as the only adapter configuration key. The adapter doesn't read `themeConfig.algolia`, which avoids built-in Docusaurus search-theme validation conflicts when you want newer DocSearch options like Agent Studio and the sidepanel. +### Client Side Tools + +Docusaurus serializes `themeConfig` for the browser and removes function values. Configure `askAi.tools` and `sidePanel.tools` in a swizzled `@theme/SearchBar` component instead of `themeConfig.docsearch`: + +```tsx title="src/theme/SearchBar/index.tsx" +import type { ToolCalls } from '@docsearch/react'; +import SearchBar from '@theme-original/SearchBar'; +import type { ReactNode } from 'react'; + +const tools: ToolCalls = { + logMessage: { + render: () => 'Tool completed.', + }, +}; + +export default function SearchBarWithTools(): ReactNode { + return ( + + ); +} +``` + +Pass `tools` through `askAi` to enable them in the modal, `sidePanel` to enable them in the side panel, or both for both interfaces. Include any other Ask AI or side-panel options when overriding the corresponding object. + ## Search Page The adapter ships a full search page (enabled by default at `/search`) with faceted filtering, an accessible "Load more" pagination, recent searches, and "Browse by section" shortcuts. Disable it with `searchPage: false`, or change its path with `searchPage: { path: 'search' }`. diff --git a/packages/website/docusaurus.config.mjs b/packages/website/docusaurus.config.mjs index e239f185..97923996 100644 --- a/packages/website/docusaurus.config.mjs +++ b/packages/website/docusaurus.config.mjs @@ -73,6 +73,9 @@ export default { indices: [{ name: 'docsearch' }], askAi: { assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef', + promptSuggestions: { + indexName: 'docsearch-markdown_prompt_suggestions', + }, }, sidePanel: true, contextualSearch: true,