Skip to content

Commit

Permalink
Handle conversations.
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewjordan committed Jan 28, 2025
1 parent 5171309 commit 6abbce2
Show file tree
Hide file tree
Showing 16 changed files with 512 additions and 285 deletions.
13 changes: 0 additions & 13 deletions components/Chat/Chat.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,6 @@ describe("Chat component", () => {
const dataProps = el.getAttribute("data-props");
const dataPropsObj = JSON.parse(dataProps!);
expect(dataPropsObj.question).toEqual("tell me about boats");
expect(dataPropsObj.isStreamingComplete).toEqual(false);
expect(dataPropsObj.message).toEqual({
answer: "fake-answer-1",
end: "stop",
});
expect(typeof dataPropsObj.conversationRef).toBe("string");
expect(uuidRegex.test(dataPropsObj.conversationRef)).toBe(true);
});
Expand All @@ -106,14 +101,6 @@ describe("Chat component", () => {
<Chat />
</SearchProvider>,
);

expect(mockMessage).toHaveBeenCalledWith(
expect.objectContaining({
auth: "fake-token",
message: "chat",
question: "boats",
}),
);
});

it("doesn't send a websocket message if the search term is empty", () => {
Expand Down
199 changes: 64 additions & 135 deletions components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,93 +1,54 @@
import { AI_DISCLAIMER, AI_SEARCH_UNSUBMITTED } from "@/lib/constants/common";
import React, { useEffect, useState } from "react";
import {
StyledResponseActions,
StyledResponseDisclaimer,
StyledUnsubmitted,
} from "@/components/Chat/Response/Response.styled";
import { defaultState, useSearchState } from "@/context/search-context";

import Announcement from "@/components/Shared/Announcement";
import { Button } from "@nulib/design-system";
import ChatFeedback from "@/components/Chat/Feedback/Feedback";
import { AI_SEARCH_UNSUBMITTED } from "@/lib/constants/common";
import ChatConversation from "./Conversation";
import ChatResponse from "@/components/Chat/Response/Response";
import Container from "@/components/Shared/Container";
import { prepareQuestion } from "@/lib/chat-helpers";
import useChatSocket from "@/hooks/useChatSocket";
import { StyledUnsubmitted } from "./Response/Response.styled";
import { styled } from "@/stitches.config";
import useQueryParams from "@/hooks/useQueryParams";
import { v4 as uuidv4 } from "uuid";

const Chat = ({
viewResultsCallback,
}: {
viewResultsCallback?: () => void;
}) => {
const { searchTerm = "" } = useQueryParams();
const { authToken, isConnected, message, sendMessage } = useChatSocket();
const [conversationRef, setConversationRef] = useState<string>();

const [streamingError, setStreamingError] = useState("");
interface Conversation {
question: string;
answer: string;
}

/**
* get the`chat` state and dispatch function from the search context
* for persisting the chat state when search screen tabs are switched
*/
const {
searchState: { chat },
searchDispatch,
} = useSearchState();
const { question, answer } = chat;
const Chat = () => {
const { searchTerm } = useQueryParams();

const [isStreamingComplete, setIsStreamingComplete] = useState(false);
const initialConversation = {
question: searchTerm,
answer: "",
};

useEffect(() => {
if (
!isStreamingComplete &&
isConnected &&
authToken &&
searchTerm &&
conversationRef
) {
resetChat();
const preparedQuestion = prepareQuestion(
searchTerm,
authToken,
conversationRef,
);
sendMessage(preparedQuestion);
}
}, [
authToken,
isStreamingComplete,
isConnected,
searchTerm,
conversationRef,
sendMessage,
const [conversationRef, setConversationRef] = useState<string>();
const [conversation, setConversation] = useState<Conversation[]>([
initialConversation,
]);
const [isStreaming, setIsStreaming] = useState(false);

useEffect(() => {
setIsStreamingComplete(false);
setConversationRef(uuidv4());
const conversationRef = uuidv4();
setIsStreaming(true);
setConversationRef(conversationRef);
setConversation([initialConversation]);
}, [searchTerm]);

useEffect(() => {
if (!message || !conversationRef) return;
}, [message]);

function handleNewQuestion() {
const input = document.getElementById("dc-search") as HTMLInputElement;
if (input) {
input.focus();
input.value = "";
}
}
const handleConversationCallback = (value: string) => {
setIsStreaming(true);
setConversation([
...conversation,
{
question: value,
answer: "",
},
]);
};

function resetChat() {
searchDispatch({
chat: defaultState.chat,
type: "updateChat",
});
}
const handleResponseCallback = (content: any) => {
setIsStreaming(false);
};

if (!searchTerm)
return (
Expand All @@ -96,68 +57,36 @@ const Chat = ({
</Container>
);

const handleResponseCallback = (content: any) => {
if (!conversationRef) return;

setIsStreamingComplete(true);
searchDispatch({
chat: {
// content here is now a react element
// once continued conversations ar e in place
// see note below for question refactor
answer: content,

// documents should be eventually removed as
// they are now integrated into content
// doing so will require some careful refactoring
// as the documents are used in feedback form
documents: [],

// question should become an entry[] with
// entry[n].question and entry[n].content
question: searchTerm || "",

ref: conversationRef,
},
type: "updateChat",
});
};

return (
<>
<ChatResponse
conversationRef={conversationRef}
isStreamingComplete={isStreamingComplete}
key={conversationRef}
message={message}
question={searchTerm}
responseCallback={handleResponseCallback}
/>
{streamingError && (
<Container>
<Announcement css={{ marginTop: "1rem" }}>
{streamingError}
</Announcement>
</Container>
)}
{isStreamingComplete && (
<>
<Container>
<StyledResponseActions>
<Button isPrimary isLowercase onClick={viewResultsCallback}>
View More Results
</Button>
<Button isLowercase onClick={handleNewQuestion}>
Ask Another Question
</Button>
</StyledResponseActions>
<StyledResponseDisclaimer>{AI_DISCLAIMER}</StyledResponseDisclaimer>
</Container>
<ChatFeedback />
</>
)}
</>
<Container>
<StyledChat
data-conversation-initial={searchTerm}
data-conversation-length={conversation.length}
data-conversation-ref={conversationRef}
>
{conversation
.filter((entry) => entry.question)
.map((entry, index) => {
return (
<ChatResponse
conversationRef={conversationRef}
key={index}
question={entry.question}
responseCallback={handleResponseCallback}
/>
);
})}
<ChatConversation
conversationCallback={handleConversationCallback}
isStreaming={isStreaming}
/>
</StyledChat>
</Container>
);
};

export default React.memo(Chat);
const StyledChat = styled("section", {
padding: "$gr5 0",
});

export default Chat;
16 changes: 16 additions & 0 deletions components/Chat/Conversation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { render, screen } from "@/test-utils";

import ChatConversation from "./Conversation";

describe("Conversaton component", () => {
const handleConversationCallback = jest.fn();

it("renders a chat conversation", () => {
render(
<ChatConversation conversationCallback={handleConversationCallback} />,
);

const wrapper = screen.getByTestId("chat-conversation");
expect(wrapper).toBeInTheDocument();
});
});
Loading

0 comments on commit 6abbce2

Please sign in to comment.