Skip to content

Commit

Permalink
feat: Add reset conversation functionality (#30)
Browse files Browse the repository at this point in the history
* feat: Add reset conversation functionality

* Address comments

* Update usage of resetConversation in code samples.
  • Loading branch information
mrderyk authored Mar 15, 2024
1 parent 83f548d commit a0e3a19
Show file tree
Hide file tree
Showing 8 changed files with 85 additions and 11 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ import { useChat } from "@vectara/react-chatbot/lib";

/* snip */

const { sendMessage, messageHistory, isLoading, hasError } = useChat(
const { sendMessage, startNewConversation, messageHistory, isLoading, hasError } = useChat(
"CUSTOMER_ID",
["CORPUS_ID_1", "CORPUS_ID_2", "CORPUS_ID_N"],
"API_KEY"
Expand All @@ -139,6 +139,10 @@ The values returned by the hook can be passed on to your custom components as pr

This is used to send a message to the chat API. Send `true` for the optional `isRetry` flag to if this is retrying a previously failed message. This allows the internal logic to correctly link the next successful answer to the failed query.

##### startNewConversation: `() => void;`

This is used to reset the conversational context of the chat. The message history will be cleared and the chatbot will "forget" everything that's been discussed.

##### messageHistory: `ChatTurn[]`

This is an array of objects representing question and answer pairs from the entire conversation. Each pair is a `ChatTurn` object. More information on types can be found [here](src/types.ts).
Expand Down
4 changes: 2 additions & 2 deletions docs/package-lock.json

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

3 changes: 2 additions & 1 deletion docs/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ const App = () => {
import { useChat } from "@vectara/react-chatbot/lib";
export const App = () => {
const { sendMessage, messageHistory, isLoading, hasError } = useChat(
const { sendMessage, startNewConversation, messageHistory, isLoading, hasError } = useChat(
DEFAULT_CUSTOMER_ID,
DEFAULT_CORPUS_IDS,
DEFAULT_API_KEY
Expand All @@ -278,6 +278,7 @@ export const App = () => {
<p>The hook returns:</p>
<ul>
<li>sendMessage - a function that sends a string to the Chat API endpoint</li>
<li>startNewConversation - a function that resets the conversational context</li>
<li>messageHistory - an array of objects representing messages from the entire conversation</li>
<li>isLoading - a boolean value indicating whether or not a chat message request is pending</li>
<li>
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

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

20 changes: 16 additions & 4 deletions src/components/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Fragment, ReactNode, useCallback, useEffect, useRef, useState } from "react";
import { VuiFlexContainer, VuiFlexItem, VuiSpacer } from "../vui";
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
import { VuiButtonSecondary, VuiFlexContainer, VuiFlexItem, VuiSpacer } from "../vui";
import { QueryInput } from "./QueryInput";
import { ChatItem } from "./ChatItem";
import { useChat } from "../useChat";
Expand Down Expand Up @@ -54,7 +54,11 @@ export const ChatView = ({
}: Props) => {
const [isOpen, setIsOpen] = useState<boolean>(isInitiallyOpen ?? false);
const [query, setQuery] = useState<string>("");
const { sendMessage, messageHistory, isLoading, hasError } = useChat(customerId, corpusIds, apiKey);
const { sendMessage, startNewConversation, messageHistory, isLoading, hasError } = useChat(
customerId,
corpusIds,
apiKey
);
const appLayoutRef = useRef<HTMLDivElement>(null);
const isScrolledToBottomRef = useRef(true);

Expand Down Expand Up @@ -147,7 +151,7 @@ export const ChatView = ({
if (messageHistory[index]?.answer === "") {
spacer = null;
} else {
spacer = index < chatItems.length - 1 ? <VuiSpacer size="m" /> : <VuiSpacer size="xl" />;
spacer = index < chatItems.length - 1 ? <VuiSpacer size="m" /> : <VuiSpacer size="l" />;
}
return (
<Fragment key={index}>
Expand All @@ -156,6 +160,14 @@ export const ChatView = ({
</Fragment>
);
})}
<VuiFlexContainer fullWidth={true} justifyContent="center">
<VuiFlexItem>
<VuiButtonSecondary color="neutral" size="xs" onClick={startNewConversation} isDisabled={isLoading}>
Start new conversation
</VuiButtonSecondary>
</VuiFlexItem>
</VuiFlexContainer>
<VuiSpacer size="l" />
</>
)}
</div>
Expand Down
1 change: 1 addition & 0 deletions src/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe("ReactChatbot", () => {

expect(screen.getByShadowText("mock-question")).toBeInTheDocument();
expect(screen.getByShadowText("mock-answer")).toBeInTheDocument();
expect(screen.getByShadowText("Start new conversation")).toBeInTheDocument();
});

it("should execute callback when sending message", () => {
Expand Down
50 changes: 50 additions & 0 deletions src/useChat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,54 @@ describe("useChat", () => {

expect(result.current.isLoading).toEqual(true);
});

it("should be able to reset the conversation", async () => {
const { result } = renderHook(() => useChat("mock-customer-id", ["1"], "mock-api-key"));
(sendSearchRequest as jest.Mock).mockImplementation(() => Promise.resolve(MOCK_API_RESPONSE));

await act(async () => {
await result.current.sendMessage({ query: "mock-query" });
await result.current.sendMessage({ query: "mock-query-2" });
});

// Assert that the second request uses the current conversation id.
expect(sendSearchRequest).toHaveBeenCalledWith(
expect.objectContaining({
chat: {
conversationId: "mock-conversation-id"
}
})
);

(sendSearchRequest as jest.Mock).mockImplementation(() =>
Promise.resolve({
...MOCK_API_RESPONSE,
summary: [
{
chat: {
conversationId: "mock-conversation-id-2",
turnId: "mock-turn-id",
text: "mock-answer"
}
}
]
})
);

await act(async () => {
await result.current.startNewConversation();
});

expect(result.current.messageHistory.length).toEqual(0);

await act(async () => {
await result.current.sendMessage({ query: "mock-query-3" });
});

const calls = (sendSearchRequest as jest.Mock).mock.calls;
const recentSendSearchRequestCall = calls[calls.length - 1][0];

// Assert that the request after reset is has no conversation id.
expect(recentSendSearchRequestCall.chat.conversationId).toEqual(undefined);
});
});
6 changes: 6 additions & 0 deletions src/useChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ export const useChat = (customerId: string, corpusIds: string[], apiKey: string)
}
};

const startNewConversation = () => {
setMessageHistory([]);
setConversationId(undefined);
};

useEffect(() => {
if (!recentAnswer) return;

Expand All @@ -107,6 +112,7 @@ export const useChat = (customerId: string, corpusIds: string[], apiKey: string)

return {
sendMessage,
startNewConversation,
messageHistory,
isLoading,
hasError
Expand Down

0 comments on commit a0e3a19

Please sign in to comment.