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"],
|
"new-cap": ["error"],
|
||||||
"no-array-constructor": ["error"],
|
"no-array-constructor": ["error"],
|
||||||
"no-bitwise": ["error"],
|
"no-bitwise": ["error"],
|
||||||
"no-continue": ["error"],
|
|
||||||
"no-lonely-if": ["error"],
|
"no-lonely-if": ["error"],
|
||||||
"no-nested-ternary": ["error"],
|
"no-nested-ternary": ["error"],
|
||||||
"no-unneeded-ternary": ["error"],
|
"no-unneeded-ternary": ["error"],
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ const minimalDocSearchConfig = {
|
||||||
} satisfies DocSearchInput;
|
} satisfies DocSearchInput;
|
||||||
|
|
||||||
const minimalAskAiConfig = {
|
const minimalAskAiConfig = {
|
||||||
assistantId: 'my-assistant-id',
|
agentId: 'my-assistant-id',
|
||||||
} satisfies NonNullable<DocSearchInput>['askAi'];
|
} satisfies NonNullable<DocSearchInput>['askAi'];
|
||||||
|
|
||||||
const askAiConfigWithIndices = {
|
const askAiConfigWithIndices = {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ declare module '@docsearch/docusaurus-adapter' {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AskAiConfig = {
|
export type AskAiConfig = {
|
||||||
assistantId: DocSearchAskAi['assistantId'];
|
agentId: DocSearchAskAi['agentId'];
|
||||||
suggestedQuestions?: DocSearchAskAi['suggestedQuestions'];
|
suggestedQuestions?: DocSearchAskAi['suggestedQuestions'];
|
||||||
searchParameters?: AgentStudioSearchParameters;
|
searchParameters?: AgentStudioSearchParameters;
|
||||||
indices?: AgentStudioIndices[];
|
indices?: AgentStudioIndices[];
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,8 @@ type AdapterDocSearchProps = Omit<
|
||||||
| 'onAskAiToggle'
|
| 'onAskAiToggle'
|
||||||
| 'onClose'
|
| 'onClose'
|
||||||
> & {
|
> & {
|
||||||
|
appId: DocSearchProps['appId'];
|
||||||
|
apiKey: DocSearchProps['apiKey'];
|
||||||
askAi?: AskAiOptions;
|
askAi?: AskAiOptions;
|
||||||
contextualSearch?: boolean;
|
contextualSearch?: boolean;
|
||||||
externalUrlRegex?: string;
|
externalUrlRegex?: string;
|
||||||
|
|
@ -422,10 +424,9 @@ function DocSearch({
|
||||||
{...panelOptions}
|
{...panelOptions}
|
||||||
variant={panelOptions.variant ?? 'inline'}
|
variant={panelOptions.variant ?? 'inline'}
|
||||||
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
|
pushSelector={panelOptions.pushSelector ?? '#__docusaurus'}
|
||||||
assistantId={sidePanelAskAi.assistantId}
|
agentId={sidePanelAskAi.agentId}
|
||||||
apiKey={sidePanelAskAi.apiKey}
|
apiKey={sidePanelAskAi.apiKey}
|
||||||
appId={sidePanelAskAi.appId}
|
appId={sidePanelAskAi.appId}
|
||||||
indexName={sidePanelAskAi.indexName}
|
|
||||||
searchParameters={sidePanelAskAi.searchParameters}
|
searchParameters={sidePanelAskAi.searchParameters}
|
||||||
indices={panelOptions.indices ?? sidePanelAskAi.indices}
|
indices={panelOptions.indices ?? sidePanelAskAi.indices}
|
||||||
suggestedQuestions={
|
suggestedQuestions={
|
||||||
|
|
@ -454,6 +455,8 @@ export default function SearchBar(
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DocSearchProvider
|
<DocSearchProvider
|
||||||
|
appId={docSearchProps.appId}
|
||||||
|
apiKey={docSearchProps.apiKey}
|
||||||
initialQuery={docSearchProps.initialQuery}
|
initialQuery={docSearchProps.initialQuery}
|
||||||
keyboardShortcuts={docSearchProps.keyboardShortcuts}
|
keyboardShortcuts={docSearchProps.keyboardShortcuts}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@ const AskAiPromptSuggestionsSchema = Joi.object({
|
||||||
}).unknown(false);
|
}).unknown(false);
|
||||||
|
|
||||||
const AskAiSchema = Joi.object({
|
const AskAiSchema = Joi.object({
|
||||||
assistantId: Joi.string().required(),
|
agentId: Joi.string().required(),
|
||||||
suggestedQuestions: Joi.boolean().optional(),
|
suggestedQuestions: Joi.boolean().optional(),
|
||||||
searchParameters: Joi.object()
|
searchParameters: Joi.object()
|
||||||
.pattern(Joi.string(), SearchParametersSchema)
|
.pattern(Joi.string(), SearchParametersSchema)
|
||||||
|
|
@ -261,7 +261,7 @@ function assertNoRemovedKeys(themeConfig: ThemeConfig): void {
|
||||||
const askAi = docsearchRecord.askAi;
|
const askAi = docsearchRecord.askAi;
|
||||||
if (typeof askAi === 'string') {
|
if (typeof askAi === 'string') {
|
||||||
throw new Error(
|
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();
|
await expect(docSearch.hits).toBeVisible();
|
||||||
|
|
||||||
const scanResults = await axe()
|
const scanResults = await axe()
|
||||||
.include('#docsearch-hits_docsearch-list')
|
.include('#docsearch-hits_docsearch_0-list')
|
||||||
.analyze();
|
.analyze();
|
||||||
|
|
||||||
await testInfo.attach('a11y-scan-results-modal-search-results', {
|
await testInfo.attach('a11y-scan-results-modal-search-results', {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export class DocSearchPage {
|
||||||
this.hits = page.locator('.DocSearch-Hits').first();
|
this.hits = page.locator('.DocSearch-Hits').first();
|
||||||
this.clearButton = page.locator('.DocSearch-Clear');
|
this.clearButton = page.locator('.DocSearch-Clear');
|
||||||
this.firstHit = page
|
this.firstHit = page
|
||||||
.locator('#docsearch-hits_docsearch-list .DocSearch-Hit a')
|
.locator('#docsearch-hits_docsearch_0-list .DocSearch-Hit a')
|
||||||
.first();
|
.first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ test.describe('Search', () => {
|
||||||
await docSearch.typeQueryMatching();
|
await docSearch.typeQueryMatching();
|
||||||
await expect(docSearch.hits).toBeVisible();
|
await expect(docSearch.hits).toBeVisible();
|
||||||
const firstHitOption = page
|
const firstHitOption = page
|
||||||
.locator('#docsearch-hits_docsearch-list')
|
.locator('#docsearch-hits_docsearch_0-list')
|
||||||
.getByRole('option')
|
.getByRole('option')
|
||||||
.first();
|
.first();
|
||||||
const firstHitOptionId = await firstHitOption.getAttribute('id');
|
const firstHitOptionId = await firstHitOption.getAttribute('id');
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,9 @@ let sidepanelInstance: SidepanelInstance | undefined = undefined;
|
||||||
|
|
||||||
sidepanelInstance = sidepanel({
|
sidepanelInstance = sidepanel({
|
||||||
container: '#docsearch-sidepanel',
|
container: '#docsearch-sidepanel',
|
||||||
indexName: 'docsearch',
|
|
||||||
appId: 'PMZUYBQDAK',
|
appId: 'PMZUYBQDAK',
|
||||||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
agentId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||||
onReady: () => {
|
onReady: () => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log('[demo-js] sidepanel onReady()');
|
console.log('[demo-js] sidepanel onReady()');
|
||||||
|
|
@ -73,9 +72,7 @@ docsearchInstance = docsearch({
|
||||||
indices: ['docsearch'],
|
indices: ['docsearch'],
|
||||||
appId: 'PMZUYBQDAK',
|
appId: 'PMZUYBQDAK',
|
||||||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||||
askAi: {
|
askAi: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
|
||||||
},
|
|
||||||
interceptAskAiEvent: (initialMessage) => {
|
interceptAskAiEvent: (initialMessage) => {
|
||||||
docsearchInstance?.close();
|
docsearchInstance?.close();
|
||||||
sidepanelInstance.open(initialMessage);
|
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 { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
const customTools: ToolCalls = {
|
const customTools: ToolCalls = {
|
||||||
printConsoleMessage: {
|
printConsoleMessage: {
|
||||||
|
|
@ -29,11 +30,11 @@ export default function BasicAskAI({
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearchAI
|
<DocSearchAI
|
||||||
indices={['docsearch']}
|
indices={[SEARCH_INDEX_NAME]}
|
||||||
appId="PMZUYBQDAK"
|
appId={APP_ID}
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey={API_KEY}
|
||||||
askAi={{
|
askAi={{
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
agentId: AGENT_ID,
|
||||||
suggestedQuestions: true,
|
suggestedQuestions: true,
|
||||||
tools: customTools,
|
tools: customTools,
|
||||||
promptSuggestions: {
|
promptSuggestions: {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ import { DocSearch } from '@docsearch/react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
export default function Basic({ theme }: { theme: DemoTheme }): JSX.Element {
|
export default function Basic({ theme }: { theme: DemoTheme }): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch
|
<DocSearch
|
||||||
indices={['docsearch']}
|
indices={[SEARCH_INDEX_NAME]}
|
||||||
appId="PMZUYBQDAK"
|
appId={APP_ID}
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey={API_KEY}
|
||||||
translations={{ button: { buttonText: 'Keyword search' } }}
|
translations={{ button: { buttonText: 'Keyword search' } }}
|
||||||
insights={true}
|
insights={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { DocSearchButton, DocSearchAskAiModal } from '@docsearch/modal';
|
||||||
import { type JSX } from 'react';
|
import { type JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
export default function Composable({
|
export default function Composable({
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -11,16 +12,9 @@ export default function Composable({
|
||||||
theme: DemoTheme;
|
theme: DemoTheme;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch theme={theme}>
|
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||||
<DocSearchButton translations={{ buttonText: 'Composable API' }} />
|
<DocSearchButton translations={{ buttonText: 'Composable API' }} />
|
||||||
<DocSearchAskAiModal
|
<DocSearchAskAiModal askAi={AGENT_ID} indices={[SEARCH_INDEX_NAME]} />
|
||||||
indices={['docsearch']}
|
|
||||||
appId="PMZUYBQDAK"
|
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
|
||||||
askAi={{
|
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { DocSearch } from '@docsearch/react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
export default function DefaultExperience({
|
export default function DefaultExperience({
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -11,9 +12,9 @@ export default function DefaultExperience({
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch
|
<DocSearch
|
||||||
indices={['docsearch']}
|
indices={[SEARCH_INDEX_NAME]}
|
||||||
appId="PMZUYBQDAK"
|
appId={APP_ID}
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey={API_KEY}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { useCallback, useRef, useState, type JSX } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { AGENT_ID, API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
let DocSearchModal: typeof DocSearchAskAiModalType | null = null;
|
let DocSearchModal: typeof DocSearchAskAiModalType | null = null;
|
||||||
|
|
||||||
|
|
@ -94,12 +95,10 @@ function DocSearch({ theme }: { theme: DemoTheme }): JSX.Element {
|
||||||
searchContainer.current &&
|
searchContainer.current &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<DocSearchModal
|
<DocSearchModal
|
||||||
indices={['docsearch']}
|
indices={[SEARCH_INDEX_NAME]}
|
||||||
appId="PMZUYBQDAK"
|
appId={APP_ID}
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey={API_KEY}
|
||||||
askAi={{
|
askAi={AGENT_ID}
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
|
||||||
}}
|
|
||||||
initialScrollY={window.scrollY}
|
initialScrollY={window.scrollY}
|
||||||
initialQuery={initialQuery}
|
initialQuery={initialQuery}
|
||||||
isAskAiActive={isAskAiActive}
|
isAskAiActive={isAskAiActive}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Sidepanel, SidepanelButton } from '@docsearch/sidepanel';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { AGENT_ID, API_KEY, APP_ID } from '../constants';
|
||||||
|
|
||||||
export default function BasicHybrid({
|
export default function BasicHybrid({
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -12,24 +13,12 @@ export default function BasicHybrid({
|
||||||
theme: DemoTheme;
|
theme: DemoTheme;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch theme={theme}>
|
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||||
<DocSearchButton />
|
<DocSearchButton />
|
||||||
<DocSearchAskAiModal
|
<DocSearchAskAiModal askAi={AGENT_ID} indices={['docsearch']} />
|
||||||
indices={['docsearch']}
|
|
||||||
appId="PMZUYBQDAK"
|
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
|
||||||
askAi={{
|
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidepanelButton />
|
<SidepanelButton />
|
||||||
<Sidepanel
|
<Sidepanel agentId={AGENT_ID} />
|
||||||
indexName="docsearch-markdown"
|
|
||||||
appId="PMZUYBQDAK"
|
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
|
||||||
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
|
||||||
/>
|
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { DocSearch } from '@docsearch/react';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { API_KEY, APP_ID, SEARCH_INDEX_NAME } from '../constants';
|
||||||
|
|
||||||
export default function MultiIndex({
|
export default function MultiIndex({
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -13,7 +14,7 @@ export default function MultiIndex({
|
||||||
<DocSearch
|
<DocSearch
|
||||||
indices={[
|
indices={[
|
||||||
{
|
{
|
||||||
name: 'docsearch',
|
name: SEARCH_INDEX_NAME,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'tailwindcss',
|
name: 'tailwindcss',
|
||||||
|
|
@ -22,8 +23,8 @@ export default function MultiIndex({
|
||||||
name: 'kubernetes',
|
name: 'kubernetes',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
appId="PMZUYBQDAK"
|
appId={APP_ID}
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey={API_KEY}
|
||||||
translations={{ button: { buttonText: 'Multi index search' } }}
|
translations={{ button: { buttonText: 'Multi index search' } }}
|
||||||
insights={true}
|
insights={true}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { SidepanelButton, Sidepanel } from '@docsearch/sidepanel';
|
||||||
import type { JSX } from 'react';
|
import type { JSX } from 'react';
|
||||||
|
|
||||||
import type { DemoTheme } from '../App';
|
import type { DemoTheme } from '../App';
|
||||||
|
import { AGENT_ID, API_KEY, APP_ID } from '../constants';
|
||||||
|
|
||||||
export default function SidepanelExample({
|
export default function SidepanelExample({
|
||||||
theme,
|
theme,
|
||||||
|
|
@ -11,13 +12,10 @@ export default function SidepanelExample({
|
||||||
theme: DemoTheme;
|
theme: DemoTheme;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch theme={theme}>
|
<DocSearch appId={APP_ID} apiKey={API_KEY} theme={theme}>
|
||||||
<SidepanelButton variant="inline" />
|
<SidepanelButton variant="inline" />
|
||||||
<Sidepanel
|
<Sidepanel
|
||||||
indexName="docsearch"
|
agentId={AGENT_ID}
|
||||||
appId="PMZUYBQDAK"
|
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
|
||||||
assistantId="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
|
||||||
variant="floating"
|
variant="floating"
|
||||||
tools={{
|
tools={{
|
||||||
printConsoleMessage: {
|
printConsoleMessage: {
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,7 @@ export default function WTransformItems({
|
||||||
]}
|
]}
|
||||||
appId="PMZUYBQDAK"
|
appId="PMZUYBQDAK"
|
||||||
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
apiKey="24b09689d5b4223813d9b8e48563c8f6"
|
||||||
askAi={{
|
askAi="ccdec697-e3fe-465b-a1c3-657e7bf18aef"
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
|
||||||
}}
|
|
||||||
insights={true}
|
insights={true}
|
||||||
transformItems={(items) => {
|
transformItems={(items) => {
|
||||||
return items.map((item: any) => ({
|
return items.map((item: any) => ({
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,10 @@ export interface DocSearchRef {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocSearchContext {
|
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;
|
docsearchState: DocSearchState;
|
||||||
setDocsearchState: (newState: DocSearchState) => void;
|
setDocsearchState: (newState: DocSearchState) => void;
|
||||||
searchButtonRef: React.RefObject<HTMLButtonElement | null>;
|
searchButtonRef: React.RefObject<HTMLButtonElement | null>;
|
||||||
|
|
@ -79,6 +83,10 @@ export interface DocSearchCallbacks {
|
||||||
|
|
||||||
export interface DocSearchProps extends DocSearchCallbacks {
|
export interface DocSearchProps extends DocSearchCallbacks {
|
||||||
children: Array<JSX.Element | null> | JSX.Element | React.ReactNode | null;
|
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;
|
theme?: DocSearchTheme;
|
||||||
initialQuery?: string;
|
initialQuery?: string;
|
||||||
keyboardShortcuts?: KeyboardShortcuts;
|
keyboardShortcuts?: KeyboardShortcuts;
|
||||||
|
|
@ -90,6 +98,8 @@ Context.displayName = 'DocSearchContext';
|
||||||
function DocSearchInner(
|
function DocSearchInner(
|
||||||
{
|
{
|
||||||
children,
|
children,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
theme,
|
theme,
|
||||||
onReady,
|
onReady,
|
||||||
onOpen,
|
onOpen,
|
||||||
|
|
@ -267,6 +277,8 @@ function DocSearchInner(
|
||||||
|
|
||||||
const value: DocSearchContext = React.useMemo(
|
const value: DocSearchContext = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
docsearchState,
|
docsearchState,
|
||||||
setDocsearchState,
|
setDocsearchState,
|
||||||
searchButtonRef,
|
searchButtonRef,
|
||||||
|
|
@ -282,6 +294,8 @@ function DocSearchInner(
|
||||||
isHybridModeSupported,
|
isHybridModeSupported,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
docsearchState,
|
docsearchState,
|
||||||
searchButtonRef,
|
searchButtonRef,
|
||||||
initialQuery,
|
initialQuery,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,18 @@ describe('@docsearch/core', () => {
|
||||||
expect(screen.getByText('State: ready')).toBeInTheDocument();
|
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 () => {
|
it('updates state from children', async () => {
|
||||||
const Comp = (): JSX.Element => {
|
const Comp = (): JSX.Element => {
|
||||||
const { docsearchState, openModal } = useDocSearch();
|
const { docsearchState, openModal } = useDocSearch();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
export type DocSearchAskAiModalProps = Omit<
|
export type DocSearchAskAiModalProps = Omit<
|
||||||
ReactDocSearchAskAiModalProps,
|
ReactDocSearchAskAiModalProps,
|
||||||
|
| 'appId'
|
||||||
|
| 'apiKey'
|
||||||
| 'initialScrollY'
|
| 'initialScrollY'
|
||||||
| 'isAskAiActive'
|
| 'isAskAiActive'
|
||||||
| 'isHybridModeSupported'
|
| 'isHybridModeSupported'
|
||||||
|
|
@ -14,12 +16,15 @@ export type DocSearchAskAiModalProps = Omit<
|
||||||
| 'onAskAiToggle'
|
| 'onAskAiToggle'
|
||||||
| 'onClose'
|
| 'onClose'
|
||||||
| 'theme'
|
| 'theme'
|
||||||
>;
|
> &
|
||||||
|
Partial<Pick<ReactDocSearchAskAiModalProps, 'appId' | 'apiKey'>>;
|
||||||
|
|
||||||
export function DocSearchAskAiModal(
|
export function DocSearchAskAiModal(
|
||||||
props: DocSearchAskAiModalProps
|
props: DocSearchAskAiModalProps
|
||||||
): JSX.Element | null {
|
): JSX.Element | null {
|
||||||
const {
|
const {
|
||||||
|
appId: providerAppId,
|
||||||
|
apiKey: providerApiKey,
|
||||||
isModalActive,
|
isModalActive,
|
||||||
onAskAiToggle,
|
onAskAiToggle,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
|
@ -29,6 +34,15 @@ export function DocSearchAskAiModal(
|
||||||
isHybridModeSupported,
|
isHybridModeSupported,
|
||||||
} = useDocSearch();
|
} = 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(
|
const containerElement = React.useMemo(
|
||||||
() => props.portalContainer ?? document.body,
|
() => props.portalContainer ?? document.body,
|
||||||
[props.portalContainer]
|
[props.portalContainer]
|
||||||
|
|
@ -43,6 +57,8 @@ export function DocSearchAskAiModal(
|
||||||
const modalProps: ReactDocSearchAskAiModalProps = React.useMemo(
|
const modalProps: ReactDocSearchAskAiModalProps = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...props,
|
...props,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
isAskAiActive,
|
isAskAiActive,
|
||||||
initialQuery: props.initialQuery ?? initialQuery,
|
initialQuery: props.initialQuery ?? initialQuery,
|
||||||
initialScrollY: initialScroll,
|
initialScrollY: initialScroll,
|
||||||
|
|
@ -52,6 +68,8 @@ export function DocSearchAskAiModal(
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
props,
|
props,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
isAskAiActive,
|
isAskAiActive,
|
||||||
initialQuery,
|
initialQuery,
|
||||||
initialScroll,
|
initialScroll,
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,33 @@ import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
export type DocSearchModalProps = Omit<
|
export type DocSearchModalProps = Omit<
|
||||||
ReactDocSearchModalProps,
|
ReactDocSearchModalProps,
|
||||||
'initialScrollY' | 'keyboardShortcuts' | 'onClose' | 'theme'
|
| 'appId'
|
||||||
>;
|
| 'apiKey'
|
||||||
|
| 'initialScrollY'
|
||||||
|
| 'keyboardShortcuts'
|
||||||
|
| 'onClose'
|
||||||
|
| 'theme'
|
||||||
|
> &
|
||||||
|
Partial<Pick<ReactDocSearchModalProps, 'appId' | 'apiKey'>>;
|
||||||
|
|
||||||
export function DocSearchModal(props: DocSearchModalProps): JSX.Element | null {
|
export function DocSearchModal(props: DocSearchModalProps): JSX.Element | null {
|
||||||
const { isModalActive, closeModal, initialQuery, registerView } =
|
const {
|
||||||
useDocSearch();
|
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(
|
const containerElement = React.useMemo(
|
||||||
() => props.portalContainer ?? document.body,
|
() => props.portalContainer ?? document.body,
|
||||||
|
|
@ -28,11 +49,13 @@ export function DocSearchModal(props: DocSearchModalProps): JSX.Element | null {
|
||||||
const modalProps: ReactDocSearchModalProps = React.useMemo(
|
const modalProps: ReactDocSearchModalProps = React.useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...props,
|
...props,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
initialQuery: props.initialQuery ?? initialQuery,
|
initialQuery: props.initialQuery ?? initialQuery,
|
||||||
initialScrollY: initialScroll,
|
initialScrollY: initialScroll,
|
||||||
onClose: closeModal,
|
onClose: closeModal,
|
||||||
}),
|
}),
|
||||||
[props, initialQuery, initialScroll, closeModal]
|
[props, appId, apiKey, initialQuery, initialScroll, closeModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
return isModalActive
|
return isModalActive
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ const DEFAULT_PROPS: DocSearchModalProps = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PROVIDER_DEFAULT_PROPS: DocSearchModalProps = {
|
||||||
|
indices: [INDEX_NAME],
|
||||||
|
transformSearchClient: DEFAULT_PROPS.transformSearchClient,
|
||||||
|
};
|
||||||
|
|
||||||
const renderComponent = (
|
const renderComponent = (
|
||||||
props: DocSearchModalProps = DEFAULT_PROPS,
|
props: DocSearchModalProps = DEFAULT_PROPS,
|
||||||
docsearchProps: Omit<DocSearchProps, 'children'> = {}
|
docsearchProps: Omit<DocSearchProps, 'children'> = {}
|
||||||
|
|
@ -67,6 +72,22 @@ describe('@docsearch/modal', () => {
|
||||||
renderComponent();
|
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', () => {
|
it('opens modal on keyboard shortcut', () => {
|
||||||
renderComponent();
|
renderComponent();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -157,11 +157,11 @@ export interface DocSearchProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocSearchComponent(
|
function DocSearchComponent(
|
||||||
props: DocSearchProps,
|
{ appId, apiKey, ...props }: DocSearchProps,
|
||||||
ref: React.ForwardedRef<DocSearchRef>
|
ref: React.ForwardedRef<DocSearchRef>
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearchProvider {...props} ref={ref}>
|
<DocSearchProvider {...props} appId={appId} apiKey={apiKey} ref={ref}>
|
||||||
<DocSearchInner {...props} />
|
<DocSearchInner {...props} />
|
||||||
</DocSearchProvider>
|
</DocSearchProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -169,7 +169,9 @@ function DocSearchComponent(
|
||||||
|
|
||||||
export const DocSearch = React.forwardRef(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 {
|
const {
|
||||||
searchButtonRef,
|
searchButtonRef,
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
|
|
@ -177,8 +179,14 @@ export function DocSearchInner(props: DocSearchProps): JSX.Element {
|
||||||
initialQuery,
|
initialQuery,
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
} = useDocSearch();
|
} = useDocSearch();
|
||||||
|
|
||||||
|
if (!appId || !apiKey) {
|
||||||
|
throw new Error('`DocSearch` requires `appId` and `apiKey` props.');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocSearchButton
|
<DocSearchButton
|
||||||
|
|
@ -191,6 +199,8 @@ export function DocSearchInner(props: DocSearchProps): JSX.Element {
|
||||||
createPortal(
|
createPortal(
|
||||||
<DocSearchModal
|
<DocSearchModal
|
||||||
{...props}
|
{...props}
|
||||||
|
appId={appId}
|
||||||
|
apiKey={apiKey}
|
||||||
initialScrollY={window.scrollY}
|
initialScrollY={window.scrollY}
|
||||||
initialQuery={initialQuery}
|
initialQuery={initialQuery}
|
||||||
translations={props?.translations?.modal}
|
translations={props?.translations?.modal}
|
||||||
|
|
|
||||||
|
|
@ -154,11 +154,6 @@ export interface PromptSuggestions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocSearchAskAi {
|
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
|
* 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.
|
* 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.
|
* ID to search the index. If not provided, the app ID will be used.
|
||||||
*/
|
*/
|
||||||
appId?: string;
|
appId?: string;
|
||||||
/** The assistant ID to use for the ask AI feature. */
|
/** The Agent Studio Agent ID to use for the Ask AI feature. */
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
/**
|
/**
|
||||||
* Enables displaying suggested questions on Ask AI's new conversation screen.
|
* Enables displaying suggested questions on Ask AI's new conversation screen.
|
||||||
*
|
*
|
||||||
|
|
@ -236,11 +231,16 @@ export interface DocSearchAIProps extends DocSearchProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DocSearchAIComponent(
|
function DocSearchAIComponent(
|
||||||
props: DocSearchAIProps,
|
{ appId, apiKey, ...props }: DocSearchAIProps,
|
||||||
ref: React.ForwardedRef<DocSearchRef>
|
ref: React.ForwardedRef<DocSearchRef>
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearchProvider {...props} ref={ref}>
|
<DocSearchProvider
|
||||||
|
{...props}
|
||||||
|
appId={appId}
|
||||||
|
apiKey={apiKey}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<DocSearchAIInner {...props} />
|
<DocSearchAIInner {...props} />
|
||||||
</DocSearchProvider>
|
</DocSearchProvider>
|
||||||
);
|
);
|
||||||
|
|
@ -248,7 +248,9 @@ function DocSearchAIComponent(
|
||||||
|
|
||||||
export const DocSearchAI = React.forwardRef(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 {
|
const {
|
||||||
searchButtonRef,
|
searchButtonRef,
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
|
|
@ -259,8 +261,14 @@ export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element {
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
isHybridModeSupported,
|
isHybridModeSupported,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
} = useDocSearch();
|
} = useDocSearch();
|
||||||
|
|
||||||
|
if (!appId || !apiKey) {
|
||||||
|
throw new Error('`DocSearchAI` requires `appId` and `apiKey` props.');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocSearchButton
|
<DocSearchButton
|
||||||
|
|
@ -273,6 +281,8 @@ export function DocSearchAIInner(props: DocSearchAIProps): JSX.Element {
|
||||||
createPortal(
|
createPortal(
|
||||||
<DocSearchAskAiModal
|
<DocSearchAskAiModal
|
||||||
{...props}
|
{...props}
|
||||||
|
appId={appId}
|
||||||
|
apiKey={apiKey}
|
||||||
initialScrollY={window.scrollY}
|
initialScrollY={window.scrollY}
|
||||||
initialQuery={initialQuery}
|
initialQuery={initialQuery}
|
||||||
translations={props?.translations?.modal}
|
translations={props?.translations?.modal}
|
||||||
|
|
|
||||||
|
|
@ -157,12 +157,12 @@ export function DocSearchAskAiModal({
|
||||||
|
|
||||||
const askAiConfig = typeof askAi === 'object' ? askAi : null;
|
const askAiConfig = typeof askAi === 'object' ? askAi : null;
|
||||||
const askAiConfigurationId = askAiConfig
|
const askAiConfigurationId = askAiConfig
|
||||||
? askAiConfig.assistantId
|
? askAiConfig.agentId
|
||||||
: (askAi as string);
|
: (askAi as string);
|
||||||
const askAiSearchParameters = askAiConfig?.searchParameters;
|
const askAiSearchParameters = askAiConfig?.searchParameters;
|
||||||
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
|
const [askAiState, setAskAiState] = React.useState<AskAiState>('initial');
|
||||||
const suggestedQuestions = useSuggestedQuestions({
|
const suggestedQuestions = useSuggestedQuestions({
|
||||||
assistantId: askAiConfigurationId,
|
agentId: askAiConfigurationId,
|
||||||
searchClient,
|
searchClient,
|
||||||
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
|
suggestedQuestionsEnabled: askAiConfig?.suggestedQuestions,
|
||||||
});
|
});
|
||||||
|
|
@ -224,10 +224,9 @@ export function DocSearchAskAiModal({
|
||||||
startNewConversation,
|
startNewConversation,
|
||||||
restoreConversation,
|
restoreConversation,
|
||||||
} = useAskAi({
|
} = useAskAi({
|
||||||
assistantId: askAiConfigurationId,
|
agentId: askAiConfigurationId,
|
||||||
apiKey: askAiConfig?.apiKey || apiKey,
|
apiKey: askAiConfig?.apiKey || apiKey,
|
||||||
appId: askAiConfig?.appId || appId,
|
appId: askAiConfig?.appId || appId,
|
||||||
indexName: askAiConfig?.indexName || defaultIndexName,
|
|
||||||
searchParameters: askAiSearchParameters,
|
searchParameters: askAiSearchParameters,
|
||||||
tools,
|
tools,
|
||||||
memory: askAiConfig?.memory,
|
memory: askAiConfig?.memory,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import type { ResultsTranslations } from './Results';
|
||||||
import { Results } from './Results';
|
import { Results } from './Results';
|
||||||
import type { ScreenStateProps } from './ScreenState';
|
import type { ScreenStateProps } from './ScreenState';
|
||||||
import type { InternalDocSearchHit } from './types';
|
import type { InternalDocSearchHit } from './types';
|
||||||
|
import { removeHighlightTags } from './utils';
|
||||||
|
|
||||||
export type ResultsScreenTranslations = Partial<{
|
export type ResultsScreenTranslations = Partial<{
|
||||||
askAiPlaceholder: string;
|
askAiPlaceholder: string;
|
||||||
|
|
@ -27,19 +28,6 @@ export function ResultsScreen({
|
||||||
resultBadgeKey,
|
resultBadgeKey,
|
||||||
...props
|
...props
|
||||||
}: ResultsScreenProps): JSX.Element {
|
}: 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(() => {
|
const renderAction = React.useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<div className="DocSearch-Hit-action">
|
<div className="DocSearch-Hit-action">
|
||||||
|
|
@ -70,14 +58,40 @@ export function ResultsScreen({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const title = removeHighlightTags(collection.items[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Results
|
<Results
|
||||||
{...props}
|
{...props}
|
||||||
key={collection.source.sourceId}
|
key={collection.source.sourceId}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
title={resultsSectionTitle}
|
title={title}
|
||||||
collection={collection}
|
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}
|
renderAction={renderAction}
|
||||||
renderResultBadge={renderResultBadge}
|
renderResultBadge={renderResultBadge}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -37,17 +37,12 @@ export type SidepanelSearchParameters = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocSearchSidepanelProps = DocSearchCallbacks & {
|
export type DocSearchSidepanelProps = DocSearchCallbacks & {
|
||||||
/** The assistant ID to use for the ask AI feature. */
|
/** The Agent Studio agent ID to use for Ask AI. */
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
/** Public api key with search permissions for the index. */
|
/** Search only API key. */
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
/** Algolia application id used by the search client. */
|
/** Algolia application ID that hosts the Agent Studio agent. */
|
||||||
appId: string;
|
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
|
* Configuration for keyboard shortcuts. Allows enabling/disabling specific
|
||||||
* shortcuts.
|
* shortcuts.
|
||||||
|
|
@ -103,6 +98,8 @@ function DocSearchSidepanelComponent(
|
||||||
): JSX.Element {
|
): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch
|
<DocSearch
|
||||||
|
appId={props.appId}
|
||||||
|
apiKey={props.apiKey}
|
||||||
keyboardShortcuts={keyboardShortcuts}
|
keyboardShortcuts={keyboardShortcuts}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -130,8 +127,16 @@ function DocSearchSidepanelComp({
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
registerView,
|
registerView,
|
||||||
initialAskAiMessage,
|
initialAskAiMessage,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
} = useDocSearch();
|
} = useDocSearch();
|
||||||
|
|
||||||
|
if (!appId || !apiKey) {
|
||||||
|
throw new Error(
|
||||||
|
'`Sidepanel` requires `appId` and `apiKey` props or values configured on the `DocSearch` provider.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const toggleSidepanelState = React.useCallback(() => {
|
const toggleSidepanelState = React.useCallback(() => {
|
||||||
setDocsearchState(docsearchState === 'sidepanel' ? 'ready' : 'sidepanel');
|
setDocsearchState(docsearchState === 'sidepanel' ? 'ready' : 'sidepanel');
|
||||||
}, [docsearchState, setDocsearchState]);
|
}, [docsearchState, setDocsearchState]);
|
||||||
|
|
|
||||||
|
|
@ -134,10 +134,9 @@ function SidepanelInner(
|
||||||
isOpen = false,
|
isOpen = false,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
assistantId,
|
agentId,
|
||||||
apiKey,
|
apiKey,
|
||||||
appId,
|
appId,
|
||||||
indexName,
|
|
||||||
variant = 'floating',
|
variant = 'floating',
|
||||||
searchParameters,
|
searchParameters,
|
||||||
pushSelector,
|
pushSelector,
|
||||||
|
|
@ -194,8 +193,7 @@ function SidepanelInner(
|
||||||
restoreConversation,
|
restoreConversation,
|
||||||
} = useAskAi({
|
} = useAskAi({
|
||||||
appId,
|
appId,
|
||||||
indexName,
|
agentId,
|
||||||
assistantId,
|
|
||||||
apiKey,
|
apiKey,
|
||||||
searchParameters,
|
searchParameters,
|
||||||
tools,
|
tools,
|
||||||
|
|
@ -204,7 +202,7 @@ function SidepanelInner(
|
||||||
});
|
});
|
||||||
|
|
||||||
const suggestedQuestions = useSuggestedQuestions({
|
const suggestedQuestions = useSuggestedQuestions({
|
||||||
assistantId,
|
agentId,
|
||||||
suggestedQuestionsEnabled,
|
suggestedQuestionsEnabled,
|
||||||
searchClient,
|
searchClient,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -445,7 +445,7 @@ describe('api', () => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
askAi={{
|
askAi={{
|
||||||
assistantId: '123',
|
agentId: '123',
|
||||||
promptSuggestions: {
|
promptSuggestions: {
|
||||||
indexName: 'prompt-suggestions',
|
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({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {
|
tools: {
|
||||||
customAction: {
|
customAction: {
|
||||||
onToolCall,
|
onToolCall,
|
||||||
|
|
@ -205,8 +204,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {
|
tools: {
|
||||||
customAction: {
|
customAction: {
|
||||||
onToolCall,
|
onToolCall,
|
||||||
|
|
@ -233,10 +231,9 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
|
||||||
memory: { userToken: 'secure-user-token' },
|
memory: { userToken: 'secure-user-token' },
|
||||||
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -250,8 +247,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -266,8 +262,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -284,8 +279,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
searchParameters,
|
searchParameters,
|
||||||
})
|
})
|
||||||
|
|
@ -308,8 +302,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
indices,
|
indices,
|
||||||
})
|
})
|
||||||
|
|
@ -335,8 +328,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
searchParameters,
|
searchParameters,
|
||||||
indices,
|
indices,
|
||||||
|
|
@ -353,8 +345,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
indices: [],
|
indices: [],
|
||||||
})
|
})
|
||||||
|
|
@ -369,8 +360,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -383,8 +373,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -408,8 +397,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -434,8 +422,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -463,8 +450,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -492,8 +478,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -506,8 +491,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -541,8 +525,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
tools: {},
|
tools: {},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -578,8 +561,7 @@ describe('useAskAi', () => {
|
||||||
useAskAi({
|
useAskAi({
|
||||||
apiKey: 'api-key',
|
apiKey: 'api-key',
|
||||||
appId: 'app-id',
|
appId: 'app-id',
|
||||||
assistantId: 'assistant-id',
|
agentId: 'assistant-id',
|
||||||
indexName: 'index-name',
|
|
||||||
searchParameters,
|
searchParameters,
|
||||||
indices,
|
indices,
|
||||||
tools: {},
|
tools: {},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
import type { AIMessage } from '../types/AskiAi';
|
import type { AIMessage } from '../types/AskiAi';
|
||||||
import { extractLinksFromMessage } from '../utils/ai';
|
import { extractLinksFromMessage } from '../utils/ai';
|
||||||
import { createFacetFilters } from '../utils/createDocSearchSources';
|
import { createFacetFilters } from '../utils/createDocSearchSources';
|
||||||
import { getFacetLabel, normalizeFacets } from '../utils/facets';
|
import {
|
||||||
|
deriveDefaultSelectedFacetsFromIndex,
|
||||||
|
getFacetLabel,
|
||||||
|
normalizeFacets,
|
||||||
|
} from '../utils/facets';
|
||||||
import {
|
import {
|
||||||
createObjectStorage,
|
createObjectStorage,
|
||||||
createStorage,
|
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', () => {
|
it('returns configured facetFilters when no dynamic facets are selected', () => {
|
||||||
expect(createFacetFilters(['language:en'], {})).toEqual(['language:en']);
|
expect(createFacetFilters(['language:en'], {})).toEqual(['language:en']);
|
||||||
});
|
});
|
||||||
|
|
@ -79,10 +111,21 @@ describe('utils', () => {
|
||||||
).toEqual(['docusaurus_tag:default', 'language:en', 'version:v2']);
|
).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(
|
expect(
|
||||||
createFacetFilters(undefined, { language: '', version: 'v2' })
|
createFacetFilters(['language:en', 'version:v2'], {
|
||||||
).toEqual(['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 type { DocSearchFacet } from '../DocSearch';
|
||||||
import { ChevronIcon } from '../icons';
|
import { ChevronIcon } from '../icons';
|
||||||
import { capitalize } from '../utils';
|
import { capitalize } from '../utils';
|
||||||
|
import type { FacetSelections } from '../utils/createDocSearchSources';
|
||||||
import { getFacetLabel } from '../utils/facets';
|
import { getFacetLabel } from '../utils/facets';
|
||||||
|
|
||||||
import { Chip } from './ui/Chip';
|
import { Chip } from './ui/Chip';
|
||||||
|
|
@ -132,7 +133,7 @@ const SelectedFacetChip = React.memo(function SelectedFacetChip({
|
||||||
value,
|
value,
|
||||||
dismissAriaLabel,
|
dismissAriaLabel,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
}: SelectedFacetChipProps): JSX.Element {
|
}: SelectedFacetChipProps): JSX.Element | null {
|
||||||
const handleDismissFacet = React.useCallback(
|
const handleDismissFacet = React.useCallback(
|
||||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
onDismiss(facetKey, e);
|
onDismiss(facetKey, e);
|
||||||
|
|
@ -153,7 +154,7 @@ const SelectedFacetChip = React.memo(function SelectedFacetChip({
|
||||||
|
|
||||||
interface FacetBarProps {
|
interface FacetBarProps {
|
||||||
facets: FacetBarFacet[];
|
facets: FacetBarFacet[];
|
||||||
selections: Record<string, string>;
|
selections: FacetSelections;
|
||||||
onSelectionChange: (facet: string, value: string) => void;
|
onSelectionChange: (facet: string, value: string) => void;
|
||||||
clearSelections: () => void;
|
clearSelections: () => void;
|
||||||
translations?: FacetBarTranslations;
|
translations?: FacetBarTranslations;
|
||||||
|
|
@ -174,9 +175,15 @@ export const FacetBar = React.memo(function FacetBar({
|
||||||
selectedFacetsAriaLabel = 'Selected search filters',
|
selectedFacetsAriaLabel = 'Selected search filters',
|
||||||
clearFacetAriaLabel = 'Clear filter:',
|
clearFacetAriaLabel = 'Clear filter:',
|
||||||
} = translations;
|
} = translations;
|
||||||
|
const visibleFacetKeys = React.useMemo(
|
||||||
|
() => new Set(facets.map((f) => f.key)),
|
||||||
|
[facets]
|
||||||
|
);
|
||||||
const selectionsToDisplay = React.useMemo(() => {
|
const selectionsToDisplay = React.useMemo(() => {
|
||||||
return Object.entries(selections).filter(([_, value]) => Boolean(value));
|
return Object.entries(selections).filter(
|
||||||
}, [selections]);
|
([key, value]) => Boolean(value) && visibleFacetKeys.has(key)
|
||||||
|
);
|
||||||
|
}, [selections, visibleFacetKeys]);
|
||||||
|
|
||||||
const triggerRefs = React.useRef(new Map<string, HTMLButtonElement>());
|
const triggerRefs = React.useRef(new Map<string, HTMLButtonElement>());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,8 +103,8 @@ describe('useDocSearchFacets', () => {
|
||||||
result.current.clearFacetSelections();
|
result.current.clearFacetSelections();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.facetSelections).toEqual({});
|
expect(result.current.facetSelections).toEqual({ language: '' });
|
||||||
expect(result.current.facetSelectionsRef.current).toEqual({});
|
expect(result.current.facetSelectionsRef.current).toEqual({ language: '' });
|
||||||
expect(onSelectionsChange).toHaveBeenCalledTimes(2);
|
expect(onSelectionsChange).toHaveBeenCalledTimes(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import type { DocSearchFacet, DocSearchIndex } from '../DocSearch';
|
||||||
import { useFacetValues } from '../useFacetValues';
|
import { useFacetValues } from '../useFacetValues';
|
||||||
import type { useSearchClient } from '../useSearchClient';
|
import type { useSearchClient } from '../useSearchClient';
|
||||||
import type { FacetSelections } from '../utils/createDocSearchSources';
|
import type { FacetSelections } from '../utils/createDocSearchSources';
|
||||||
import { normalizeFacets } from '../utils/facets';
|
import {
|
||||||
|
deriveDefaultSelectedFacetsFromIndex,
|
||||||
|
normalizeFacets,
|
||||||
|
} from '../utils/facets';
|
||||||
|
|
||||||
export interface UseDocSearchFacetsProps {
|
export interface UseDocSearchFacetsProps {
|
||||||
facets?: DocSearchFacet[];
|
facets?: DocSearchFacet[];
|
||||||
|
|
@ -38,18 +41,18 @@ export function useDocSearchFacets({
|
||||||
() => normalizeFacets(facets),
|
() => normalizeFacets(facets),
|
||||||
[facets]
|
[facets]
|
||||||
);
|
);
|
||||||
|
const normalizedFacetsRef = React.useRef(normalizedFacets);
|
||||||
const facetValues = useFacetValues({
|
const facetValues = useFacetValues({
|
||||||
facets: normalizedFacets,
|
facets: normalizedFacets,
|
||||||
indexes,
|
indexes,
|
||||||
searchClient,
|
searchClient,
|
||||||
});
|
});
|
||||||
const [facetSelections, setFacetSelections] = React.useState<FacetSelections>(
|
const [facetSelections, setFacetSelections] = React.useState<FacetSelections>(
|
||||||
{}
|
() => deriveDefaultSelectedFacetsFromIndex(indexes)
|
||||||
);
|
);
|
||||||
const facetSelectionsRef = React.useRef(facetSelections);
|
const facetSelectionsRef = React.useRef(facetSelections);
|
||||||
|
|
||||||
const onSelectionsChangeRef = React.useRef(onSelectionsChange);
|
const onSelectionsChangeRef = React.useRef(onSelectionsChange);
|
||||||
onSelectionsChangeRef.current = onSelectionsChange;
|
|
||||||
|
|
||||||
const visibleFacets = React.useMemo(
|
const visibleFacets = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -59,6 +62,11 @@ export function useDocSearchFacets({
|
||||||
[facetValues, normalizedFacets]
|
[facetValues, normalizedFacets]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
normalizedFacetsRef.current = normalizedFacets;
|
||||||
|
onSelectionsChangeRef.current = onSelectionsChange;
|
||||||
|
});
|
||||||
|
|
||||||
const applySelections = React.useCallback((next: FacetSelections): void => {
|
const applySelections = React.useCallback((next: FacetSelections): void => {
|
||||||
facetSelectionsRef.current = next;
|
facetSelectionsRef.current = next;
|
||||||
setFacetSelections(next);
|
setFacetSelections(next);
|
||||||
|
|
@ -67,15 +75,13 @@ export function useDocSearchFacets({
|
||||||
|
|
||||||
const handleFacetSelectionChange = React.useCallback(
|
const handleFacetSelectionChange = React.useCallback(
|
||||||
(facet: string, value: string): void => {
|
(facet: string, value: string): void => {
|
||||||
if (facetSelectionsRef.current[facet] === value) return;
|
const existing = facetSelectionsRef.current[facet];
|
||||||
|
|
||||||
|
if (existing === value) return;
|
||||||
|
|
||||||
const next = { ...facetSelectionsRef.current };
|
const next = { ...facetSelectionsRef.current };
|
||||||
|
|
||||||
if (value === '') {
|
next[facet] = value;
|
||||||
delete next[facet];
|
|
||||||
} else {
|
|
||||||
next[facet] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
applySelections(next);
|
applySelections(next);
|
||||||
},
|
},
|
||||||
|
|
@ -84,7 +90,15 @@ export function useDocSearchFacets({
|
||||||
|
|
||||||
const clearFacetSelections = React.useCallback(() => {
|
const clearFacetSelections = React.useCallback(() => {
|
||||||
if (Object.keys(facetSelectionsRef.current).length === 0) return;
|
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]);
|
}, [applySelections]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,9 @@ import type {
|
||||||
type UseChat = UseChatHelpers<AIMessage>;
|
type UseChat = UseChatHelpers<AIMessage>;
|
||||||
|
|
||||||
type UseAskAiParams = {
|
type UseAskAiParams = {
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
appId: string;
|
appId: string;
|
||||||
indexName: string;
|
|
||||||
searchParameters?: AgentStudioSearchParameters;
|
searchParameters?: AgentStudioSearchParameters;
|
||||||
tools: ToolCalls;
|
tools: ToolCalls;
|
||||||
memory?: Memory;
|
memory?: Memory;
|
||||||
|
|
@ -71,7 +70,7 @@ type UseAskAi = (params: UseAskAiParams) => UseAskAiReturn;
|
||||||
|
|
||||||
type AgentStudioTransportParams = Pick<
|
type AgentStudioTransportParams = Pick<
|
||||||
UseAskAiParams,
|
UseAskAiParams,
|
||||||
'apiKey' | 'appId' | 'assistantId'
|
'apiKey' | 'appId' | 'agentId'
|
||||||
> & {
|
> & {
|
||||||
searchParameters?: AgentStudioSearchParameters;
|
searchParameters?: AgentStudioSearchParameters;
|
||||||
userToken?: string;
|
userToken?: string;
|
||||||
|
|
@ -81,7 +80,7 @@ type AgentStudioTransportParams = Pick<
|
||||||
const getAgentStudioTransport = ({
|
const getAgentStudioTransport = ({
|
||||||
appId,
|
appId,
|
||||||
apiKey,
|
apiKey,
|
||||||
assistantId,
|
agentId,
|
||||||
searchParameters,
|
searchParameters,
|
||||||
userToken,
|
userToken,
|
||||||
indices,
|
indices,
|
||||||
|
|
@ -100,7 +99,7 @@ const getAgentStudioTransport = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DefaultChatTransport({
|
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: {
|
headers: {
|
||||||
'x-algolia-application-id': appId,
|
'x-algolia-application-id': appId,
|
||||||
'x-algolia-api-key': apiKey,
|
'x-algolia-api-key': apiKey,
|
||||||
|
|
@ -126,7 +125,7 @@ const getAgentStudioTransport = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAskAi: UseAskAi = ({
|
export const useAskAi: UseAskAi = ({
|
||||||
assistantId,
|
agentId,
|
||||||
apiKey,
|
apiKey,
|
||||||
appId,
|
appId,
|
||||||
tools = EMPTY_TOOLS,
|
tools = EMPTY_TOOLS,
|
||||||
|
|
@ -141,12 +140,12 @@ export const useAskAi: UseAskAi = ({
|
||||||
getAgentStudioTransport({
|
getAgentStudioTransport({
|
||||||
apiKey,
|
apiKey,
|
||||||
appId,
|
appId,
|
||||||
assistantId,
|
agentId,
|
||||||
searchParameters,
|
searchParameters,
|
||||||
userToken: memory?.userToken,
|
userToken: memory?.userToken,
|
||||||
indices,
|
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:
|
// Store transport in a ref since it is dependent on unstable dependencies:
|
||||||
|
|
@ -224,10 +223,10 @@ export const useAskAi: UseAskAi = ({
|
||||||
|
|
||||||
const sendFeedback = useCallback<OnAskAiFeedback>(
|
const sendFeedback = useCallback<OnAskAiFeedback>(
|
||||||
async (messageId, { thumbs, tags, notes }): Promise<void> => {
|
async (messageId, { thumbs, tags, notes }): Promise<void> => {
|
||||||
if (!assistantId) return;
|
if (!agentId) return;
|
||||||
|
|
||||||
const res = await postAgentStudioFeedback({
|
const res = await postAgentStudioFeedback({
|
||||||
agentId: assistantId,
|
agentId,
|
||||||
vote: thumbs,
|
vote: thumbs,
|
||||||
messageId,
|
messageId,
|
||||||
appId,
|
appId,
|
||||||
|
|
@ -244,7 +243,7 @@ export const useAskAi: UseAskAi = ({
|
||||||
{ tags, notes }
|
{ tags, notes }
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[assistantId, appId, apiKey, conversations]
|
[agentId, appId, apiKey, conversations]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onStopStreaming = useCallback(async (): Promise<void> => {
|
const onStopStreaming = useCallback(async (): Promise<void> => {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ export function useFacetValues({
|
||||||
const facetKeys = stableFacetKeys ? stableFacetKeys.split(',') : [];
|
const facetKeys = stableFacetKeys ? stableFacetKeys.split(',') : [];
|
||||||
|
|
||||||
if (facetKeys.length === 0 || indexes.length === 0) {
|
if (facetKeys.length === 0 || indexes.length === 0) {
|
||||||
setFacetValues({});
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
};
|
};
|
||||||
|
|
@ -41,7 +40,6 @@ export function useFacetValues({
|
||||||
searchClient
|
searchClient
|
||||||
.search<DocSearchHit>({
|
.search<DocSearchHit>({
|
||||||
requests: indexes.map((index) => ({
|
requests: indexes.map((index) => ({
|
||||||
...index.searchParameters,
|
|
||||||
indexName: index.name,
|
indexName: index.name,
|
||||||
query: '',
|
query: '',
|
||||||
hitsPerPage: 0,
|
hitsPerPage: 0,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,13 @@ import type {
|
||||||
} from '.';
|
} from '.';
|
||||||
|
|
||||||
type UseSuggestedQuestionsProps = {
|
type UseSuggestedQuestionsProps = {
|
||||||
assistantId: string | null;
|
agentId: string | null;
|
||||||
searchClient: DocSearchTransformClient;
|
searchClient: DocSearchTransformClient;
|
||||||
suggestedQuestionsEnabled?: boolean;
|
suggestedQuestionsEnabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSuggestedQuestions = ({
|
export const useSuggestedQuestions = ({
|
||||||
assistantId,
|
agentId,
|
||||||
searchClient,
|
searchClient,
|
||||||
suggestedQuestionsEnabled = false,
|
suggestedQuestionsEnabled = false,
|
||||||
}: UseSuggestedQuestionsProps): SuggestedQuestionHit[] => {
|
}: UseSuggestedQuestionsProps): SuggestedQuestionHit[] => {
|
||||||
|
|
@ -30,7 +30,7 @@ export const useSuggestedQuestions = ({
|
||||||
requests: [
|
requests: [
|
||||||
{
|
{
|
||||||
indexName: SUGGESTED_QUETIONS_INDEX_NAME,
|
indexName: SUGGESTED_QUETIONS_INDEX_NAME,
|
||||||
filters: `state:published AND assistantId:${assistantId}`,
|
filters: `state:published AND assistantId:${agentId}`,
|
||||||
hitsPerPage: 3,
|
hitsPerPage: 3,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -41,10 +41,10 @@ export const useSuggestedQuestions = ({
|
||||||
setSuggestedQuestions(result.hits);
|
setSuggestedQuestions(result.hits);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (suggestedQuestionsEnabled && assistantId && assistantId !== '') {
|
if (suggestedQuestionsEnabled && agentId && agentId !== '') {
|
||||||
getSuggestedQuestions();
|
getSuggestedQuestions();
|
||||||
}
|
}
|
||||||
}, [suggestedQuestionsEnabled, assistantId, searchClient]);
|
}, [suggestedQuestionsEnabled, agentId, searchClient]);
|
||||||
|
|
||||||
return suggestedQuestions;
|
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,
|
AutocompleteSource,
|
||||||
AutocompleteState,
|
AutocompleteState,
|
||||||
} from '@algolia/autocomplete-core';
|
} 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 React from 'react';
|
||||||
|
|
||||||
import type { DocSearchIndex, DocSearchProps } from '../DocSearch';
|
import type { DocSearchIndex, DocSearchProps } from '../DocSearch';
|
||||||
|
|
@ -34,19 +38,43 @@ export function createFacetFilters(
|
||||||
searchParametersFacetFilters: SearchParamsObject['facetFilters'],
|
searchParametersFacetFilters: SearchParamsObject['facetFilters'],
|
||||||
facetSelections: FacetSelections
|
facetSelections: FacetSelections
|
||||||
): SearchParamsObject['facetFilters'] {
|
): SearchParamsObject['facetFilters'] {
|
||||||
const dynamicFacetFilters = Object.entries(facetSelections)
|
const selections = Object.entries(facetSelections);
|
||||||
.filter(([, value]) => value)
|
const selectedFacets = new Set(selections.map(([facet]) => facet));
|
||||||
.map(([facet, value]) => `${facet}:${value}`);
|
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;
|
return searchParametersFacetFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!searchParametersFacetFilters) {
|
let configuredFacetFilters: FacetFilters = [];
|
||||||
return dynamicFacetFilters;
|
|
||||||
|
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({
|
export function buildNoQuerySources({
|
||||||
|
|
@ -221,10 +249,8 @@ export async function buildQuerySources({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = Object.values(sources).flat();
|
return Object.values<DocSearchHit[]>(sources).map((items, index) => ({
|
||||||
|
sourceId: `hits_${result.index}_${index}`,
|
||||||
return {
|
|
||||||
sourceId: `hits_${result.index}`,
|
|
||||||
onSelect({ item, event }): void {
|
onSelect({ item, event }): void {
|
||||||
saveRecentSearch(item);
|
saveRecentSearch(item);
|
||||||
if (!isModifierEvent(event)) {
|
if (!isModifierEvent(event)) {
|
||||||
|
|
@ -234,36 +260,34 @@ export async function buildQuerySources({
|
||||||
getItemUrl({ item }): string {
|
getItemUrl({ item }): string {
|
||||||
return item.url;
|
return item.url;
|
||||||
},
|
},
|
||||||
getItems() {
|
getItems(): InternalDocSearchHit[] {
|
||||||
return Object.values(
|
return Object.values(
|
||||||
groupBy(items, (item) => item.hierarchy.lvl1, maxResultsPerGroup)
|
groupBy(items, (item) => item.hierarchy.lvl1, maxResultsPerGroup)
|
||||||
)
|
)
|
||||||
.map((groupedHits) =>
|
.map((groupedHits) =>
|
||||||
groupedHits
|
groupedHits.map((item) => {
|
||||||
.map((item) => {
|
let parent: InternalDocSearchHit | null = null;
|
||||||
let parent: InternalDocSearchHit | null = null;
|
|
||||||
|
|
||||||
const potentialParent = groupedHits.find(
|
const potentialParent = groupedHits.find(
|
||||||
(siblingItem) =>
|
(siblingItem) =>
|
||||||
siblingItem.type === 'lvl1' &&
|
siblingItem.type === 'lvl1' &&
|
||||||
siblingItem.hierarchy.lvl1 === item.hierarchy.lvl1
|
siblingItem.hierarchy.lvl1 === item.hierarchy.lvl1
|
||||||
) as InternalDocSearchHit | undefined;
|
) as InternalDocSearchHit | undefined;
|
||||||
|
|
||||||
if (item.type !== 'lvl1' && potentialParent) {
|
if (item.type !== 'lvl1' && potentialParent) {
|
||||||
parent = potentialParent;
|
parent = potentialParent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
__docsearch_parent: parent,
|
__docsearch_parent: parent,
|
||||||
...insightsParams,
|
...insightsParams,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.flat()
|
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
},
|
},
|
||||||
};
|
}));
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error).name === 'RetryError') {
|
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;
|
export const MAX_FACETS = 5;
|
||||||
|
|
||||||
|
|
@ -40,3 +42,36 @@ export function getFacetLabel(facet: DocSearchFacet): string {
|
||||||
.replace(/[._-]+/g, ' ')
|
.replace(/[._-]+/g, ' ')
|
||||||
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
.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 />
|
<SidepanelButton />
|
||||||
<Sidepanel
|
<Sidepanel
|
||||||
appId="YOUR_APP_ID"
|
appId="YOUR_APP_ID"
|
||||||
indexName="YOUR_INDEX_NAME"
|
|
||||||
apiKey="YOUR_SEARCH_API_KEY"
|
apiKey="YOUR_SEARCH_API_KEY"
|
||||||
assistantId="YOUR_ASK_AI_ASSISTANT_ID"
|
agentId="YOUR_ASK_AI_ASSISTANT_ID"
|
||||||
/>
|
/>
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ import type { JSX } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
export type SidepanelProps = DocSearchSidepanelProps['panel'] &
|
export type SidepanelProps = DocSearchSidepanelProps['panel'] &
|
||||||
Omit<DocSearchSidepanelProps, 'button' | 'panel' | 'theme'> &
|
Omit<
|
||||||
|
DocSearchSidepanelProps,
|
||||||
|
'appId' | 'apiKey' | 'button' | 'panel' | 'theme'
|
||||||
|
> &
|
||||||
|
Partial<Pick<DocSearchSidepanelProps, 'appId' | 'apiKey'>> &
|
||||||
SidepanelSearchParameters;
|
SidepanelSearchParameters;
|
||||||
|
|
||||||
export function Sidepanel({
|
export function Sidepanel({
|
||||||
|
|
@ -17,6 +21,8 @@ export function Sidepanel({
|
||||||
...props
|
...props
|
||||||
}: SidepanelProps): JSX.Element {
|
}: SidepanelProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
|
appId: providerAppId,
|
||||||
|
apiKey: providerApiKey,
|
||||||
docsearchState,
|
docsearchState,
|
||||||
setDocsearchState,
|
setDocsearchState,
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
|
|
@ -24,6 +30,15 @@ export function Sidepanel({
|
||||||
initialAskAiMessage,
|
initialAskAiMessage,
|
||||||
} = useDocSearch();
|
} = 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 => {
|
const handleOpen = React.useCallback((): void => {
|
||||||
setDocsearchState('sidepanel');
|
setDocsearchState('sidepanel');
|
||||||
}, [setDocsearchState]);
|
}, [setDocsearchState]);
|
||||||
|
|
@ -49,12 +64,16 @@ export function Sidepanel({
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
initialMessage: initialAskAiMessage,
|
initialMessage: initialAskAiMessage,
|
||||||
...props,
|
...props,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
docsearchState,
|
docsearchState,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
handleClose,
|
handleClose,
|
||||||
props,
|
props,
|
||||||
|
appId,
|
||||||
|
apiKey,
|
||||||
keyboardShortcuts,
|
keyboardShortcuts,
|
||||||
initialAskAiMessage,
|
initialAskAiMessage,
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ bun add @docsearch/core@^5.0.0-beta @docsearch/modal@^5.0.0-beta @docsearch/css@
|
||||||
|
|
||||||
## Add keyword search
|
## 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"
|
```tsx title="KeywordSearch.tsx"
|
||||||
import { DocSearch } from '@docsearch/core';
|
import { DocSearch } from '@docsearch/core';
|
||||||
|
|
@ -85,14 +85,14 @@ export function KeywordSearch({
|
||||||
indexName,
|
indexName,
|
||||||
}: KeywordSearchProps): JSX.Element {
|
}: KeywordSearchProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch>
|
<DocSearch appId={appId} apiKey={apiKey}>
|
||||||
<DocSearchButton
|
<DocSearchButton
|
||||||
translations={{
|
translations={{
|
||||||
buttonText: 'Search docs',
|
buttonText: 'Search docs',
|
||||||
buttonAriaLabel: 'Search documentation',
|
buttonAriaLabel: 'Search documentation',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<DocSearchModal appId={appId} apiKey={apiKey} indices={[indexName]} />
|
<DocSearchModal indices={[indexName]} />
|
||||||
</DocSearch>
|
</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
|
## 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"
|
```tsx title="SearchWithAskAi.tsx"
|
||||||
import { DocSearch } from '@docsearch/core';
|
import { DocSearch } from '@docsearch/core';
|
||||||
|
|
@ -115,30 +115,28 @@ interface SearchWithAskAiProps {
|
||||||
appId: string;
|
appId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
indexName: string;
|
indexName: string;
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchWithAskAi({
|
export function SearchWithAskAi({
|
||||||
appId,
|
appId,
|
||||||
apiKey,
|
apiKey,
|
||||||
indexName,
|
indexName,
|
||||||
assistantId,
|
agentId,
|
||||||
}: SearchWithAskAiProps): JSX.Element {
|
}: SearchWithAskAiProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<DocSearch>
|
<DocSearch appId={appId} apiKey={apiKey}>
|
||||||
<DocSearchButton />
|
<DocSearchButton />
|
||||||
<DocSearchAskAiModal
|
<DocSearchAskAiModal
|
||||||
appId={appId}
|
|
||||||
apiKey={apiKey}
|
|
||||||
indices={[indexName]}
|
indices={[indexName]}
|
||||||
askAi={{ assistantId }}
|
askAi={agentId}
|
||||||
/>
|
/>
|
||||||
</DocSearch>
|
</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
|
## Understand the shared state
|
||||||
|
|
||||||
|
|
@ -200,7 +198,7 @@ interface ControlledSearchProps {
|
||||||
appId: string;
|
appId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
indexName: string;
|
indexName: string;
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
onReady?: () => void;
|
onReady?: () => void;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
|
@ -231,7 +229,7 @@ export function ControlledSearch(props: ControlledSearchProps): JSX.Element {
|
||||||
appId={props.appId}
|
appId={props.appId}
|
||||||
apiKey={props.apiKey}
|
apiKey={props.apiKey}
|
||||||
indices={[props.indexName]}
|
indices={[props.indexName]}
|
||||||
askAi={{ assistantId: props.assistantId }}
|
askAi={props.agentId}
|
||||||
/>
|
/>
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
);
|
);
|
||||||
|
|
@ -339,7 +337,7 @@ interface LazySearchProps {
|
||||||
appId: string;
|
appId: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
indexName: string;
|
indexName: string;
|
||||||
assistantId: string;
|
agentId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LazySearch(props: LazySearchProps): JSX.Element {
|
export function LazySearch(props: LazySearchProps): JSX.Element {
|
||||||
|
|
@ -354,7 +352,7 @@ export function LazySearch(props: LazySearchProps): JSX.Element {
|
||||||
appId={props.appId}
|
appId={props.appId}
|
||||||
apiKey={props.apiKey}
|
apiKey={props.apiKey}
|
||||||
indices={[props.indexName]}
|
indices={[props.indexName]}
|
||||||
askAi={{ assistantId: props.assistantId }}
|
askAi={props.agentId}
|
||||||
/>
|
/>
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
);
|
);
|
||||||
|
|
@ -401,12 +399,12 @@ Add `@docsearch/css/dist/_askai.css` when you use `DocSearchAskAiModal`. Bundler
|
||||||
|
|
||||||
| API | Required configuration | Provider-managed behavior |
|
| 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 |
|
| `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 |
|
| `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 |
|
| `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`.
|
`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`
|
## 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"
|
```diff title="app.js"
|
||||||
docsearch({
|
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
|
## 5. Update result customization
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ Public API key with search permission.
|
||||||
|
|
||||||
> `type: Array<string | DocSearchIndex>` | **optional**
|
> `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"
|
```tsx title="Search.tsx"
|
||||||
<DocSearch
|
<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`
|
### `facets`
|
||||||
|
|
||||||
> `type: DocSearchFacet[]` | **optional**
|
> `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.
|
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`
|
### `maxResultsPerGroup`
|
||||||
|
|
||||||
> `type: number` | **optional**
|
> `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`.
|
Agent Studio assistant ID or configuration. This prop is required by `DocSearchAI`.
|
||||||
|
|
||||||
#### `assistantId`
|
#### `agentId`
|
||||||
|
|
||||||
> `type: string` | **required**
|
> `type: string` | **required**
|
||||||
|
|
||||||
Agent Studio assistant ID.
|
Agent Studio agent ID.
|
||||||
|
|
||||||
#### `appId`
|
#### `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`.
|
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`
|
#### `suggestedQuestions`
|
||||||
|
|
||||||
> `type: boolean` | **optional**
|
> `type: boolean` | **optional**
|
||||||
|
|
@ -289,7 +271,7 @@ Search parameters keyed by index name. Each value supports `filters`, `attribute
|
||||||
```tsx title="Search.tsx"
|
```tsx title="Search.tsx"
|
||||||
<DocSearchAI
|
<DocSearchAI
|
||||||
askAi={{
|
askAi={{
|
||||||
assistantId: 'YOUR_ASSISTANT_ID',
|
agentId: 'YOUR_AGENT_ID',
|
||||||
searchParameters: {
|
searchParameters: {
|
||||||
docs: {
|
docs: {
|
||||||
filters: 'language:en',
|
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`.
|
- `react`, `react-dom`, and `@types/react` support versions `>=16.8.0 <20.0.0`.
|
||||||
- `search-insights` supports versions `>=1 <3` and is optional.
|
- `search-insights` supports versions `>=1 <3` and is optional.
|
||||||
- The browser build targets ES2017.
|
- 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.
|
- `UseDocSearchKeyboardEventsProps.onInput` and `searchButtonRef` remain accepted for compatibility but are deprecated.
|
||||||
- `DocSearch` is keyword-only in v5. Use `DocSearchAI` for Ask AI.
|
- `DocSearch` is keyword-only in v5. Use `DocSearchAI` for Ask AI.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ export default {
|
||||||
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
apiKey: '24b09689d5b4223813d9b8e48563c8f6',
|
||||||
indices: [{ name: 'docsearch' }],
|
indices: [{ name: 'docsearch' }],
|
||||||
askAi: {
|
askAi: {
|
||||||
assistantId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
agentId: 'ccdec697-e3fe-465b-a1c3-657e7bf18aef',
|
||||||
},
|
},
|
||||||
sidePanel: true,
|
sidePanel: true,
|
||||||
contextualSearch: true,
|
contextualSearch: true,
|
||||||
|
|
|
||||||
|
|
@ -80,17 +80,26 @@ export default function DemoApp() {
|
||||||
|
|
||||||
HTMLElement.prototype.focus = function focusNoScroll(options) {
|
HTMLElement.prototype.focus = function focusNoScroll(options) {
|
||||||
if (!userEngaged) return undefined;
|
if (!userEngaged) return undefined;
|
||||||
return nativeFocus.call(this, { preventScroll: true, ...(options ?? {}) });
|
return nativeFocus.call(this, {
|
||||||
|
preventScroll: true,
|
||||||
|
...(options ?? {}),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Element.prototype.scrollIntoView = function containedScrollIntoView(arg) {
|
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;
|
let ancestor = this.parentElement;
|
||||||
while (ancestor) {
|
while (ancestor) {
|
||||||
const overflowY = getComputedStyle(ancestor).overflowY;
|
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 =
|
const top =
|
||||||
this.getBoundingClientRect().top - ancestor.getBoundingClientRect().top + ancestor.scrollTop;
|
this.getBoundingClientRect().top -
|
||||||
|
ancestor.getBoundingClientRect().top +
|
||||||
|
ancestor.scrollTop;
|
||||||
ancestor.scrollTo({ top, behavior });
|
ancestor.scrollTo({ top, behavior });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -164,10 +173,9 @@ export default function DemoApp() {
|
||||||
<DocSearchSidepanel
|
<DocSearchSidepanel
|
||||||
ref={sidepanelRef}
|
ref={sidepanelRef}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
indexName={INDEX_NAME}
|
|
||||||
appId={APP_ID}
|
appId={APP_ID}
|
||||||
apiKey={API_KEY}
|
apiKey={API_KEY}
|
||||||
assistantId={ASSISTANT_ID}
|
agentId={ASSISTANT_ID}
|
||||||
button={{ variant: 'inline' }}
|
button={{ variant: 'inline' }}
|
||||||
panel={{ suggestedQuestions: true }}
|
panel={{ suggestedQuestions: true }}
|
||||||
/>
|
/>
|
||||||
|
|
@ -186,7 +194,7 @@ export default function DemoApp() {
|
||||||
indices={[INDEX_NAME]}
|
indices={[INDEX_NAME]}
|
||||||
appId={APP_ID}
|
appId={APP_ID}
|
||||||
apiKey={API_KEY}
|
apiKey={API_KEY}
|
||||||
askAi={{ assistantId: ASSISTANT_ID }}
|
askAi={ASSISTANT_ID}
|
||||||
navigator={navigator}
|
navigator={navigator}
|
||||||
/>
|
/>
|
||||||
</DocSearch>
|
</DocSearch>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ npm install @docsearch/react@4 @docsearch/css@4
|
||||||
import { DocSearch } from '@docsearch/react';
|
import { DocSearch } from '@docsearch/react';
|
||||||
import '@docsearch/css';
|
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)
|
### Docusaurus (recommended)
|
||||||
|
|
@ -47,7 +47,7 @@ themeConfig: {
|
||||||
docsearch: {
|
docsearch: {
|
||||||
appId: 'YOUR_APP_ID',
|
appId: 'YOUR_APP_ID',
|
||||||
apiKey: 'YOUR_SEARCH_KEY',
|
apiKey: 'YOUR_SEARCH_KEY',
|
||||||
indexName: 'YOUR_INDEX',
|
indices: ['YOUR_INDEX'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue