Skip to content

Commit

Permalink
chat-headless: reset EventClient if active when restarting convo (#53)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
popestr authored Jul 11, 2024
1 parent eee6257 commit 80bf7f2
Show file tree
Hide file tree
Showing 15 changed files with 107 additions and 9 deletions.
5 changes: 5 additions & 0 deletions apps/test-site/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ function ChatComponent() {
});
}, [actions]);

const onRestart = useCallback(() => {
actions.restartConversation();
}, [actions]);

return (
<div>
{messages.map((m, i) => (
Expand All @@ -98,6 +102,7 @@ function ChatComponent() {
<button onClick={onClick}>Send</button>
<button onClick={onClickStream}>Send (Stream)</button>
<button onClick={onReport}>report</button>
<button onClick={onRestart}>Restart</button>
</div>
);
}
Expand Down
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/chat-headless-react/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file

The following npm package may be included in this product:

- @babel/[email protected].7
- @babel/[email protected].8

This package contains the following license and notice below:

Expand Down
2 changes: 1 addition & 1 deletion packages/chat-headless/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ https://www.npmjs.com/package/generate-license-file

The following npm package may be included in this product:

- @babel/[email protected].7
- @babel/[email protected].8

This package contains the following license and notice below:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: - <code>message</code>: A new message has been received. - <code>typing</code>: The agent is typing. - <code>close</code>: 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. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [@yext/chat-headless](./chat-headless.md) &gt; [ChatEventClient](./chat-headless.chateventclient.md) &gt; [resetSession](./chat-headless.chateventclient.resetsession.md)

## ChatEventClient.resetSession() method

Reset the current chat session.

**Signature:**

```typescript
resetSession(): void;
```
**Returns:**

void

2 changes: 1 addition & 1 deletion packages/chat-headless/docs/chat-headless.chatheadless.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | <p>Resets all fields within the [ConversationState](./chat-headless.conversationstate.md)<!-- -->, and sets the active client to the <code>bot</code> client, if one was provided when constructing the [ChatHeadless](./chat-headless.chatheadless.md) instance.</p><p>If a [ChatEventClient](./chat-headless.chateventclient.md) is currently active before reset, that client's <code>resetSession</code> method is called.</p> |
| [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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down
1 change: 1 addition & 0 deletions packages/chat-headless/etc/chat-headless.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface ChatEventClient {
init(messageResponse: MessageResponse): Promise<void>;
on(eventName: "message" | "typing" | "close", cb: (data: any) => void): void;
processMessage(request: MessageRequest): Promise<void>;
resetSession(): void;
}

// @public
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-headless/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 7 additions & 0 deletions packages/chat-headless/src/ChatHeadlessImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion packages/chat-headless/src/models/ChatHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/chat-headless/src/models/clients/ChatEventClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export interface ChatEventClient {
* Provide the current chat session.
*/
getSession(): any;

/**
* Reset the current chat session.
*/
resetSession(): void;
}

export function isChatEventClient(
Expand Down
40 changes: 39 additions & 1 deletion packages/chat-headless/tests/chatheadless.clients.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any[]> = {};
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,
Expand All @@ -149,21 +180,28 @@ function createMockHttpClient(
}

function createMockEventClient(
callbacks: Record<string, any[]>
callbacks?: Record<string, any[]>
): 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;
}
9 changes: 8 additions & 1 deletion packages/chat-headless/tests/chatheadless.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ChatHttpClient,
ConversationState,
HeadlessConfig,
Message,
Expand Down Expand Up @@ -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();
});

Expand Down

0 comments on commit 80bf7f2

Please sign in to comment.