Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine chat response. #314

Merged
merged 1 commit into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions components/Chat/index.tsx → components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useEffect, useState } from "react";

import SourceDocuments from "./components/Answer/SourceDocuments";
import StreamingAnswer from "./components/Answer/StreamingAnswer";
import { StreamingMessage } from "./types/chat";
import ChatResponse from "@/components/Chat/Response/Response";
import { StreamingMessage } from "@/types/components/chat";
import { Work } from "@nulib/dcapi-types";
import { prepareQuestion } from "../../lib/chat-helpers";
import { prepareQuestion } from "@/lib/chat-helpers";

const Chat = ({
authToken,
Expand Down Expand Up @@ -66,19 +65,15 @@ const Chat = ({
};
}, [chatSocket, chatSocket?.url]);

if (!question) return null;

return (
<>
<h3>{question}</h3>
{question && (
<>
<SourceDocuments source_documents={sourceDocuments} />
<StreamingAnswer
answer={streamedAnswer}
isComplete={isStreamingComplete}
/>
</>
)}
</>
<ChatResponse
isStreamingComplete={isStreamingComplete}
question={question}
sourceDocuments={sourceDocuments}
streamedAnswer={streamedAnswer}
/>
);
};

Expand Down
26 changes: 26 additions & 0 deletions components/Chat/Response/Images.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { render, screen, waitFor } from "@testing-library/react";

import ResponseImages from "@/components/Chat/Response/Images";
import { sampleWork1 } from "@/mocks/sample-work1";
import { sampleWork2 } from "@/mocks/sample-work2";

describe("ResponseImages", () => {
it("renders the component", async () => {
const sourceDocuments = [sampleWork1, sampleWork2];

render(<ResponseImages sourceDocuments={sourceDocuments} />);

sourceDocuments.forEach(async (doc) => {
// check that the item is not in the document on initial render
expect(screen.queryByText(`${doc?.title}`)).not.toBeInTheDocument();

// check that the items are in the document after 1 second
await waitFor(
() => {
expect(screen.getByText(`${doc?.title}`)).toBeInTheDocument();
},
{ timeout: 1000 }
);
});
});
});
29 changes: 29 additions & 0 deletions components/Chat/Response/Images.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useEffect, useState } from "react";

import GridItem from "@/components/Grid/Item";
import { StyledImages } from "@/components/Chat/Response/Response.styled";
import { Work } from "@nulib/dcapi-types";

const ResponseImages = ({ sourceDocuments }: { sourceDocuments: Work[] }) => {
const [nextIndex, setNextIndex] = useState(0);

useEffect(() => {
if (nextIndex < sourceDocuments.length) {
const timer = setTimeout(() => {
setNextIndex(nextIndex + 1);
}, 382);

return () => clearTimeout(timer);
}
}, [nextIndex, sourceDocuments.length]);

return (
<StyledImages>
{sourceDocuments.slice(0, nextIndex).map((document: Work) => (
<GridItem key={document.id} item={document} />
))}
</StyledImages>
);
};

export default ResponseImages;
123 changes: 123 additions & 0 deletions components/Chat/Response/Response.styled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { keyframes, styled } from "@/stitches.config";

/* eslint sort-keys: 0 */

const CursorKeyframes = keyframes({
"50%": {
opacity: 0,
},
});

const StyledResponse = styled("section", {
display: "flex",
position: "relative",
gap: "$gr5",
zIndex: "0",
margin: "0 $gr4",

"@xl": {
margin: "0 $gr4",
},

"@lg": {
margin: "0",
},
});

const StyledResponseAside = styled("aside", {
// background: "linear-gradient(7deg, $white 0%, $gray6 100%)",
width: "38.2%",
flexShrink: 0,
borderRadius: "inherit",
borderTopLeftRadius: "unset",
borderBottomLeftRadius: "unset",
});

const StyledResponseContent = styled("div", {
width: "61.8%",
flexGrow: 0,
});

const StyledResponseWrapper = styled("div", {
background:
"linear-gradient(0deg, $white calc(100% - 100px), $brightBlueB calc(100% + 100px))",
padding: "$gr6 0 $gr4",
});

const StyledImages = styled("div", {
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
gap: "$gr4",

"> div": {
width: "calc(33% - 20px)",

"&:nth-child(1)": {
width: "calc(66% - 10px)",
},

figure: {
padding: "0",

"> div": {
boxShadow: "5px 5px 13px rgba(0, 0, 0, 0.25)",
},

figcaption: {
"span:first-of-type": {
textOverflow: "ellipsis",
display: "-webkit-box",
WebkitLineClamp: "3",
WebkitBoxOrient: "vertical",
overflow: "hidden",
},
},
},
},
});

const StyledQuestion = styled("h3", {
fontFamily: "$northwesternDisplayBold",
fontWeight: "400",
fontSize: "$gr6",
lineHeight: "1.35em",
margin: "0",
padding: "0 0 $gr3 0",
color: "$black",
});

const StyledStreamedAnswer = styled("article", {
fontSize: "$gr3",
lineHeight: "1.7em",

strong: {
fontWeight: "400",
fontFamily: "$northwesternSansBold",
},

"span.markdown-cursor": {
position: "relative",
marginLeft: "$gr1",

"&::before": {
content: '""',
position: "absolute",
top: "-5px",
width: "9px",
height: "1.38em",
backgroundColor: "$black20",
animation: `${CursorKeyframes} 1s linear infinite`,
},
},
});

export {
StyledResponse,
StyledResponseAside,
StyledResponseContent,
StyledResponseWrapper,
StyledImages,
StyledQuestion,
StyledStreamedAnswer,
};
55 changes: 55 additions & 0 deletions components/Chat/Response/Response.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
StyledQuestion,
StyledResponse,
StyledResponseAside,
StyledResponseContent,
StyledResponseWrapper,
} from "./Response.styled";

import BouncingLoader from "@/components/Shared/BouncingLoader";
import Container from "@/components/Shared/Container";
import React from "react";
import ResponseImages from "@/components/Chat/Response/Images";
import ResponseStreamedAnswer from "@/components/Chat/Response/StreamedAnswer";
import { Work } from "@nulib/dcapi-types";

interface ChatResponseProps {
isStreamingComplete: boolean;
question: string;
sourceDocuments: Work[];
streamedAnswer?: string;
}

const ChatResponse: React.FC<ChatResponseProps> = ({
isStreamingComplete,
question,
sourceDocuments,
streamedAnswer,
}) => {
return (
<StyledResponseWrapper>
<Container containerType="wide">
<StyledResponse>
<StyledResponseContent>
<StyledQuestion>{question}</StyledQuestion>
{streamedAnswer ? (
<ResponseStreamedAnswer
isStreamingComplete={isStreamingComplete}
streamedAnswer={streamedAnswer}
/>
) : (
<BouncingLoader />
)}
</StyledResponseContent>
{sourceDocuments.length > 0 && (
<StyledResponseAside>
<ResponseImages sourceDocuments={sourceDocuments} />
</StyledResponseAside>
)}
</StyledResponse>
</Container>
</StyledResponseWrapper>
);
};

export default ChatResponse;
20 changes: 20 additions & 0 deletions components/Chat/Response/StreamedAnswer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from "react";
import { StyledStreamedAnswer } from "@/components/Chat/Response/Response.styled";
import useMarkdown from "@/hooks/useMarkdown";

const ResponseStreamedAnswer = ({
isStreamingComplete,
streamedAnswer,
}: {
isStreamingComplete: boolean;
streamedAnswer: string;
}) => {
const { jsx: content } = useMarkdown({
hasCursor: !isStreamingComplete,
markdown: streamedAnswer,
});

return <StyledStreamedAnswer>{content}</StyledStreamedAnswer>;
};

export default ResponseStreamedAnswer;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Chat from "@/components/Chat";
import useChatSocket from "../../../hooks/useChatSocket";
import Chat from "@/components/Chat/Chat";
import useChatSocket from "@/hooks/useChatSocket";
import useQueryParams from "@/hooks/useQueryParams";

const ChatWrapper = () => {
Expand All @@ -9,9 +9,7 @@ const ChatWrapper = () => {
if (!authToken || !chatSocket || !question) return null;

return (
<div style={{ background: "#f0f0f0", padding: "2rem" }}>
<Chat authToken={authToken} chatSocket={chatSocket} question={question} />
</div>
<Chat authToken={authToken} chatSocket={chatSocket} question={question} />
);
};

Expand Down
Loading
Loading