diff --git a/etc/search-ui-react.api.md b/etc/search-ui-react.api.md
index 00e0fb86..0ba595fb 100644
--- a/etc/search-ui-react.api.md
+++ b/etc/search-ui-react.api.md
@@ -35,6 +35,19 @@ export interface AfterDropdownInputFocusProps {
value: string;
}
+// @public
+export function AISignpostIcon(input: {
+ className?: string;
+}): React_2.JSX.Element;
+
+// @public
+export interface AISignpostProps {
+ icon?: React_2.JSX.Element;
+ label?: string;
+ popoverBody?: string;
+ popoverHeader?: string;
+}
+
// @public
export function AlternativeVerticals(input: AlternativeVerticalsProps): React_2.JSX.Element | null;
@@ -377,11 +390,13 @@ export interface GenerativeDirectAnswerCssClasses {
// @public
export interface GenerativeDirectAnswerProps {
+ aiSignpostProps?: AISignpostProps;
answerHeader?: string | React_2.JSX.Element;
CitationCard?: (props: CitationProps) => React_2.JSX.Element | null;
CitationsContainer?: (props: CitationsProps) => React_2.JSX.Element | null;
citationsHeader?: string | React_2.JSX.Element;
customCssClasses?: GenerativeDirectAnswerCssClasses;
+ hideAISignpost?: boolean;
}
// @public
@@ -1158,7 +1173,7 @@ export interface VisualAutocompleteConfig {
// Warnings were encountered during analysis:
//
-// dist/index.d.ts:1891:5 - (ae-forgotten-export) The symbol "translations" needs to be exported by the entry point index.d.ts
+// dist/index.d.ts:1919:5 - (ae-forgotten-export) The symbol "translations" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package)
diff --git a/locales/en/search-ui-react.json b/locales/en/search-ui-react.json
index b5d2d958..2230b0fd 100644
--- a/locales/en/search-ui-react.json
+++ b/locales/en/search-ui-react.json
@@ -1,5 +1,8 @@
{
"aiGeneratedAnswer": "AI Generated Answer",
+ "aiGeneratedAnswerSignpostLabel": "AI-Generated",
+ "aiGeneratedAnswerSignpostPopoverHeader": "AI-Generated Content",
+ "aiGeneratedAnswerSignpostPopoverBody": "This answer was generated by AI from matching search results and may be incomplete or inaccurate. Review the matching results for full context.",
"allCategories": "All Categories",
"appliedFiltersToCurrentSearch": "Applied filters to current search",
"apply": "Apply",
@@ -20,6 +23,7 @@
"conductASearch": "Conduct a search",
"currentLocation": "Current Location",
"didYouMean": "Did you mean {{correctedQuery}} ",
+ "dismiss": "Dismiss",
"dropDownScreenReaderInstructions": "When autocomplete results are available, use up and down arrows to review and enter to select.",
"feedback": "Feedback",
"filterGroupSearchInputLabel": "Search {{title}} Options",
diff --git a/package-lock.json b/package-lock.json
index 055c6c39..51587948 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@yext/search-ui-react",
- "version": "3.0.6",
+ "version": "3.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@yext/search-ui-react",
- "version": "3.0.6",
+ "version": "3.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"@restart/ui": "^1.0.1",
@@ -59,6 +59,7 @@
"@types/jest": "^29.5.12",
"@types/jest-image-snapshot": "^6.2.3",
"@types/lodash": "^4.14.199",
+ "@types/mapbox-gl": "^2.7.5",
"@types/prop-types": "^15.7.15",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^5.16.0",
@@ -6745,6 +6746,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mapbox-gl": {
+ "version": "2.7.21",
+ "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.21.tgz",
+ "integrity": "sha512-Dx9MuF2kKgT/N22LsMUB4b3acFZh9clVqz9zv1fomoiPoBrJolwYxpWA/9LPO/2N0xWbKi4V+pkjTaFkkx/4wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/mdast": {
"version": "3.0.15",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz",
diff --git a/package.json b/package.json
index 7a263365..ac55ae84 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@yext/search-ui-react",
- "version": "3.0.6",
+ "version": "3.1.0",
"description": "A library of React Components for powering Yext Search integrations",
"author": "watson@yext.com",
"license": "BSD-3-Clause",
@@ -88,6 +88,7 @@
"@types/jest": "^29.5.12",
"@types/jest-image-snapshot": "^6.2.3",
"@types/lodash": "^4.14.199",
+ "@types/mapbox-gl": "^2.7.5",
"@types/prop-types": "^15.7.15",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^5.16.0",
diff --git a/src/components/GenerativeDirectAnswer.tsx b/src/components/GenerativeDirectAnswer.tsx
index f9aa893e..99d2cf15 100644
--- a/src/components/GenerativeDirectAnswer.tsx
+++ b/src/components/GenerativeDirectAnswer.tsx
@@ -10,7 +10,10 @@ import { useComposedCssClasses } from '../hooks';
import { useCardAnalytics } from '../hooks/useCardAnalytics';
import { DefaultRawDataType } from '../models/index';
import { executeGenerativeDirectAnswer } from '../utils/search-operations';
+import { AISignpostIcon } from '../icons/AISignpostIcon';
+import { CloseIcon } from '../icons/CloseIcon';
import { Markdown, MarkdownCssClasses } from './Markdown';
+import { useId } from '../hooks/useId';
import React, { useCallback, useMemo, useRef } from 'react';
/**
@@ -50,6 +53,10 @@ export interface GenerativeDirectAnswerProps {
customCssClasses?: GenerativeDirectAnswerCssClasses,
/** The header for the answer section of the generative direct answer. */
answerHeader?: string | React.JSX.Element,
+ /** Whether to hide the AI signpost for the generative direct answer. */
+ hideAISignpost?: boolean,
+ /** The props to pass to the AI signpost component. */
+ aiSignpostProps?: AISignpostProps,
/** The header for the citations section of the generative direct answer. */
citationsHeader?: string | React.JSX.Element,
/**
@@ -74,6 +81,8 @@ export interface GenerativeDirectAnswerProps {
export function GenerativeDirectAnswer({
customCssClasses,
answerHeader,
+ hideAISignpost = false,
+ aiSignpostProps,
citationsHeader,
CitationCard,
CitationsContainer = Citations,
@@ -117,6 +126,8 @@ export function GenerativeDirectAnswer({
gdaResponse={gdaResponse}
cssClasses={cssClasses}
answerHeader={answerHeader}
+ hideAISignpost={hideAISignpost}
+ aiSignpostProps={aiSignpostProps}
linkClickHandler={handleClickEvent}
/>
void
}
+/**
+ * Props for the built-in AI signpost component.
+ *
+ * @public
+ */
+export interface AISignpostProps {
+ /** Icon displayed before the signpost label. Defaults to the SDK's AI signpost icon. */
+ icon?: React.JSX.Element,
+ /** Label displayed in the signpost button. Defaults to "AI-Generated". */
+ label?: string,
+ /** Header displayed in the signpost popover. Defaults to "AI-Generated Content". */
+ popoverHeader?: string,
+ /** Body displayed in the signpost popover. */
+ popoverBody?: string
+}
+
+/**
+ * Displays AI signpost content for the generative direct answer.
+ */
+function AISignpost({
+ icon,
+ label,
+ popoverHeader,
+ popoverBody
+}: AISignpostProps): React.JSX.Element {
+ const { t } = useTranslation();
+ const [isOpen, setIsOpen] = React.useState(false);
+ const popoverId = useId('ai-signpost-popover');
+ const popoverHeaderId = useId('ai-signpost-popover-header');
+ const popoverDescriptionId = useId('ai-signpost-popover-description');
+ const handleSignpostClick = useCallback(() => {
+ setIsOpen(current => !current);
+ }, []);
+ const onSignpostClose = useCallback(() => {
+ setIsOpen(false);
+ }, []);
+
+ return (
+
+
+ {icon ?? }
+ {label ?? t('aiGeneratedAnswerSignpostLabel')}
+
+ {isOpen && (
+
+
+
+
+
+
+
+
+
+ {popoverBody ?? t('aiGeneratedAnswerSignpostPopoverBody')}
+
+
+
+ )}
+
+ );
+}
+
/**
* The answer section of the Generative Direct Answer.
*/
@@ -146,6 +240,8 @@ function Answer(props: AnswerProps) {
gdaResponse,
cssClasses,
answerHeader,
+ hideAISignpost,
+ aiSignpostProps,
linkClickHandler
} = props;
const { t } = useTranslation();
@@ -166,6 +262,7 @@ function Answer(props: AnswerProps) {
{answerHeader ?? t('aiGeneratedAnswer')}
+ {!hideAISignpost && }
+
+
+ );
+}
diff --git a/src/utils/i18n.ts b/src/utils/i18n.ts
index 72750377..41d49a17 100644
--- a/src/utils/i18n.ts
+++ b/src/utils/i18n.ts
@@ -1,4 +1,4 @@
-import i18next from 'i18next';
+import { createInstance } from 'i18next';
import { initReactI18next } from 'react-i18next';
const { supportedLocales } = require('./supportedLocales.ts');
@@ -16,7 +16,7 @@ supportedLocales.forEach((locale: string) => {
*
* @internal
*/
-const i18nInstance = i18next.createInstance();
+const i18nInstance = createInstance();
i18nInstance.use(initReactI18next).init({
fallbackLng: 'en',
ns: [NAMESPACE],
diff --git a/test-site/package-lock.json b/test-site/package-lock.json
index 70b9ead9..dffe6a5a 100644
--- a/test-site/package-lock.json
+++ b/test-site/package-lock.json
@@ -33,7 +33,7 @@
},
"..": {
"name": "@yext/search-ui-react",
- "version": "3.0.6",
+ "version": "3.1.0",
"license": "BSD-3-Clause",
"dependencies": {
"@restart/ui": "^1.0.1",
@@ -86,6 +86,7 @@
"@types/jest": "^29.5.12",
"@types/jest-image-snapshot": "^6.2.3",
"@types/lodash": "^4.14.199",
+ "@types/mapbox-gl": "^2.7.5",
"@types/prop-types": "^15.7.15",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^5.16.0",
diff --git a/test-site/src/pages/UniversalPage.tsx b/test-site/src/pages/UniversalPage.tsx
index 8d34258e..2718da15 100644
--- a/test-site/src/pages/UniversalPage.tsx
+++ b/test-site/src/pages/UniversalPage.tsx
@@ -1,5 +1,6 @@
import { provideHeadless, useSearchActions, Result } from '@yext/search-headless-react';
import {
+ AISignpostIcon,
DirectAnswer,
DropdownItem,
ResultsCount,
@@ -81,6 +82,13 @@ function CustomCitationsComponent(props: CitationsProps): React.JSX.Element | nu
)
}
+function CustomAISignpost(): React.JSX.Element {
+ return (
+
+
+ );
+}
+
export default function UniversalPage(): React.JSX.Element {
const searchActions = useSearchActions();
useLayoutEffect(() => {
@@ -96,18 +104,23 @@ export default function UniversalPage(): React.JSX.Element {
universalAutocompleteLimit={20}
/>
-
-
- {/* Example of passing in custom citations component to GDA */}
{/* */}
+ {/* Example of passing in custom citations cards & signposting to GDA */}
+
+
{
beforeEach(() => {
mockAnswersState(mockedState);
});
+
+ it('displays the default AI signpost below the answer header', () => {
+ render( );
+
+ const answerHeader = screen.getByText('AI Generated Answer');
+ const signpost = screen.getByRole('button', { name: 'AI-Generated' });
+ const answerLink = screen.getByRole('link', { name: generativeDirectAnswerText });
+
+ expect(answerHeader.compareDocumentPosition(signpost) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ expect(signpost.compareDocumentPosition(answerLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
+ });
+
+ it('displays the default AI signpost popover', async () => {
+ render( );
+
+ await userEvent.click(screen.getByRole('button', { name: 'AI-Generated' }));
+
+ expect(screen.getByRole('dialog', { name: 'AI-Generated Content' })).toBeTruthy();
+ expect(screen.getByText(
+ 'This answer was generated by AI from matching search results and may be incomplete or inaccurate. Review the matching results for full context.'
+ )).toBeTruthy();
+ });
+
+ it('hides the AI signpost', () => {
+ render( );
+
+ expect(screen.queryByRole('button', { name: 'AI-Generated' })).toBeNull();
+ });
+
+ it('overrides the AI signpost content', async () => {
+ render(Custom Icon,
+ label: 'Custom Label',
+ popoverHeader: 'Custom Header',
+ popoverBody: 'Custom Body'
+ }}
+ />);
+
+ expect(screen.getByTestId('custom-ai-signpost-icon')).toBeTruthy();
+ await userEvent.click(screen.getByRole('button', { name: 'Custom Icon Custom Label' }));
+
+ expect(screen.getByRole('dialog', { name: 'Custom Header' })).toBeTruthy();
+ expect(screen.getByText('Custom Body')).toBeTruthy();
+ });
+
it('answer text and all citations are displayed', () => {
render( );
expect(screen.getByText(generativeDirectAnswerText)).toBeDefined();