Skip to content

Commit

Permalink
Add feedback form for chat responses.
Browse files Browse the repository at this point in the history
  • Loading branch information
mathewjordan committed Jun 21, 2024
1 parent 3556f3a commit 3248e82
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 74 deletions.
62 changes: 55 additions & 7 deletions components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React, { useEffect, useState } from "react";

import { Button } from "@nulib/design-system";
import ChatFeedback from "@/components/Chat/Feedback/Feedback";
import ChatResponse from "@/components/Chat/Response/Response";
import Container from "@/components/Shared/Container";
import { StyledResponseActions } from "@/components/Chat/Response/Response.styled";
import { Work } from "@nulib/dcapi-types";
import { pluralize } from "@/lib/utils/count-helpers";
import { prepareQuestion } from "@/lib/chat-helpers";
import useChatSocket from "@/hooks/useChatSocket";
import useQueryParams from "@/hooks/useQueryParams";
import { useSearchState } from "@/context/search-context";

const Chat = () => {
const Chat = ({ totalResults }: { totalResults?: number }) => {
const { searchTerm = "" } = useQueryParams();
const { authToken, isConnected, message, sendMessage } = useChatSocket();
const { searchDispatch, searchState } = useSearchState();
Expand Down Expand Up @@ -47,15 +52,58 @@ const Chat = () => {
}
}, [message, searchTerm, sourceDocuments, searchDispatch]);

function handleResultsTab() {
if (window.scrollY === 0) {
searchDispatch({ activeTab: "results", type: "updateActiveTab" });
return;
}

window.scrollTo({ behavior: "instant", top: 0 });

const checkScroll = () => {
if (window.scrollY === 0) {
searchDispatch({ activeTab: "results", type: "updateActiveTab" });
window.removeEventListener("scroll", checkScroll);
}
};

window.addEventListener("scroll", checkScroll);
}

function handleNewQuestion() {
const input = document.getElementById("dc-search") as HTMLInputElement;
if (input) {
input.focus();
input.value = "";
}
}

if (!searchTerm) return null;

return (
<ChatResponse
isStreamingComplete={!!answer}
searchTerm={searchTerm}
sourceDocuments={sameQuestionExists ? documents : sourceDocuments}
streamedAnswer={sameQuestionExists ? answer : streamedAnswer}
/>
<>
<ChatResponse
isStreamingComplete={!!answer}
searchTerm={searchTerm}
sourceDocuments={sameQuestionExists ? documents : sourceDocuments}
streamedAnswer={sameQuestionExists ? answer : streamedAnswer}
/>
{answer && (
<>
<Container>
<StyledResponseActions>
<Button isPrimary isLowercase onClick={handleResultsTab}>
View {pluralize("Result", totalResults || 0)}
</Button>
<Button isLowercase onClick={handleNewQuestion}>
Ask another Question
</Button>
</StyledResponseActions>
</Container>
<ChatFeedback />
</>
)}
</>
);
};

Expand Down
116 changes: 116 additions & 0 deletions components/Chat/Feedback/Feedback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Button } from "@nulib/design-system";
import ChatFeedbackOptIn from "@/components/Chat/Feedback/OptIn";
import ChatFeedbackOption from "@/components/Chat/Feedback/Option";
import ChatFeedbackTextArea from "@/components/Chat/Feedback/TextArea";
import Container from "@/components/Shared/Container";
import { styled } from "@/stitches.config";
import { useState } from "react";

const ChatFeedback = () => {
const [isExpanded, setIsExpanded] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);

function handleSubmit() {
console.log("submit feedback");
setIsSubmitted(true);
}

return (
<StyledChatFeedback isSubmitted={isSubmitted}>
<Container>
<StyledChatFeedbackActivate>
<Button isLowercase isText onClick={() => setIsExpanded(true)}>
<span>
Uncertain about this response? Let us know why <strong></strong>
</span>
</Button>
</StyledChatFeedbackActivate>
{isSubmitted ? (
<StyledChatFeedbackConfirmation>
Solid. Thanks for submitting!
</StyledChatFeedbackConfirmation>
) : (
<StyledChatFeedbackForm isExpanded={isExpanded}>
<ChatFeedbackOption
name="style"
label="Don't like the response style"
/>
<ChatFeedbackOption
name="factually"
label="Not factually correct"
/>
<ChatFeedbackOption
name="instructions"
label="Didn't fully follow instruction"
/>
<ChatFeedbackOption
name="refused"
label="Refused when it shouldn't have"
/>
<ChatFeedbackOption name="lazy" label="Being lazy" />
<ChatFeedbackOption name="unsafe" label="Unsafe or problematic" />
<ChatFeedbackOption name="other" label="Other" />
<ChatFeedbackTextArea />
<ChatFeedbackOptIn />
<Button isLowercase isPrimary onClick={handleSubmit}>
Submit
</Button>
</StyledChatFeedbackForm>
)}
</Container>
</StyledChatFeedback>
);
};

/* eslint-disable sort-keys */
const StyledChatFeedbackActivate = styled("div", {
margin: "0 0 $gr2 ",

button: {
fontSize: "$gr3",
},

strong: {
fontFamily: "$northwesternSansBold",
fontWeight: "400",
fontSize: "$gr3",
},
});

const StyledChatFeedbackConfirmation = styled("div", {
fontSize: "$gr3",
});

const StyledChatFeedbackForm = styled("form", {
margin: "$gr3 0",
transition: "200ms all ease-in-out",
width: "61.8%",

variants: {
isExpanded: {
true: {
opacity: "1",
height: "auto",
},
false: {
opacity: "0",
height: "0",
},
},
},
});

const StyledChatFeedback = styled("div", {
variants: {
isSubmitted: {
true: {
[`& ${StyledChatFeedbackActivate}`]: {
display: "none",
},
},
false: {},
},
},
});

export default ChatFeedback;
39 changes: 39 additions & 0 deletions components/Chat/Feedback/OptIn.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { render, screen } from "@testing-library/react";

import ChatFeedbackOptIn from "@/components/Chat/Feedback/OptIn";
import React from "react";
import { UserContext } from "@/context/user-context";

const mockUserContextValue = {
user: {
name: "foo",
email: "[email protected]",
sub: "123",
isLoggedIn: true,
isReadingRoom: false,
},
};

describe("ChatFeedbackOptIn", () => {
it("renders a checkbox input with the user email value", () => {
render(
<UserContext.Provider value={mockUserContextValue}>
<ChatFeedbackOptIn />
</UserContext.Provider>
);

const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("value", "[email protected]");
expect(checkbox).toBeInTheDocument();
});

it("renders a label", () => {
render(
<UserContext.Provider value={mockUserContextValue}>
<ChatFeedbackOptIn />
</UserContext.Provider>
);
const label = screen.getByText(/please follow up with me/i);
expect(label).toBeInTheDocument();
});
});
23 changes: 23 additions & 0 deletions components/Chat/Feedback/OptIn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { UserContext } from "@/context/user-context";
import { styled } from "@/stitches.config";
import { useContext } from "react";

const ChatFeedbackOptIn = () => {
const { user } = useContext(UserContext);

return (
<StyledChatFeedbackOptIn>
<input name="email" type="checkbox" value={user?.email} /> Please follow
up with me regarding this issue.
</StyledChatFeedbackOptIn>
);
};

/* eslint-disable sort-keys */
const StyledChatFeedbackOptIn = styled("label", {
display: "block",
margin: "$gr3 0",
fontSize: "$gr2",
});

export default ChatFeedbackOptIn;
21 changes: 21 additions & 0 deletions components/Chat/Feedback/Option.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// test ChatFeedbackOption.tsx

import { render, screen } from "@testing-library/react";

import ChatFeedbackOption from "@/components/Chat/Feedback/Option";

describe("ChatFeedbackOption", () => {
it("renders a checkbox input", () => {
render(<ChatFeedbackOption name="test" label="This is a test." />);
const checkbox = screen.getByTestId("chat-feedback-option-test");
expect(checkbox).toHaveAttribute("aria-checked", "false");
expect(checkbox).toHaveAttribute("tabindex", "0");
expect(checkbox).toBeInTheDocument();
});

it("renders a label", () => {
render(<ChatFeedbackOption name="test" label="This is a test." />);
const label = screen.getByText(/this is a test/i);
expect(label).toBeInTheDocument();
});
});
88 changes: 88 additions & 0 deletions components/Chat/Feedback/Option.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useRef, useState } from "react";

import { styled } from "@/stitches.config";

const ChatFeedbackOption = ({
name,
label,
}: {
name: string;
label: string;
}) => {
const [isChecked, setIsChecked] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);

function handleOnChange() {
setIsChecked(inputRef?.current?.checked ?? false);
}

const handleKeyDown = (event: React.KeyboardEvent<HTMLLabelElement>) => {
if (event.key === " ") {
event.preventDefault();
setIsChecked(!isChecked);
}
};

const handleClick = () => {
setIsChecked(!isChecked);
};

return (
<StyledChatFeedbackOption
aria-checked={isChecked}
data-testid={`chat-feedback-option-${name}`}
isChecked={isChecked}
onClick={handleClick}
onKeyDown={handleKeyDown}
role="checkbox"
tabIndex={0}
>
<input
name={name}
id={`chat-feedback-option-${name}`}
onChange={handleOnChange}
ref={inputRef}
type="checkbox"
/>
{label}
</StyledChatFeedbackOption>
);
};

/* eslint-disable sort-keys */
const StyledChatFeedbackOption = styled("label", {
display: "inline-flex",
alignItems: "center",
fontSize: "$gr2",
margin: "0 $gr1 $gr1 0",
borderRadius: "1rem",
cursor: "pointer",
transition: "$dcAll",
padding: "$gr1 $gr2",
gap: "3px",

"&:hover": {
boxShadow: "3px 3px 8px #0002",
},

input: {
display: "none",
},

variants: {
isChecked: {
true: {
color: "$white",
border: "1px solid $black80",
backgroundColor: "$black80",
},
false: {
color: "$black50",
border: "1px solid $black20",
backgroundColor: "$white",
},
},
},
});

export default ChatFeedbackOption;
Loading

0 comments on commit 3248e82

Please sign in to comment.