Skip to content

Commit

Permalink
Implement generative ai into search bar component
Browse files Browse the repository at this point in the history
  • Loading branch information
adamjarling committed Mar 7, 2024
1 parent e08e522 commit 45d43e5
Show file tree
Hide file tree
Showing 19 changed files with 604 additions and 131 deletions.
12 changes: 6 additions & 6 deletions components/Chat/components/Answer/Information.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as Tooltip from "@radix-ui/react-tooltip";

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

interface AnswerInformationProps {
timestamp: number;
}

export const generativeAIWarning = `The answers and provided links are generated using chatGPT and metadata from Northwestern University Libraries Digital Collections. This is an experiment and results may be inaccurate, irrelevant, or potentially harmful.`;

export const AnswerInformation: React.FC<AnswerInformationProps> = ({
timestamp,
}) => {
Expand All @@ -32,10 +35,7 @@ export const AnswerInformation: React.FC<AnswerInformationProps> = ({
<Tooltip.Content side="bottom" sideOffset={3} collisionPadding={19}>
<AnswerTooltipArrow />
<AnswerTooltipContent>
The answers and provided links are generated using chatGPT and
metadata from Northwestern University Libraries Digital
Collections. This is an experiment and results may be
inaccurate, irrelevant, or potentially harmful.
{generativeAIWarning}
<em>Answered on {answerDate}</em>
</AnswerTooltipContent>
</Tooltip.Content>
Expand Down Expand Up @@ -71,11 +71,11 @@ export const AnswerTooltip = styled("span", {
},
});

const AnswerTooltipArrow = styled(Tooltip.Arrow, {
export const AnswerTooltipArrow = styled(Tooltip.Arrow, {
fill: "$brightBlueB",
});

const AnswerTooltipContent = styled("div", {
export const AnswerTooltipContent = styled("div", {
background: "$white",
boxShadow: "0 13px 21px 0 rgba(0, 0, 0, 0.13)",
width: "450px",
Expand Down
42 changes: 41 additions & 1 deletion components/Header/Primary.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
import { render, screen } from "@/test-utils";
import HeaderPrimary from "./Primary";

import HeaderPrimary from "./Primary";
import { createDynamicRouteParser } from "next-router-mock/dynamic-routes";
import mockRouter from "next-router-mock";

// Tell mockRouter about the dynamic routes in our app:
mockRouter.useParser(
createDynamicRouteParser(["/search", "/collections/[id]"])
);

jest.mock("@/components/Search/Search", () => {
return function DummySearch() {
return <div data-testid="search-ui-component">Search</div>;
};
});

jest.mock("@/components/Search/JumpTo", () => {
return function DummySearchJumpTo() {
return <div data-testid="search-jump-to">Jump To</div>;
};
});

describe("HeaderPrimary", () => {
beforeEach(() => {
mockRouter.setCurrentUrl("/search");
Expand All @@ -13,4 +31,26 @@ describe("HeaderPrimary", () => {
const wrapper = screen.getByTestId("header-primary-ui-component");
expect(wrapper).toBeInTheDocument();
});

it("renders the search component", () => {
render(<HeaderPrimary />);
const search = screen.getByTestId("search-ui-component");
expect(search).toBeInTheDocument();
});

it("renders the search jump to component", () => {
mockRouter.setCurrentUrl(
"https://devbox.library.northwestern.edu:3000/collections/1c2e2200-c12d-4c7f-8b87-a935c349898a"
);
render(<HeaderPrimary />);

expect(screen.queryByTestId("search-ui-component")).toBeNull();
expect(screen.getByTestId("search-jump-to")).toBeInTheDocument();
});

it("renders browse collections link", () => {
render(<HeaderPrimary />);
const link = screen.getByText("Browse Collections");
expect(link).toBeInTheDocument();
});
});
61 changes: 61 additions & 0 deletions components/Search/GenerativeAI.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as Tooltip from "@radix-ui/react-tooltip";

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

/* eslint sort-keys: 0 */

const GenerativeAIToggleWrapper = styled("div", {
color: "$black50",
fontSize: "$gr2",
display: "flex",
flexShrink: "0",
alignItems: "center",
marginRight: "$gr1",

"& label": {
cursor: "pointer",
flexShrink: "0",
marginLeft: "3px",
marginRight: "4px",
},

"& svg": {
position: "relative",
padding: "0 0 1px",
height: "$gr3",
width: "$gr3",
fill: "$black50",
},
});

const GenerativeAIDialogMessage = styled("p", {});

const FlexBody = styled("div", {
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
width: "100%",
});

const DialogButtonRow = styled("div", {
display: "flex",
justifyContent: "flex-end",
});

const TooltipTrigger = styled(Tooltip.Trigger, {
background: "transparent",
border: "none",
});

const TooltipContent = styled(Tooltip.Content, {
zIndex: 2,
});

export {
DialogButtonRow,
FlexBody,
GenerativeAIDialogMessage,
GenerativeAIToggleWrapper,
TooltipContent,
TooltipTrigger,
};
117 changes: 117 additions & 0 deletions components/Search/GenerativeAIToggle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import {
SearchProvider,
defaultState as defaultSearchState,
} from "@/context/search-context";
import { render, screen } from "@testing-library/react";

import GenerativeAIToggle from "./GenerativeAIToggle";
import React from "react";
import { UserContext } from "@/context/user-context";
import { UserContext as UserContextType } from "@/types/context/user";
import mockRouter from "next-router-mock";
import userEvent from "@testing-library/user-event";

const defaultUser = {
user: {
email: "[email protected]",
isLoggedIn: true,
isReadingRoom: false,
name: "Ace Frehley",
sub: "xyz123",
},
};

const withUserProvider = (
Component: React.ReactNode,
user: UserContextType = defaultUser
) => {
return <UserContext.Provider value={user}>{Component}</UserContext.Provider>;
};

const withSearchProvider = (
Component: React.ReactNode,
initialState = defaultSearchState
) => {
return (
<SearchProvider initialState={initialState}>{Component}</SearchProvider>
);
};

describe("GenerativeAIToggle", () => {
it("renders the generative AI toggle UI and toggles state for a logged in user", async () => {
const user = userEvent.setup();
render(
withUserProvider(
withSearchProvider(<GenerativeAIToggle isSearchActive={true} />)
)
);

const label = screen.getByLabelText("Use Generative AI");
const checkbox = screen.getByRole("checkbox");

expect(label).toBeInTheDocument();
expect(checkbox).toHaveAttribute("data-state", "unchecked");

await user.click(checkbox);
expect(checkbox).toHaveAttribute("data-state", "checked");
});

it("renders the generative AI tooltip", () => {
render(withSearchProvider(<GenerativeAIToggle isSearchActive={true} />));
// Target the svg icon itself
const tooltip = screen.getByText("Information Circle");

expect(tooltip).toBeInTheDocument();
});

it("renders the generative AI dialog for a non-logged in user", async () => {
const user = userEvent.setup();
const nonLoggedInUser = {
user: {
...defaultUser.user,
isLoggedIn: false,
},
};

render(
withUserProvider(
withSearchProvider(<GenerativeAIToggle isSearchActive={true} />),
nonLoggedInUser
)
);

const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);

const generativeAIDialog = screen.queryByText(
"You must be logged in with a Northwestern NetID to use the Generative AI search feature."
);
const cancelButton = screen.getByText("Cancel");

expect(generativeAIDialog).toBeInTheDocument();
expect(screen.getByText("Login")).toBeInTheDocument();
expect(cancelButton).toBeInTheDocument();

await user.click(cancelButton);

expect(generativeAIDialog).not.toBeInTheDocument();
});

it("renders a toggled generative ai state when a query param is set", () => {
const activeSearchState = {
...defaultSearchState,
isGenerativeAI: true,
};

mockRouter.setCurrentUrl("/search?ai=true");
render(
withSearchProvider(
<GenerativeAIToggle isSearchActive={true} />,
activeSearchState
)
);

const checkbox = screen.getByRole("checkbox");
expect(checkbox).toHaveAttribute("data-state", "checked");
});
});
94 changes: 94 additions & 0 deletions components/Search/GenerativeAIToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import * as Tooltip from "@radix-ui/react-tooltip";

import {
CheckboxIndicator,
CheckboxRoot as CheckboxRootStyled,
} from "@/components/Shared/Checkbox.styled";
import {
DialogButtonRow,
FlexBody,
GenerativeAIDialogMessage,
GenerativeAIToggleWrapper,
TooltipContent,
TooltipTrigger,
} from "@/components/Search/GenerativeAI.styled";
import { TooltipArrow, TooltipBody } from "../Shared/Tooltip.styled";

import { Button } from "@nulib/design-system";
import GenerativeAIDialog from "@/components/Shared/Dialog";
import { IconCheck } from "@/components/Shared/SVG/Icons";
import { IconInfo } from "@/components/Shared/SVG/Icons";
import React from "react";
import { generativeAIWarning } from "@/components/Chat/components/Answer/Information";
import useGenerativeAISearchToggle from "@/hooks/useGenerativeAISearchToggle";

function GenerativeAITooltip() {
return (
<Tooltip.Provider delayDuration={20}>
<Tooltip.Root data-testid="tooltip">
<TooltipTrigger>
<IconInfo />
</TooltipTrigger>
<Tooltip.Portal>
<TooltipContent side="bottom" sideOffset={3} collisionPadding={19}>
<TooltipArrow />
<TooltipBody>{generativeAIWarning}</TooltipBody>
</TooltipContent>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
);
}

type GenerativeAIToggleProps = {
isSearchActive: boolean;
};

export default function GenerativeAIToggle({
isSearchActive,
}: GenerativeAIToggleProps) {
const { closeDialog, dialog, isChecked, handleCheckChange, handleLogin } =
useGenerativeAISearchToggle();

return (
<>
<GenerativeAIToggleWrapper
{...(isSearchActive ? { css: { marginRight: "$gr5" } } : {})}
>
<CheckboxRootStyled
checked={isChecked}
id="isGenerativeAI"
onCheckedChange={handleCheckChange}
>
<CheckboxIndicator>
<IconCheck />
</CheckboxIndicator>
</CheckboxRootStyled>
<label htmlFor="isGenerativeAI">Use Generative AI</label>
<GenerativeAITooltip />
</GenerativeAIToggleWrapper>

<div style={{ display: "flex" }}>
<GenerativeAIDialog
isOpen={dialog.isOpen}
title={dialog.title}
handleCloseClick={closeDialog}
size="small"
>
<FlexBody>
<GenerativeAIDialogMessage>
You must be logged in with a Northwestern NetID to use the
Generative AI search feature.
</GenerativeAIDialogMessage>
<DialogButtonRow>
<Button isPrimary onClick={handleLogin}>
Login
</Button>
<Button onClick={closeDialog}>Cancel</Button>
</DialogButtonRow>
</FlexBody>
</GenerativeAIDialog>
</div>
</>
);
}
Loading

0 comments on commit 45d43e5

Please sign in to comment.