From 7d0af3ad7a9befc508a6fefd33ed5893df5a9964 Mon Sep 17 00:00:00 2001 From: Maxfield Kouzel Date: Thu, 18 Jun 2026 10:53:09 -0400 Subject: [PATCH 1/2] feat: add AI signpost to chat pop up Adds an signpost to the ChatPopUp component that informs users that chat responses are AI-generated. The content of the signpost is customizable, but defaults are provided. The signposting is required to comply with upcoming EU regulations, and is therefore on by default. J=[WAT-5644](https://yext.atlassian.net/browse/WAT-5644) TEST=manual,auto --- docs/chat-ui-react.aisignposticon.md | 26 +++++ docs/chat-ui-react.aisignpostprops.icon.md | 13 +++ docs/chat-ui-react.aisignpostprops.label.md | 13 +++ docs/chat-ui-react.aisignpostprops.md | 23 ++++ ...at-ui-react.aisignpostprops.popoverbody.md | 13 +++ ...-ui-react.aisignpostprops.popoverheader.md | 13 +++ docs/chat-ui-react.chatheader.md | 4 +- ...i-react.chatheaderprops.aisignpostprops.md | 13 +++ ...ui-react.chatheaderprops.hideaisignpost.md | 13 +++ docs/chat-ui-react.chatheaderprops.md | 2 + docs/chat-ui-react.md | 4 +- etc/chat-ui-react.api.md | 17 ++- package-lock.json | 3 +- package.json | 3 +- src/components/ChatHeader.tsx | 101 +++++++++++++++++- src/components/ChatPopUp.tsx | 4 + src/components/index.ts | 7 +- src/hooks/useId.tsx | 63 +++++++++++ src/icons/AISignpostIcon.tsx | 27 +++++ tests/components/ChatHeader.stories.tsx | 23 ++++ tests/components/ChatHeader.test.tsx | 52 +++++++++ 21 files changed, 429 insertions(+), 8 deletions(-) create mode 100644 docs/chat-ui-react.aisignposticon.md create mode 100644 docs/chat-ui-react.aisignpostprops.icon.md create mode 100644 docs/chat-ui-react.aisignpostprops.label.md create mode 100644 docs/chat-ui-react.aisignpostprops.md create mode 100644 docs/chat-ui-react.aisignpostprops.popoverbody.md create mode 100644 docs/chat-ui-react.aisignpostprops.popoverheader.md create mode 100644 docs/chat-ui-react.chatheaderprops.aisignpostprops.md create mode 100644 docs/chat-ui-react.chatheaderprops.hideaisignpost.md create mode 100644 src/hooks/useId.tsx create mode 100644 src/icons/AISignpostIcon.tsx diff --git a/docs/chat-ui-react.aisignposticon.md b/docs/chat-ui-react.aisignposticon.md new file mode 100644 index 0000000..a3bc627 --- /dev/null +++ b/docs/chat-ui-react.aisignposticon.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostIcon](./chat-ui-react.aisignposticon.md) + +## AISignpostIcon() function + +Default icon for the AI signpost. + +**Signature:** + +```typescript +export declare function AISignpostIcon({ className, }: { + className?: string; +}): React.JSX.Element; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { className, } | { className?: string; } | | + +**Returns:** + +React.JSX.Element + diff --git a/docs/chat-ui-react.aisignpostprops.icon.md b/docs/chat-ui-react.aisignpostprops.icon.md new file mode 100644 index 0000000..0b7048c --- /dev/null +++ b/docs/chat-ui-react.aisignpostprops.icon.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostProps](./chat-ui-react.aisignpostprops.md) > [icon](./chat-ui-react.aisignpostprops.icon.md) + +## AISignpostProps.icon property + +Icon displayed before the signpost label. Defaults to the SDK's AI signpost icon. + +**Signature:** + +```typescript +icon?: React.JSX.Element; +``` diff --git a/docs/chat-ui-react.aisignpostprops.label.md b/docs/chat-ui-react.aisignpostprops.label.md new file mode 100644 index 0000000..0b94322 --- /dev/null +++ b/docs/chat-ui-react.aisignpostprops.label.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostProps](./chat-ui-react.aisignpostprops.md) > [label](./chat-ui-react.aisignpostprops.label.md) + +## AISignpostProps.label property + +Label displayed in the signpost button. Defaults to "AI-Powered". + +**Signature:** + +```typescript +label?: string; +``` diff --git a/docs/chat-ui-react.aisignpostprops.md b/docs/chat-ui-react.aisignpostprops.md new file mode 100644 index 0000000..15543cd --- /dev/null +++ b/docs/chat-ui-react.aisignpostprops.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostProps](./chat-ui-react.aisignpostprops.md) + +## AISignpostProps interface + +Props for the built-in AI signpost component. + +**Signature:** + +```typescript +export interface AISignpostProps +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [icon?](./chat-ui-react.aisignpostprops.icon.md) | | React.JSX.Element | _(Optional)_ Icon displayed before the signpost label. Defaults to the SDK's AI signpost icon. | +| [label?](./chat-ui-react.aisignpostprops.label.md) | | string | _(Optional)_ Label displayed in the signpost button. Defaults to "AI-Powered". | +| [popoverBody?](./chat-ui-react.aisignpostprops.popoverbody.md) | | string | _(Optional)_ Body displayed in the signpost popover. | +| [popoverHeader?](./chat-ui-react.aisignpostprops.popoverheader.md) | | string | _(Optional)_ Header displayed in the signpost popover. Defaults to "AI-Generated Content". | + diff --git a/docs/chat-ui-react.aisignpostprops.popoverbody.md b/docs/chat-ui-react.aisignpostprops.popoverbody.md new file mode 100644 index 0000000..3003b1f --- /dev/null +++ b/docs/chat-ui-react.aisignpostprops.popoverbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostProps](./chat-ui-react.aisignpostprops.md) > [popoverBody](./chat-ui-react.aisignpostprops.popoverbody.md) + +## AISignpostProps.popoverBody property + +Body displayed in the signpost popover. + +**Signature:** + +```typescript +popoverBody?: string; +``` diff --git a/docs/chat-ui-react.aisignpostprops.popoverheader.md b/docs/chat-ui-react.aisignpostprops.popoverheader.md new file mode 100644 index 0000000..d903e5e --- /dev/null +++ b/docs/chat-ui-react.aisignpostprops.popoverheader.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [AISignpostProps](./chat-ui-react.aisignpostprops.md) > [popoverHeader](./chat-ui-react.aisignpostprops.popoverheader.md) + +## AISignpostProps.popoverHeader property + +Header displayed in the signpost popover. Defaults to "AI-Generated Content". + +**Signature:** + +```typescript +popoverHeader?: string; +``` diff --git a/docs/chat-ui-react.chatheader.md b/docs/chat-ui-react.chatheader.md index dbdfe63..c58fcaa 100644 --- a/docs/chat-ui-react.chatheader.md +++ b/docs/chat-ui-react.chatheader.md @@ -9,14 +9,14 @@ A component that renders the header of a chat bot panel, including the title and **Signature:** ```typescript -export declare function ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, onClose, customCssClasses, }: ChatHeaderProps): React.JSX.Element; +export declare function ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, hideAISignpost, aiSignpostProps, onClose, customCssClasses, }: ChatHeaderProps): React.JSX.Element; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| { title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, onClose, customCssClasses, } | [ChatHeaderProps](./chat-ui-react.chatheaderprops.md) | | +| { title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, hideAISignpost, aiSignpostProps, onClose, customCssClasses, } | [ChatHeaderProps](./chat-ui-react.chatheaderprops.md) | | **Returns:** diff --git a/docs/chat-ui-react.chatheaderprops.aisignpostprops.md b/docs/chat-ui-react.chatheaderprops.aisignpostprops.md new file mode 100644 index 0000000..3786fce --- /dev/null +++ b/docs/chat-ui-react.chatheaderprops.aisignpostprops.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [ChatHeaderProps](./chat-ui-react.chatheaderprops.md) > [aiSignpostProps](./chat-ui-react.chatheaderprops.aisignpostprops.md) + +## ChatHeaderProps.aiSignpostProps property + +The props to pass to the built-in AI signpost component. + +**Signature:** + +```typescript +aiSignpostProps?: AISignpostProps; +``` diff --git a/docs/chat-ui-react.chatheaderprops.hideaisignpost.md b/docs/chat-ui-react.chatheaderprops.hideaisignpost.md new file mode 100644 index 0000000..790a128 --- /dev/null +++ b/docs/chat-ui-react.chatheaderprops.hideaisignpost.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [@yext/chat-ui-react](./chat-ui-react.md) > [ChatHeaderProps](./chat-ui-react.chatheaderprops.md) > [hideAISignpost](./chat-ui-react.chatheaderprops.hideaisignpost.md) + +## ChatHeaderProps.hideAISignpost property + +Whether to hide the AI signpost. Defaults to false. + +**Signature:** + +```typescript +hideAISignpost?: boolean; +``` diff --git a/docs/chat-ui-react.chatheaderprops.md b/docs/chat-ui-react.chatheaderprops.md index f60250c..4a9868e 100644 --- a/docs/chat-ui-react.chatheaderprops.md +++ b/docs/chat-ui-react.chatheaderprops.md @@ -16,8 +16,10 @@ export interface ChatHeaderProps | Property | Modifiers | Type | Description | | --- | --- | --- | --- | +| [aiSignpostProps?](./chat-ui-react.chatheaderprops.aisignpostprops.md) | | [AISignpostProps](./chat-ui-react.aisignpostprops.md) | _(Optional)_ The props to pass to the built-in AI signpost component. | | [closeButtonIcon?](./chat-ui-react.chatheaderprops.closebuttonicon.md) | | JSX.Element | _(Optional)_ Custom icon for for close button. | | [customCssClasses?](./chat-ui-react.chatheaderprops.customcssclasses.md) | | [ChatHeaderCssClasses](./chat-ui-react.chatheadercssclasses.md) | _(Optional)_ CSS classes for customizing the component styling. | +| [hideAISignpost?](./chat-ui-react.chatheaderprops.hideaisignpost.md) | | boolean | _(Optional)_ Whether to hide the AI signpost. Defaults to false. | | [onClose?](./chat-ui-react.chatheaderprops.onclose.md) | | () => void | _(Optional)_ A function which is called when the close button is clicked. | | [restartButtonIcon?](./chat-ui-react.chatheaderprops.restartbuttonicon.md) | | JSX.Element | _(Optional)_ Custom icon for for restart button. | | [showCloseButton?](./chat-ui-react.chatheaderprops.showclosebutton.md) | | boolean | _(Optional)_ Displays a close button which will invoke [ChatHeaderProps.onClose](./chat-ui-react.chatheaderprops.onclose.md) on click. Default to false. | diff --git a/docs/chat-ui-react.md b/docs/chat-ui-react.md index eb19aca..af8410e 100644 --- a/docs/chat-ui-react.md +++ b/docs/chat-ui-react.md @@ -8,7 +8,8 @@ | Function | Description | | --- | --- | -| [ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, onClose, customCssClasses, })](./chat-ui-react.chatheader.md) | A component that renders the header of a chat bot panel, including the title and a button to reset the conversation. | +| [AISignpostIcon({ className, })](./chat-ui-react.aisignposticon.md) | Default icon for the AI signpost. | +| [ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, hideAISignpost, aiSignpostProps, onClose, customCssClasses, })](./chat-ui-react.chatheader.md) | A component that renders the header of a chat bot panel, including the title and a button to reset the conversation. | | [ChatInput({ placeholder, stream, inputAutoFocus, handleError, sendButtonIcon, customCssClasses, onSend, onRetry, })](./chat-ui-react.chatinput.md) | A component that allows user to input message and send to Chat API. | | [ChatPanel(props)](./chat-ui-react.chatpanel.md) | A component that renders a full panel for chat bot interactions. This includes the message bubbles for the conversation, input box with send button, and header (if provided). | | [ChatPopUp(props)](./chat-ui-react.chatpopup.md) | A component that renders a popup button that displays and hides a panel for chat bot interactions. | @@ -20,6 +21,7 @@ | Interface | Description | | --- | --- | +| [AISignpostProps](./chat-ui-react.aisignpostprops.md) | Props for the built-in AI signpost component. | | [ChatHeaderCssClasses](./chat-ui-react.chatheadercssclasses.md) | The CSS class interface for the [ChatHeader()](./chat-ui-react.chatheader.md) component. | | [ChatHeaderProps](./chat-ui-react.chatheaderprops.md) | The props for the [ChatHeader()](./chat-ui-react.chatheader.md) component. | | [ChatInputCssClasses](./chat-ui-react.chatinputcssclasses.md) | The CSS class interface for the [ChatInput()](./chat-ui-react.chatinput.md) component. | diff --git a/etc/chat-ui-react.api.md b/etc/chat-ui-react.api.md index fe9b106..a5f4861 100644 --- a/etc/chat-ui-react.api.md +++ b/etc/chat-ui-react.api.md @@ -10,7 +10,20 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; // @public -export function ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, onClose, customCssClasses, }: ChatHeaderProps): React_2.JSX.Element; +export function AISignpostIcon({ className, }: { + className?: string; +}): React_2.JSX.Element; + +// @public +export interface AISignpostProps { + icon?: React_2.JSX.Element; + label?: string; + popoverBody?: string; + popoverHeader?: string; +} + +// @public +export function ChatHeader({ title, showRestartButton, restartButtonIcon, showCloseButton, closeButtonIcon, hideAISignpost, aiSignpostProps, onClose, customCssClasses, }: ChatHeaderProps): React_2.JSX.Element; // @public export interface ChatHeaderCssClasses { @@ -30,8 +43,10 @@ export interface ChatHeaderCssClasses { // @public export interface ChatHeaderProps { + aiSignpostProps?: AISignpostProps; closeButtonIcon?: JSX.Element; customCssClasses?: ChatHeaderCssClasses; + hideAISignpost?: boolean; onClose?: () => void; restartButtonIcon?: JSX.Element; showCloseButton?: boolean; diff --git a/package-lock.json b/package-lock.json index 06f7dcd..b36ce49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "rehype-raw": "^5.0.0", "rehype-sanitize": "^4.0.0", "remark-gfm": "^1.0.0", - "tailwind-merge": "^1.12.0" + "tailwind-merge": "^1.12.0", + "use-isomorphic-layout-effect": "^1.1.2" }, "devDependencies": { "@babel/core": "^7.21.8", diff --git a/package.json b/package.json index df9029a..9415d14 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,8 @@ "rehype-raw": "^5.0.0", "rehype-sanitize": "^4.0.0", "remark-gfm": "^1.0.0", - "tailwind-merge": "^1.12.0" + "tailwind-merge": "^1.12.0", + "use-isomorphic-layout-effect": "^1.1.2" }, "overrides": { "handlebars": "^4.7.9", diff --git a/src/components/ChatHeader.tsx b/src/components/ChatHeader.tsx index ec03de0..83913d6 100644 --- a/src/components/ChatHeader.tsx +++ b/src/components/ChatHeader.tsx @@ -5,6 +5,8 @@ import React, { useCallback, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; import { CrossIcon } from "../icons/Cross"; import { withStylelessCssClasses } from "../utils/withStylelessCssClasses"; +import { AISignpostIcon } from "../icons/AISignpostIcon"; +import { useId } from "../hooks/useId"; /** * The CSS class interface for the {@link ChatHeader} component. @@ -32,6 +34,14 @@ const builtInCssClasses: Readonly = closeButtonIcon: "text-white w-[26px] h-[26px]", }); +const aiSignpostDefaults = { + label: "AI-Powered", + popoverHeader: "AI-Generated Content", + popoverBody: + "This content is generated by AI from language models and previous responses. It may be" + + " incomplete or inaccurate. Please check statements for accuracy.", +}; + /** * The props for the {@link ChatHeader} component. * @@ -58,10 +68,30 @@ export interface ChatHeaderProps { restartButtonIcon?: JSX.Element; /** Custom icon for for close button. */ closeButtonIcon?: JSX.Element; + /** Whether to hide the AI signpost. Defaults to false. */ + hideAISignpost?: boolean; + /** The props to pass to the built-in AI signpost component. */ + aiSignpostProps?: AISignpostProps; /** CSS classes for customizing the component styling. */ customCssClasses?: ChatHeaderCssClasses; } +/** + * 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-Powered". */ + label?: string; + /** Header displayed in the signpost popover. Defaults to "AI-Generated Content". */ + popoverHeader?: string; + /** Body displayed in the signpost popover. */ + popoverBody?: string; +} + /** * A component that renders the header of a chat bot panel, * including the title and a button to reset the conversation. @@ -76,6 +106,8 @@ export function ChatHeader({ restartButtonIcon, showCloseButton, closeButtonIcon, + hideAISignpost = false, + aiSignpostProps, onClose, customCssClasses, }: ChatHeaderProps) { @@ -101,7 +133,10 @@ export function ChatHeader({ return (
-

{title}

+
+

{title}

+ {!hideAISignpost && } +
{showRestartButton && ( + {isOpen && ( + + )} +
+ ); +} diff --git a/src/components/ChatPopUp.tsx b/src/components/ChatPopUp.tsx index 3693e77..fd07ae3 100644 --- a/src/components/ChatPopUp.tsx +++ b/src/components/ChatPopUp.tsx @@ -141,6 +141,8 @@ export function ChatPopUp(props: ChatPopUpProps) { title, footer, isOpen, + hideAISignpost, + aiSignpostProps, } = props; const reportAnalyticsEvent = useReportAnalyticsEvent(); @@ -230,6 +232,8 @@ export function ChatPopUp(props: ChatPopUpProps) { title={title} showRestartButton={showRestartButton} showCloseButton={true} + hideAISignpost={hideAISignpost} + aiSignpostProps={aiSignpostProps} onClose={onClose} customCssClasses={cssClasses.headerCssClasses} /> diff --git a/src/components/index.ts b/src/components/index.ts index 723fc36..d217dee 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,7 +2,12 @@ export { ChatInput } from "./ChatInput"; export type { ChatInputCssClasses, ChatInputProps } from "./ChatInput"; export { ChatHeader } from "./ChatHeader"; -export type { ChatHeaderCssClasses, ChatHeaderProps } from "./ChatHeader"; +export { AISignpostIcon } from "../icons/AISignpostIcon"; +export type { + AISignpostProps, + ChatHeaderCssClasses, + ChatHeaderProps, +} from "./ChatHeader"; export { MessageBubble } from "./MessageBubble"; export type { diff --git a/src/hooks/useId.tsx b/src/hooks/useId.tsx new file mode 100644 index 0000000..bd3b842 --- /dev/null +++ b/src/hooks/useId.tsx @@ -0,0 +1,63 @@ +// Copied with minor modifications from +// https://github.com/reach/reach-ui/blob/dev/packages/auto-id/src/reach-auto-id.ts + +import React, { useEffect, useState } from 'react'; +import useIsomorphicLayoutEffect from 'use-isomorphic-layout-effect'; + +const useLayoutEffect = typeof useIsomorphicLayoutEffect === 'function' + ? useIsomorphicLayoutEffect + : useIsomorphicLayoutEffect['default']; + +let serverHandoffComplete = false; +let id = 0; +function genId(baseName: string): string { + ++id; + return baseName + '-' + id.toString(); +} + +// Workaround for https://github.com/webpack/webpack/issues/14814 +// https://github.com/eps1lon/material-ui/blob/8d5f135b4d7a58253a99ab56dce4ac8de61f5dc1/packages/mui-utils/src/useId.ts#L21 +const maybeReactUseId: undefined | (() => string) = (React as any)[ + 'useId'.toString() + ]; + +/** + * useId + * + * Autogenerate IDs to facilitate WAI-ARIA and server rendering. + * + * Note: The returned ID will initially be empty string and will update after a + * component mounts. + * + * @see Docs https://reach.tech/auto-id + */ + +export function useId(baseName: string): string { + const reactId = maybeReactUseId?.(); + + // If this instance isn't part of the initial render, we don't have to do the + // double render/patch-up dance. We can just generate the ID and return it. + const initialId = (serverHandoffComplete ? genId(baseName) : ''); + const [id, setId] = useState(initialId); + + useLayoutEffect(() => { + if (id === '') { + // Patch the ID after render. We do this in `useLayoutEffect` to avoid any + // rendering flicker, though it'll make the first render slower (unlikely + // to matter, but you're welcome to measure your app and let us know if + // it's a problem). + setId(genId(baseName)); + } + }, [baseName, id]); + + useEffect(() => { + if (serverHandoffComplete === false) { + // Flag all future uses of `useId` to skip the update dance. This is in + // `useEffect` because it goes after `useLayoutEffect`, ensuring we don't + // accidentally bail out of the patch-up dance prematurely. + serverHandoffComplete = true; + } + }, []); + + return reactId ?? id; +} diff --git a/src/icons/AISignpostIcon.tsx b/src/icons/AISignpostIcon.tsx new file mode 100644 index 0000000..9f807ee --- /dev/null +++ b/src/icons/AISignpostIcon.tsx @@ -0,0 +1,27 @@ +import React from "react"; + +/** + * Default icon for the AI signpost. + * + * @public + */ +export function AISignpostIcon({ + className, +}: { + className?: string; +}): React.JSX.Element { + return ( + + ); +} diff --git a/tests/components/ChatHeader.stories.tsx b/tests/components/ChatHeader.stories.tsx index ea73385..e2e38bb 100644 --- a/tests/components/ChatHeader.stories.tsx +++ b/tests/components/ChatHeader.stories.tsx @@ -31,3 +31,26 @@ export const HeaderWithButtons: StoryObj = { customCssClasses: {}, }, }; + +export const HeaderWithCustomSignpost: StoryObj = { + ...Primary, + args: { + title: "Chat Header", + aiSignpostProps: { + label: "AI-Assisted", + popoverHeader: "AI-Assisted Content", + popoverBody: + "This response may contain AI-assisted content. Review important details before relying on it.", + }, + customCssClasses: {}, + }, +}; + +export const HeaderWithoutSignpost: StoryObj = { + ...Primary, + args: { + title: "Chat Header", + hideAISignpost: true, + customCssClasses: {}, + }, +}; diff --git a/tests/components/ChatHeader.test.tsx b/tests/components/ChatHeader.test.tsx index 0540181..6da1aef 100644 --- a/tests/components/ChatHeader.test.tsx +++ b/tests/components/ChatHeader.test.tsx @@ -49,3 +49,55 @@ it("does not display close button by default", () => { render(); expect(screen.queryByLabelText("Close Chat")).toBeNull(); }); + +it("displays the default AI signpost popover", async () => { + render(); + + const signpost = screen.getByRole("button", { name: "AI-Powered" }); + expect(screen.queryByRole("dialog")).toBeNull(); + + await act(() => userEvent.click(signpost)); + + expect( + screen.getByRole("dialog", { name: "AI-Generated Content" }) + ).toBeTruthy(); + expect( + screen.getByText( + "This content is generated by AI from language models and previous responses. It may be incomplete or inaccurate. Please check statements for accuracy." + ) + ).toBeTruthy(); + + await act(() => + userEvent.click(screen.getByRole("button", { name: "Dismiss" })) + ); + expect(screen.queryByRole("dialog")).toBeNull(); +}); + +it("supports hiding and customizing the AI signpost", async () => { + const { rerender } = render( + + ); + expect(screen.queryByRole("button", { name: "AI-Powered" })).toBeNull(); + + rerender( + Custom Icon, + label: "Custom Label", + popoverHeader: "Custom Header", + popoverBody: "Custom Body", + }} + /> + ); + + expect(screen.getByTestId("custom-ai-signpost-icon")).toBeTruthy(); + await act(() => + userEvent.click( + screen.getByRole("button", { name: "Custom Icon Custom Label" }) + ) + ); + + expect(screen.getByRole("dialog", { name: "Custom Header" })).toBeTruthy(); + expect(screen.getByText("Custom Body")).toBeTruthy(); +}); From 52a850b0b1bcc9b0656837ee69a64d72fe33e9f4 Mon Sep 17 00:00:00 2001 From: Maxfield Kouzel Date: Thu, 18 Jun 2026 12:37:02 -0400 Subject: [PATCH 2/2] linting fix --- src/hooks/useId.tsx | 2 +- test-site/package-lock.json | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/hooks/useId.tsx b/src/hooks/useId.tsx index bd3b842..251123a 100644 --- a/src/hooks/useId.tsx +++ b/src/hooks/useId.tsx @@ -17,7 +17,7 @@ function genId(baseName: string): string { // Workaround for https://github.com/webpack/webpack/issues/14814 // https://github.com/eps1lon/material-ui/blob/8d5f135b4d7a58253a99ab56dce4ac8de61f5dc1/packages/mui-utils/src/useId.ts#L21 -const maybeReactUseId: undefined | (() => string) = (React as any)[ +const maybeReactUseId: undefined | (() => string) = (React as never)[ 'useId'.toString() ]; diff --git a/test-site/package-lock.json b/test-site/package-lock.json index 160d8d0..27ee56c 100644 --- a/test-site/package-lock.json +++ b/test-site/package-lock.json @@ -23,7 +23,7 @@ }, "..": { "name": "@yext/chat-ui-react", - "version": "0.12.5", + "version": "0.12.6", "license": "BSD-3-Clause", "dependencies": { "react-markdown": "^6.0.3", @@ -31,7 +31,8 @@ "rehype-raw": "^5.0.0", "rehype-sanitize": "^4.0.0", "remark-gfm": "^1.0.0", - "tailwind-merge": "^1.12.0" + "tailwind-merge": "^1.12.0", + "use-isomorphic-layout-effect": "^1.1.2" }, "devDependencies": { "@babel/core": "^7.21.8",