From 80bf7f2d644a9cde425fd6f452d8488137cc0369 Mon Sep 17 00:00:00 2001 From: Ryan Pope Date: Thu, 11 Jul 2024 11:01:49 -0400 Subject: [PATCH] chat-headless: reset EventClient if active when restarting convo (#53) If a ChatEventClient is the active client when restarting the conversation, call its resetSession method and move to the next available client for continuation of the conversation. TEST=manual Ran with locally updated chat-core, saw expected results in test app. Added and updated unit tests to reflect new behavior. --- apps/test-site/src/App.tsx | 5 +++ package-lock.json | 12 +++++- .../chat-headless-react/THIRD-PARTY-NOTICES | 2 +- packages/chat-headless/THIRD-PARTY-NOTICES | 2 +- .../docs/chat-headless.chateventclient.md | 1 + ...t-headless.chateventclient.resetsession.md | 17 ++++++++ .../docs/chat-headless.chatheadless.md | 2 +- ...adless.chatheadless.restartconversation.md | 4 +- .../chat-headless/etc/chat-headless.api.md | 1 + packages/chat-headless/package.json | 2 +- .../chat-headless/src/ChatHeadlessImpl.ts | 7 ++++ .../chat-headless/src/models/ChatHeadless.ts | 7 +++- .../src/models/clients/ChatEventClient.ts | 5 +++ .../tests/chatheadless.clients.test.ts | 40 ++++++++++++++++++- .../chat-headless/tests/chatheadless.test.ts | 9 ++++- 15 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 packages/chat-headless/docs/chat-headless.chateventclient.resetsession.md diff --git a/apps/test-site/src/App.tsx b/apps/test-site/src/App.tsx index d78ab7f..856f3de 100644 --- a/apps/test-site/src/App.tsx +++ b/apps/test-site/src/App.tsx @@ -83,6 +83,10 @@ function ChatComponent() { }); }, [actions]); + const onRestart = useCallback(() => { + actions.restartConversation(); + }, [actions]); + return (
{messages.map((m, i) => ( @@ -98,6 +102,7 @@ function ChatComponent() { +
); } diff --git a/package-lock.json b/package-lock.json index 58478ee..66145fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18722,7 +18722,7 @@ }, "packages/chat-headless": { "name": "@yext/chat-headless", - "version": "0.10.0", + "version": "0.10.1", "license": "BSD-3-Clause", "dependencies": { "@reduxjs/toolkit": "^1.9.5", @@ -19047,6 +19047,16 @@ "@types/yargs-parser": "*" } }, + "packages/chat-headless-react/node_modules/@yext/chat-headless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@yext/chat-headless/-/chat-headless-0.10.0.tgz", + "integrity": "sha512-rYabLNfU8KcMDw/HqAkWbFuu2AavSjEtU5kYYAVdTRcS3LyrsNXe/l7lp11R3RCmJCcJnn5TXqhcTKzfI8I9zA==", + "dependencies": { + "@reduxjs/toolkit": "^1.9.5", + "@yext/analytics": "^0.6.3", + "@yext/chat-core": "^0.8.2" + } + }, "packages/chat-headless-react/node_modules/ansi-styles": { "version": "4.3.0", "dev": true, diff --git a/packages/chat-headless-react/THIRD-PARTY-NOTICES b/packages/chat-headless-react/THIRD-PARTY-NOTICES index 3c61571..02de88c 100644 --- a/packages/chat-headless-react/THIRD-PARTY-NOTICES +++ b/packages/chat-headless-react/THIRD-PARTY-NOTICES @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file The following npm package may be included in this product: - - @babel/runtime@7.24.7 + - @babel/runtime@7.24.8 This package contains the following license and notice below: diff --git a/packages/chat-headless/THIRD-PARTY-NOTICES b/packages/chat-headless/THIRD-PARTY-NOTICES index 71fd972..bf44b66 100644 --- a/packages/chat-headless/THIRD-PARTY-NOTICES +++ b/packages/chat-headless/THIRD-PARTY-NOTICES @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file The following npm package may be included in this product: - - @babel/runtime@7.24.7 + - @babel/runtime@7.24.8 This package contains the following license and notice below: diff --git a/packages/chat-headless/docs/chat-headless.chateventclient.md b/packages/chat-headless/docs/chat-headless.chateventclient.md index f5b061c..f6457d1 100644 --- a/packages/chat-headless/docs/chat-headless.chateventclient.md +++ b/packages/chat-headless/docs/chat-headless.chateventclient.md @@ -21,4 +21,5 @@ export interface ChatEventClient | [init(messageResponse)](./chat-headless.chateventclient.init.md) | Initializes the client, using credentials and data in the provided message to setup a chat session. | | [on(eventName, cb)](./chat-headless.chateventclient.on.md) | Registers an event listener for a specified event. Supported events are: - message: A new message has been received. - typing: The agent is typing. - close: The chat session has been closed. | | [processMessage(request)](./chat-headless.chateventclient.processmessage.md) | Processes a message request. The response should be emitted as a message event. | +| [resetSession()](./chat-headless.chateventclient.resetsession.md) | Reset the current chat session. | diff --git a/packages/chat-headless/docs/chat-headless.chateventclient.resetsession.md b/packages/chat-headless/docs/chat-headless.chateventclient.resetsession.md new file mode 100644 index 0000000..cd783a3 --- /dev/null +++ b/packages/chat-headless/docs/chat-headless.chateventclient.resetsession.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [@yext/chat-headless](./chat-headless.md) > [ChatEventClient](./chat-headless.chateventclient.md) > [resetSession](./chat-headless.chateventclient.resetsession.md) + +## ChatEventClient.resetSession() method + +Reset the current chat session. + +**Signature:** + +```typescript +resetSession(): void; +``` +**Returns:** + +void + diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.md b/packages/chat-headless/docs/chat-headless.chatheadless.md index 75f8748..94db4c2 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.md @@ -21,7 +21,7 @@ export interface ChatHeadless | [getNextMessage(text, source)](./chat-headless.chatheadless.getnextmessage.md) | Performs a Chat API request for the next message generated by chat bot using the conversation state (e.g. message history and notes). Update the state with the response data. | | [initLocalStorage()](./chat-headless.chatheadless.initlocalstorage.md) | Loads the [ConversationState](./chat-headless.conversationstate.md) from local storage, if present, and adds a listener to keep the conversation state in sync with the stored state | | [report(eventPayload)](./chat-headless.chatheadless.report.md) | Send Chat related analytics event to Yext Analytics API. | -| [restartConversation()](./chat-headless.chatheadless.restartconversation.md) | Resets all fields within [ConversationState](./chat-headless.conversationstate.md) | +| [restartConversation()](./chat-headless.chatheadless.restartconversation.md) |

Resets all fields within the [ConversationState](./chat-headless.conversationstate.md), and sets the active client to the bot client, if one was provided when constructing the [ChatHeadless](./chat-headless.chatheadless.md) instance.

If a [ChatEventClient](./chat-headless.chateventclient.md) is currently active before reset, that client's resetSession method is called.

| | [setCanSendMessage(canSendMessage)](./chat-headless.chatheadless.setcansendmessage.md) | Sets [ConversationState.canSendMessage](./chat-headless.conversationstate.cansendmessage.md) to the specified state | | [setChatLoadingStatus(isLoading)](./chat-headless.chatheadless.setchatloadingstatus.md) | Sets [ConversationState.isLoading](./chat-headless.conversationstate.isloading.md) to the specified loading state | | [setContext(context)](./chat-headless.chatheadless.setcontext.md) | Sets [MetaState.context](./chat-headless.metastate.context.md) to the specified context. | diff --git a/packages/chat-headless/docs/chat-headless.chatheadless.restartconversation.md b/packages/chat-headless/docs/chat-headless.chatheadless.restartconversation.md index 0caa99b..42edde7 100644 --- a/packages/chat-headless/docs/chat-headless.chatheadless.restartconversation.md +++ b/packages/chat-headless/docs/chat-headless.chatheadless.restartconversation.md @@ -4,7 +4,9 @@ ## ChatHeadless.restartConversation() method -Resets all fields within [ConversationState](./chat-headless.conversationstate.md) +Resets all fields within the [ConversationState](./chat-headless.conversationstate.md), and sets the active client to the `bot` client, if one was provided when constructing the [ChatHeadless](./chat-headless.chatheadless.md) instance. + +If a [ChatEventClient](./chat-headless.chateventclient.md) is currently active before reset, that client's `resetSession` method is called. **Signature:** diff --git a/packages/chat-headless/etc/chat-headless.api.md b/packages/chat-headless/etc/chat-headless.api.md index d1b5195..bc24c62 100644 --- a/packages/chat-headless/etc/chat-headless.api.md +++ b/packages/chat-headless/etc/chat-headless.api.md @@ -44,6 +44,7 @@ export interface ChatEventClient { init(messageResponse: MessageResponse): Promise; on(eventName: "message" | "typing" | "close", cb: (data: any) => void): void; processMessage(request: MessageRequest): Promise; + resetSession(): void; } // @public diff --git a/packages/chat-headless/package.json b/packages/chat-headless/package.json index 170d381..4453e12 100644 --- a/packages/chat-headless/package.json +++ b/packages/chat-headless/package.json @@ -1,6 +1,6 @@ { "name": "@yext/chat-headless", - "version": "0.10.0", + "version": "0.10.1", "description": "A state manager library powered by Redux for Yext Chat integrations", "main": "./dist/commonjs/src/index.js", "module": "./dist/esm/src/index.mjs", diff --git a/packages/chat-headless/src/ChatHeadlessImpl.ts b/packages/chat-headless/src/ChatHeadlessImpl.ts index 4d9c94c..e5aaa5a 100644 --- a/packages/chat-headless/src/ChatHeadlessImpl.ts +++ b/packages/chat-headless/src/ChatHeadlessImpl.ts @@ -44,6 +44,7 @@ import { isChatEventClient } from "./models/clients/ChatEventClient"; export class ChatHeadlessImpl implements ChatHeadless { private config: HeadlessConfig; private chatClient: ChatClient; + private botClient: ChatClient; private clients: ChatClient[]; private stateManager: ReduxStateManager; private chatAnalyticsService: ChatAnalyticsService; @@ -71,6 +72,7 @@ export class ChatHeadlessImpl implements ChatHeadless { // bot client is the default client. // If agent client is provided, it will be used as the second client on handoff this.chatClient = botClient ?? provideChatCore(this.config); + this.botClient = this.chatClient; this.clients = [this.chatClient]; if (agentClient) { this.clients.push(agentClient); @@ -264,6 +266,11 @@ export class ChatHeadlessImpl implements ChatHeadless { } restartConversation() { + if (isChatEventClient(this.chatClient)) { + this.chatClient.resetSession(); + } + this.chatClient = this.botClient; + this.setConversationId(undefined); this.setChatLoadingStatus(false); this.setCanSendMessage(true); diff --git a/packages/chat-headless/src/models/ChatHeadless.ts b/packages/chat-headless/src/models/ChatHeadless.ts index 443c691..7d1ce6f 100644 --- a/packages/chat-headless/src/models/ChatHeadless.ts +++ b/packages/chat-headless/src/models/ChatHeadless.ts @@ -110,7 +110,12 @@ export interface ChatHeadless { */ initLocalStorage(): void; /** - * Resets all fields within {@link ConversationState} + * Resets all fields within the {@link ConversationState}, and sets the active + * client to the `bot` client, if one was provided when constructing the + * {@link ChatHeadless} instance. + * + * If a {@link ChatEventClient} is currently active before reset, that client's + * `resetSession` method is called. * * @public */ diff --git a/packages/chat-headless/src/models/clients/ChatEventClient.ts b/packages/chat-headless/src/models/clients/ChatEventClient.ts index b1f74f1..b06ad3b 100644 --- a/packages/chat-headless/src/models/clients/ChatEventClient.ts +++ b/packages/chat-headless/src/models/clients/ChatEventClient.ts @@ -48,6 +48,11 @@ export interface ChatEventClient { * Provide the current chat session. */ getSession(): any; + + /** + * Reset the current chat session. + */ + resetSession(): void; } export function isChatEventClient( diff --git a/packages/chat-headless/tests/chatheadless.clients.test.ts b/packages/chat-headless/tests/chatheadless.clients.test.ts index 8eed381..6d8ee43 100644 --- a/packages/chat-headless/tests/chatheadless.clients.test.ts +++ b/packages/chat-headless/tests/chatheadless.clients.test.ts @@ -125,6 +125,37 @@ it("update state on events from event client", async () => { expect(headless.state.conversation.isLoading).toBeFalsy(); }); +it("resets session and uses bot client on reset", async () => { + const botClient = createMockHttpClient([ + { message: createMessage("message 1"), notes: {}, integrationDetails: {} }, //trigger handoff + { message: createMessage("message 2"), notes: {} }, + ]); + const callbacks: Record = {}; + const agentClient = createMockEventClient(callbacks); + const headless = provideChatHeadless(config, { + bot: botClient, + agent: agentClient, + }); + + // start with bot client, immediately trigger handoff + await headless.getNextMessage(); + expect(botClient.getNextMessage).toHaveBeenCalledTimes(1); + expect(agentClient.init).toHaveBeenCalledTimes(1); + + // with agent client, get next message + await headless.getNextMessage(); + expect(agentClient.processMessage).toHaveBeenCalledTimes(1); + + // reset session, switching back to bot client + headless.restartConversation(); + expect(agentClient.resetSession).toHaveBeenCalledTimes(1); + expect(agentClient.getSession()).toBeUndefined(); + + // with bot client, get next message + await headless.getNextMessage(); + expect(botClient.getNextMessage).toHaveBeenCalledTimes(2); +}); + function createMessage(text: string): Message { return { text, @@ -149,21 +180,28 @@ function createMockHttpClient( } function createMockEventClient( - callbacks: Record + callbacks?: Record ): ChatEventClient { const client: ChatEventClient = { init: jest.fn(), on: (event, cb) => { + if (!callbacks) { + return; + } if (!callbacks[event]) { callbacks[event] = []; } callbacks[event].push(cb); }, processMessage: jest.fn(async () => { + if (!callbacks) { + return; + } callbacks["message"]?.forEach((cb) => cb("bot message")); }), emit: jest.fn(), getSession: jest.fn(), + resetSession: jest.fn(), }; return client; } diff --git a/packages/chat-headless/tests/chatheadless.test.ts b/packages/chat-headless/tests/chatheadless.test.ts index bdc26aa..0eae889 100644 --- a/packages/chat-headless/tests/chatheadless.test.ts +++ b/packages/chat-headless/tests/chatheadless.test.ts @@ -1,4 +1,5 @@ import { + ChatHttpClient, ConversationState, HeadlessConfig, Message, @@ -28,7 +29,13 @@ const mockedMetaState: MetaState = { }; beforeEach(() => { - jest.spyOn(coreLib, "provideChatCore").mockImplementation(); + jest.spyOn(coreLib, "provideChatCore").mockImplementation(() => { + const client: ChatHttpClient = { + getNextMessage: jest.fn(), + streamNextMessage: jest.fn(), + }; + return client; + }); localStorage.clear(); });