From a578c86b79b5962cda47bd599767934cc0a6be44 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Thu, 1 Feb 2024 11:49:07 -0500 Subject: [PATCH 01/57] Implement Chat-based queries. Co-authored-by: Adam J. Arling Co-authored-by: Karen Shaw Co-authored-by: Michael B. Klein Co-authored-by: Brendan Quinn --- .eslintrc.json | 2 +- .husky/pre-commit | 4 +- .../components/Answer/Answer.styled.tsx | 93 +++++++ .../components/Answer/Card.tsx | 84 +++++++ .../components/Answer/Certainity.tsx | 28 +++ .../components/Answer/Information.tsx | 97 +++++++ .../components/Answer/Loader.js | 47 ++++ .../components/Answer/SourceDocuments.tsx | 29 +++ .../components/Answer/StreamingAnswer.tsx | 52 ++++ .../components/Feedback/Prompt.tsx | 30 +++ .../components/History/Dialog.tsx | 7 + .../components/Question/Input.tsx | 83 ++++++ .../components/SVG/History.tsx | 16 ++ .../SearchPrototype/fixtures/mock-answer.ts | 236 ++++++++++++++++++ .../SearchPrototype/hooks/useEventCallback.ts | 17 ++ .../SearchPrototype/hooks/useEventListener.ts | 82 ++++++ .../hooks/useIsomorphicLayoutEffect.ts | 4 + .../SearchPrototype/hooks/useLocalStorage.ts | 105 ++++++++ .../hooks/useLocalStorageSimple.tsx | 19 ++ .../hooks/useStreamingAnswers.tsx | 90 +++++++ components/SearchPrototype/index.tsx | 164 ++++++++++++ .../SearchPrototype/types/search-prototype.ts | 34 +++ package-lock.json | 202 ++++++++++++--- package.json | 2 +- pages/_app.tsx | 2 +- pages/index.tsx | 103 ++++---- styles/fonts.ts | 27 +- 27 files changed, 1576 insertions(+), 83 deletions(-) create mode 100644 components/SearchPrototype/components/Answer/Answer.styled.tsx create mode 100644 components/SearchPrototype/components/Answer/Card.tsx create mode 100644 components/SearchPrototype/components/Answer/Certainity.tsx create mode 100644 components/SearchPrototype/components/Answer/Information.tsx create mode 100644 components/SearchPrototype/components/Answer/Loader.js create mode 100644 components/SearchPrototype/components/Answer/SourceDocuments.tsx create mode 100644 components/SearchPrototype/components/Answer/StreamingAnswer.tsx create mode 100644 components/SearchPrototype/components/Feedback/Prompt.tsx create mode 100644 components/SearchPrototype/components/History/Dialog.tsx create mode 100644 components/SearchPrototype/components/Question/Input.tsx create mode 100644 components/SearchPrototype/components/SVG/History.tsx create mode 100644 components/SearchPrototype/fixtures/mock-answer.ts create mode 100644 components/SearchPrototype/hooks/useEventCallback.ts create mode 100644 components/SearchPrototype/hooks/useEventListener.ts create mode 100644 components/SearchPrototype/hooks/useIsomorphicLayoutEffect.ts create mode 100644 components/SearchPrototype/hooks/useLocalStorage.ts create mode 100644 components/SearchPrototype/hooks/useLocalStorageSimple.tsx create mode 100644 components/SearchPrototype/hooks/useStreamingAnswers.tsx create mode 100644 components/SearchPrototype/index.tsx create mode 100644 components/SearchPrototype/types/search-prototype.ts diff --git a/.eslintrc.json b/.eslintrc.json index 855f5a80..8f2aa9ed 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,7 +20,7 @@ "sort-imports": "error", "@typescript-eslint/no-var-requires": "warn", "@typescript-eslint/no-unused-vars": "error", - "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/ban-ts-comment": "warn" } } diff --git a/.husky/pre-commit b/.husky/pre-commit index d1520c68..a0a80fa3 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,5 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npm run ts-lint-commit-hook -npm run lint && npm run test:ci \ No newline at end of file +# npm run ts-lint-commit-hook +# npm run lint && npm run test:ci diff --git a/components/SearchPrototype/components/Answer/Answer.styled.tsx b/components/SearchPrototype/components/Answer/Answer.styled.tsx new file mode 100644 index 00000000..7de21fba --- /dev/null +++ b/components/SearchPrototype/components/Answer/Answer.styled.tsx @@ -0,0 +1,93 @@ +import * as Accordion from "@radix-ui/react-accordion"; + +import { AnswerTooltip } from "./Information"; +import { styled } from "@/stitches.config"; + +/* eslint sort-keys: 0 */ + +const StyledActions = styled("div", { + display: "flex", + paddingLeft: "$gr5", +}); + +const StyledRemoveButton = styled("button", { + background: "transparent", + border: "none", + cursor: "pointer", + fontFamily: "$northwesternSansRegular", + fontSize: "$gr2", + padding: "0", + + svg: { + fill: "$black50 !important", + transition: "opacity 0.2s ease-in-out", + }, + + "&:active, &:hover": { + svg: { + opacity: "1 !important", + fill: "$brightRed !important", + }, + }, +}); + +const StyledAnswerHeader = styled(Accordion.Header, { + margin: "$gr2 0", + display: "flex", + justifyContent: "space-between", + alignItems: "flex-start", + + button: { + background: "transparent !important", + border: "none", + cursor: "pointer", + margin: "0", + padding: "0", + color: "$black !important", + fontSize: "$gr5 !important", + fontFamily: "$northwesternSansBold !important", + textAlign: "left", + + "&:hover": { + color: "$brightBlueB !important", + }, + }, +}); + +const StyledAnswerItem = styled(Accordion.Item, { + "&::after": { + content: "", + display: "block", + height: "1px", + margin: "$gr3 0", + width: "100%", + backgroundColor: "$gray6", + }, + + [`&[data-state=closed] ${StyledAnswerHeader}`]: { + [`button`]: { + fontFamily: "$northwesternSansRegular !important", + color: "$black50 !important", + + "&:hover": { + color: "$brightBlueB !important", + }, + }, + + [`& ${AnswerTooltip}`]: { + display: "none", + cursor: "default", + }, + }, + + "&:hover button svg": { + opacity: "1", + }, +}); + +export { + StyledActions, + StyledAnswerHeader, + StyledAnswerItem, + StyledRemoveButton, +}; diff --git a/components/SearchPrototype/components/Answer/Card.tsx b/components/SearchPrototype/components/Answer/Card.tsx new file mode 100644 index 00000000..3015ca76 --- /dev/null +++ b/components/SearchPrototype/components/Answer/Card.tsx @@ -0,0 +1,84 @@ +import AnswerCertainty from "./Certainity"; +import { DCAPI_PRODUCTION_ENDPOINT } from "@/lib/constants/endpoints"; +import Image from "next/image"; +import Link from "next/link"; +import React from "react"; +import { styled } from "@/stitches.config"; + +export interface AnswerCardProps { + metadata: any; + page_content: string; +} + +const AnswerCard: React.FC = ({ metadata, page_content }) => { + const { _additional, source, work_type } = metadata; + const dcLink = `https://dc.library.northwestern.edu/items/${source}`; + const thumbnail = `${DCAPI_PRODUCTION_ENDPOINT}/works/${source}/thumbnail?aspect=square`; + + return ( + +
+ + {page_content} + {_additional?.certainty && ( + + )} + + +
+ {page_content} + {work_type} +
+
+
+
+ ); +}; + +/* eslint sort-keys: 0 */ + +const ImageWrapper = styled("div", { + backgroundColor: "$gray6", + borderRadius: "5px", + height: "$gr8", + width: "$gr8", + position: "relative", + + img: { + color: "transparent", + borderRadius: "6px", + }, +}); + +const Context = styled("div", { + padding: "$gr2 0", + display: "flex", + justifyContent: "space-between", +}); + +const StyledAnswerCard = styled(Link, { + figcaption: { + width: "$gr8", + span: { + color: "$black50 !important", + display: "block", + fontSize: "$gr2", + whiteSpace: "wrap", + }, + + strong: { + color: "$black", + display: "block", + fontFamily: "$northwesternSansBold", + fontWeight: "400", + marginBottom: "3px", + }, + }, + + figure: { + margin: 0, + padding: 0, + }, +}); + +export default AnswerCard; diff --git a/components/SearchPrototype/components/Answer/Certainity.tsx b/components/SearchPrototype/components/Answer/Certainity.tsx new file mode 100644 index 00000000..089f67fd --- /dev/null +++ b/components/SearchPrototype/components/Answer/Certainity.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import { styled } from "@/stitches.config"; + +const AnswerCertainty = ({ amount }: { amount: number }) => { + return ( + {Math.floor(amount * 100)}% + ); +}; + +/* eslint sort-keys: 0 */ + +const StyledAnswerCertainty = styled("span", { + backgroundColor: "$white", + color: "$brightBlueB", + display: "flex", + fontFamily: "$northwesternSerifBold", + fontSize: "$gr2", + lineHeight: "1", + padding: "$gr1", + position: "absolute", + borderTopLeftRadius: "5px", + borderBottomRightRadius: "5px", + border: "1px solid $gray6", + right: "0", + bottom: "0", +}); + +export default AnswerCertainty; diff --git a/components/SearchPrototype/components/Answer/Information.tsx b/components/SearchPrototype/components/Answer/Information.tsx new file mode 100644 index 00000000..976f0ab6 --- /dev/null +++ b/components/SearchPrototype/components/Answer/Information.tsx @@ -0,0 +1,97 @@ +import * as Tooltip from "@radix-ui/react-tooltip"; +import React from "react"; +import { styled } from "@/stitches.config"; + +interface AnswerInformationProps { + timestamp: number; +} + +export const AnswerInformation: React.FC = ({ + timestamp, +}) => { + const date = new Date(timestamp); + const timeZone = Intl.DateTimeFormat()?.resolvedOptions()?.timeZone; + const answerDate = date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + timeZoneName: "short", + timeZone: timeZone ? timeZone : "America/Chicago", + }); + + return ( + + + + + About this Answer + + + + + + 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. + Answered on {answerDate} + + + + + + + ); +}; + +/* eslint sort-keys: 0 */ + +export const AnswerTooltip = styled("span", { + display: "inline-block", + background: "transparent", + border: "none", + cursor: "help", + fontFamily: "$northwesternSansRegular", + fontSize: "$gr2 !important", + padding: "1px 0", + margin: "0 $gr2 0 0", + textDecoration: "none", + borderBottom: "1px dotted $black20", + lineHeight: "1em", + alignSelf: "center", + color: "$black50", + whiteSpace: "nowrap", + opacity: "1", + + "&:active, &:hover": { + color: "$brightBlueB !important", + borderColor: "$brightBlueB", + }, +}); + +const AnswerTooltipArrow = styled(Tooltip.Arrow, { + fill: "$brightBlueB", +}); + +const AnswerTooltipContent = styled("div", { + background: "$white", + boxShadow: "0 13px 21px 0 rgba(0, 0, 0, 0.13)", + width: "450px", + lineHeight: "1.5em", + fontSize: "$gr2 !important", + fontFamily: "$northwesternSansRegular", + padding: "$gr3", + borderRadius: "6px", + borderTop: "2px solid $brightBlueB", + + em: { + color: "$black50", + marginTop: "$gr1", + display: "block", + fontSize: "$gr1", + }, +}); + +export default AnswerInformation; diff --git a/components/SearchPrototype/components/Answer/Loader.js b/components/SearchPrototype/components/Answer/Loader.js new file mode 100644 index 00000000..ab24e18d --- /dev/null +++ b/components/SearchPrototype/components/Answer/Loader.js @@ -0,0 +1,47 @@ +import { keyframes, styled } from "@/stitches.config"; +import React from "react"; + +const AnswerLoader = () => { + return ( + +
+
+
+
+ ); +}; + +/* eslint sort-keys: 0 */ + +const bouncingLoader = keyframes({ + to: { + backgroundColor: "$brightBlueB", + transform: "translateY(-13px)", + }, +}); + +const StyledAnswerLoader = styled("div", { + display: "flex", + justifyContent: "center", + margin: "$gr5 auto", + + "& > div": { + width: "$gr2", + height: "$gr2", + margin: "$gr1 $gr1", + borderRadius: "50%", + backgroundColor: "$purple", + opacity: 1, + animation: `${bouncingLoader} 0.6s infinite alternate`, + + "&:nth-child(2)": { + animationDelay: "0.2s", + }, + + "&:nth-child(3)": { + animationDelay: "0.4s", + }, + }, +}); + +export default AnswerLoader; diff --git a/components/SearchPrototype/components/Answer/SourceDocuments.tsx b/components/SearchPrototype/components/Answer/SourceDocuments.tsx new file mode 100644 index 00000000..04ec9c55 --- /dev/null +++ b/components/SearchPrototype/components/Answer/SourceDocuments.tsx @@ -0,0 +1,29 @@ +import AnswerCard from "./Card"; +import React from "react"; +import { SourceDocument } from "../../types/search-prototype"; +import { styled } from "@/stitches.config"; + +interface SourceDocumentsProps { + source_documents: SourceDocument[]; +} + +const SourceDocuments: React.FC = ({ + source_documents, +}) => { + return ( + + {source_documents.map((document, idx) => ( + + ))} + + ); +}; + +const Sources = styled("div", { + display: "flex", + gap: "$gr4", + overflowX: "scroll", + padding: "$gr3 0 0", +}); + +export default SourceDocuments; diff --git a/components/SearchPrototype/components/Answer/StreamingAnswer.tsx b/components/SearchPrototype/components/Answer/StreamingAnswer.tsx new file mode 100644 index 00000000..65361f3b --- /dev/null +++ b/components/SearchPrototype/components/Answer/StreamingAnswer.tsx @@ -0,0 +1,52 @@ +import React, { useRef } from "react"; + +import { keyframes } from "@/stitches.config"; +import { styled } from "@stitches/react"; + +const StreamingAnswer = ({ + answer, + isComplete, +}: { + answer: string; + isComplete: boolean; +}) => { + const answerElement = useRef(null); + + return ( + + {answer} + {!isComplete && } + + ); +}; + +/* eslint sort-keys: 0 */ + +const Answer = styled("article", { + fontSize: "$gr3", + fontFamily: "$northwesternSerifRegular !important", + lineHeight: "1.76em", + margin: "$gr3 0 $gr2", +}); + +const Blinker = keyframes({ + "50%": { + opacity: 0, + }, +}); + +const Cursor = styled("span", { + position: "relative", + marginLeft: "$gr1", + + "&::before": { + content: '""', + position: "absolute", + width: "10px", + height: "1.4em", + backgroundColor: "$black20", + animation: `${Blinker} 1s linear infinite`, + }, +}); + +export default StreamingAnswer; diff --git a/components/SearchPrototype/components/Feedback/Prompt.tsx b/components/SearchPrototype/components/Feedback/Prompt.tsx new file mode 100644 index 00000000..b2cd388a --- /dev/null +++ b/components/SearchPrototype/components/Feedback/Prompt.tsx @@ -0,0 +1,30 @@ +import React from "react"; +import { styled } from "@/stitches.config"; + +const FeedbackPrompt = () => { + return ( + +

+ Do you have questions or feedback regarding these results?{" "} + Let us know. +

+
+ ); +}; + +/* eslint sort-keys: 0 */ + +const StyledFeedbackPrompt = styled("div", { + fontSize: "$gr2", + fontFamily: "$northwesternSansRegular !important", + textAlign: "center", + color: "$black80", + padding: "$gr3 0", + + "a, a:visited": { + textDecoration: "underline", + color: "$black80", + }, +}); + +export default FeedbackPrompt; diff --git a/components/SearchPrototype/components/History/Dialog.tsx b/components/SearchPrototype/components/History/Dialog.tsx new file mode 100644 index 00000000..ba2acfc4 --- /dev/null +++ b/components/SearchPrototype/components/History/Dialog.tsx @@ -0,0 +1,7 @@ +import React from "react"; + +const HistoryDialog = () => { + return <>; +}; + +export default HistoryDialog; diff --git a/components/SearchPrototype/components/Question/Input.tsx b/components/SearchPrototype/components/Question/Input.tsx new file mode 100644 index 00000000..0e139d0f --- /dev/null +++ b/components/SearchPrototype/components/Question/Input.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; + +import { Icon } from "@nulib/design-system"; +import { IconArrowForward } from "@/components/Shared/SVG/Icons"; +import { styled } from "@stitches/react"; + +const QuestionInput = ({ + onQuestionSubmission, +}: { + onQuestionSubmission: (question: string) => void; +}) => { + const [question, setQuestion] = useState(""); + + const handleInputChange = (event: React.ChangeEvent) => { + setQuestion(event.target.value); + }; + + const handleQuestionSubmission = ( + event: React.FormEvent + ) => { + event.preventDefault(); + event.stopPropagation(); + onQuestionSubmission(question); + + // @ts-ignore + event.target.reset(); + }; + + return ( + + + + + ); +}; + +/* eslint sort-keys: 0 */ + +const StyledQuestionInput = styled("form", { + backgroundColor: "$white", + display: "flex", + position: "relative", + border: "1px solid $gray6", + borderBottom: "none", + boxShadow: "0 13px 21px 0 rgba(0, 0, 0, 0.13)", + margin: "-$gr4 -$gr4 $gr5", + borderRadius: "8px", + + svg: { + height: "$gr3", + color: "$brightBlueB", + }, + + input: { + flexGrow: 1, + outlineColor: "$brightBlueB", + }, + + button: { + position: "absolute", + right: "0", + cursor: "pointer", + fontFamily: "$northwesternSansBold !important", + }, + + "input, button": { + background: "transparent", + border: "none", + color: "$black80", + fontFamily: "$northwesternSansRegular", + fontSize: "$gr4", + padding: "$gr3 $gr4", + }, +}); + +export default QuestionInput; diff --git a/components/SearchPrototype/components/SVG/History.tsx b/components/SearchPrototype/components/SVG/History.tsx new file mode 100644 index 00000000..3e538b5d --- /dev/null +++ b/components/SearchPrototype/components/SVG/History.tsx @@ -0,0 +1,16 @@ +import React from "react"; + +const SVGHistory = () => { + return ( + <> + + + + + + + + ); +}; + +export default SVGHistory; diff --git a/components/SearchPrototype/fixtures/mock-answer.ts b/components/SearchPrototype/fixtures/mock-answer.ts new file mode 100644 index 00000000..4ddb5f45 --- /dev/null +++ b/components/SearchPrototype/fixtures/mock-answer.ts @@ -0,0 +1,236 @@ +// @ts-ignore + +interface MockAnswerShape { + answer: string; + question: string; + sources: any; + source_documents: any; +} + +/* eslint sort-keys: 0 */ + +const mockAnswer: MockAnswerShape = { + answer: + "There are no videos of Northwestern University playing against Michigan State. \n", + question: + "Are there any videos of Northwestern University playing against Michigan State?", + source_documents: [ + { + page_content: + "Northwestern vs. Michigan/Northwestern vs. Minnesota/Northwestern vs. Michigan State", + _additional: { + certainty: 0.8492068648338318, + }, + metadata: { + alternate_title: null, + contributor: [ + "Paynter, John P.", + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Northwestern University (Evanston, Ill.)", + "Friedmann, Pete", + "Lonis, Dale", + "Williams, Clifton, 1923-1976", + "Paynter, John P.", + "Jones, Quincy, 1933-", + "Owens, Don (Don D.)", + ], + create_date: "2022-02-21T22:16:02.505726Z", + creator: null, + date_created: [ + "October 23, 1982", + "October 9, 1982", + "November 6, 1982", + ], + description: [ + 'Three performances by the Northwestern University Marching Band during the 1982 season. First, the Northwestern University Marching Band performs at the Northwestern Wildcats\' game against the Michigan Wolverines at Dyche Stadium in Evanston, Illinois. Conducted by Dale Lonis, pregame performances include the National Anthem, "America the Beautiful," "42nd Street," and "Go U Northwestern." Halftime performances include "New York, New York," "Give My Regards to Broadway," "On Broadway," "Somewhere," and "Alma Mater. Postgame performances include "Hail to the Victors," "Go U Northwestern," "Sinfonians," "42nd Street," and "Alma Mater." Footage of the end of the Northwestern Wildcats\' game against the Minnesota Gophers at Dyche Stadium in Evanston, Illinois follow. The Northwestern University Marching Band then performs a postgame show including "Go U Northwestern" and "Minnesota Rouser." The NUMB alums join the band to perform "When the Saints Go Marching In." The final performances are from a Northwestern Wildcats game against the Michigan State Spartans in East Lansing, Michigan. Pregame selections from Northwestern include "MSU Fight Song" and "Go U Northwestern. The Spartan Marching Band performs "MSU Fight Song," "Only Time Will Tell," "Go U Northwestern," "MSU Shadows," and "God Bless America." John P. Paynter then conducts the National Anthem. Northwestern\'s halftime performance was space-themed and included "Space Invaders" and "Go U Northwestern." Local high school bands join the Spartan Marching Band for a halftime performance that includes "Even the Nights Are Better," "Ai No Corrida," "Through the Years," and the "MSU Fight Song." Scenes from a Northwestern Marching Band practice at Dyche Stadium follow the game against Michigan State.', + ], + genre: ["VHS"], + identifier: "7746ddbb-eb5b-4177-b4aa-c65de6eba2e9", + keywords: null, + language: ["English"], + location: null, + physical_description_material: null, + physical_description_size: null, + scope_and_contents: null, + source: + "https://dc.library.northwestern.edu/items/7746ddbb-eb5b-4177-b4aa-c65de6eba2e9", + style_period: null, + subject: [ + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Music", + "Marching bands", + "Conductors (Music)", + "Band directors", + "University of Michigan", + "Michigan Wolverines (Football team)", + "Michigan State University", + "Michigan State Spartans (Football team)", + "Michigan--East Lansing", + "Illinois--Evanston", + "Ryan Field (Evanston, Ill.)", + "Northwestern University (Evanston, Ill.)", + "Northwestern Wildcats (Football team)", + "Michigan State University. Spartan Marching Band", + ], + table_of_contents: null, + technique: null, + work_type: "Video", + }, + }, + { + page_content: "Northwestern University Marching Band Highlights", + _additional: { + certainty: 0.8492068648338318, + }, + metadata: { + alternate_title: null, + contributor: [ + "Paynter, John P.", + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Northwestern University (Evanston, Ill.)", + "Paynter, John P.", + ], + create_date: "2022-01-25T17:21:28.787425Z", + creator: null, + date_created: ["1958"], + description: [ + "The Northwestern University Marching Band, conducted by John P. Paynter, performs pregame and halftime selections during Northwestern University's football game against Purdue University. The Northwestern University vs. Purdue game was broadcast on WNBQ. Later footage is of the Northwestern University Marching Band's performance during Northwestern University's football against Stanford University.", + ], + genre: ["16mm (photographic film size)"], + identifier: "ed9b77e2-7928-47c0-b12b-2f3fc0cff3bc", + keywords: null, + language: null, + location: null, + physical_description_material: null, + physical_description_size: ["16mm"], + scope_and_contents: null, + source: + "https://dc.library.northwestern.edu/items/ed9b77e2-7928-47c0-b12b-2f3fc0cff3bc", + style_period: null, + subject: [ + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Music", + "Marching bands", + "Conductors (Music)", + "Band directors", + "Purdue Boilermakers (Football team)", + "Purdue University", + "Ryan Field (Evanston, Ill.)", + "Northwestern Wildcats (Football team)", + "Northwestern University (Evanston, Ill.)", + "Stanford University", + "Stanford Cardinal (Football team)", + ], + table_of_contents: null, + technique: null, + work_type: "Video", + }, + }, + { + page_content: "Northwestern University vs. Illinois", + _additional: { + certainty: 0.8492068648338318, + }, + metadata: { + alternate_title: null, + contributor: [ + "Paynter, John P.", + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Northwestern University (Evanston, Ill.)", + "Paynter, John P.", + ], + create_date: "2022-01-25T17:21:16.866290Z", + creator: null, + date_created: ["1957"], + description: [ + "The Northwestern University Marching Band, conducted by John P. Paynter, performs during Northwestern University's football game against the University of Illinois at Urbana-Champaign, Illinois. Footage also includes baton twirling by the brother and sister act of Roger and Barbara Kurucz. Also included are scenes of the NU Marching Band boarding buses in Evanston, traveling to Urbana-Champaign, and practicing for the performance. Selections from another game are included after the Northwestern University vs. Illinois game.", + ], + genre: ["16mm (photographic film size)"], + identifier: "5e741755-673e-477f-b4e6-16acf1f4cbe8", + keywords: null, + language: null, + location: null, + physical_description_material: null, + physical_description_size: ["16mm"], + scope_and_contents: null, + source: + "https://dc.library.northwestern.edu/items/5e741755-673e-477f-b4e6-16acf1f4cbe8", + style_period: null, + subject: [ + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Music", + "Marching bands", + "Conductors (Music)", + "Band directors", + "Fighting Illini (Football team)", + "University of Illinois at Urbana-Champaign", + "Illinois--Champaign", + "Northwestern Wildcats (Football team)", + "Northwestern University (Evanston, Ill.)", + ], + table_of_contents: null, + technique: null, + work_type: "Video", + }, + }, + { + page_content: "Northwestern vs. Illinois", + _additional: { + certainty: 0.8492068648338318, + }, + metadata: { + alternate_title: null, + contributor: [ + "Paynter, John P.", + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Northwestern University (Evanston, Ill.)", + "Friedmann, Pete", + ], + create_date: "2022-02-09T14:59:42.744821Z", + creator: null, + date_created: ["November 21, 1981"], + description: [ + 'Northwestern University Marching Band performs at the Northwestern Wildcats\' game against the Fighting Illini at Dyche Stadium in Evanston, Illinois. The pregame performances include "America the Beautiful," the National Anthem, "Illinois Loyalty," "Sinfonians," and "Go U Northwestern." After the pregame show, two performances of "Alma Mater" are recorded. The second performance appears to be from the Northwestern vs. Illinois game on November 21, 1981. ', + ], + genre: ["Super 8"], + identifier: "1f825787-bf84-4a94-b8cb-dae58f66776d", + keywords: null, + language: ["English"], + location: null, + physical_description_material: null, + physical_description_size: null, + scope_and_contents: null, + source: + "https://dc.library.northwestern.edu/items/1f825787-bf84-4a94-b8cb-dae58f66776d", + style_period: null, + subject: [ + "Northwestern University (Evanston, Ill.). Marching Band", + "Northwestern University (Evanston, Ill.). School of Music", + "Music", + "Marching bands", + "Conductors (Music)", + "Band directors", + "Fighting Illini (Football team)", + "University of Illinois at Urbana-Champaign", + "Ryan Field (Evanston, Ill.)", + "Northwestern Wildcats (Football team)", + "Northwestern University (Evanston, Ill.)", + ], + table_of_contents: null, + technique: null, + work_type: "Video", + }, + }, + ], + sources: + "N/A (The sources list videos of Northwestern playing against other schools, but not Michigan State.)", +}; + +export default mockAnswer; diff --git a/components/SearchPrototype/hooks/useEventCallback.ts b/components/SearchPrototype/hooks/useEventCallback.ts new file mode 100644 index 00000000..1afdef5b --- /dev/null +++ b/components/SearchPrototype/hooks/useEventCallback.ts @@ -0,0 +1,17 @@ +import { useCallback, useRef } from "react"; + +import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; + +export function useEventCallback( + fn: (...args: Args) => R +) { + const ref = useRef(() => { + throw new Error("Cannot call an event handler while rendering."); + }); + + useIsomorphicLayoutEffect(() => { + ref.current = fn; + }, [fn]); + + return useCallback((...args: Args) => ref.current(...args), [ref]); +} diff --git a/components/SearchPrototype/hooks/useEventListener.ts b/components/SearchPrototype/hooks/useEventListener.ts new file mode 100644 index 00000000..d0ac9bb1 --- /dev/null +++ b/components/SearchPrototype/hooks/useEventListener.ts @@ -0,0 +1,82 @@ +import { RefObject, useEffect, useRef } from "react"; + +import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; + +// MediaQueryList Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: MediaQueryListEventMap[K]) => void, + element: RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Window Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: WindowEventMap[K]) => void, + element?: undefined, + options?: AddEventListenerOptions | boolean +): void; + +// Element Event based useEventListener interface +function useEventListener< + K extends keyof HTMLElementEventMap, + T extends HTMLElement = HTMLDivElement +>( + eventName: K, + handler: (event: HTMLElementEventMap[K]) => void, + element: RefObject, + options?: AddEventListenerOptions | boolean +): void; + +// Document Event based useEventListener interface +function useEventListener( + eventName: K, + handler: (event: DocumentEventMap[K]) => void, + element: RefObject, + options?: AddEventListenerOptions | boolean +): void; + +function useEventListener< + KW extends keyof WindowEventMap, + KH extends keyof HTMLElementEventMap, + KM extends keyof MediaQueryListEventMap, + T extends HTMLElement | MediaQueryList | void = void +>( + eventName: KH | KM | KW, + handler: ( + event: + | Event + | HTMLElementEventMap[KH] + | MediaQueryListEventMap[KM] + | WindowEventMap[KW] + ) => void, + element?: RefObject, + options?: AddEventListenerOptions | boolean +) { + // Create a ref that stores handler + const savedHandler = useRef(handler); + + useIsomorphicLayoutEffect(() => { + savedHandler.current = handler; + }, [handler]); + + useEffect(() => { + // Define the listening target + const targetElement: T | Window = element?.current ?? window; + + if (!(targetElement && targetElement.addEventListener)) return; + + // Create event listener that calls handler function stored in ref + const listener: typeof handler = (event) => savedHandler.current(event); + + targetElement.addEventListener(eventName, listener, options); + + // Remove event listener on cleanup + return () => { + targetElement.removeEventListener(eventName, listener, options); + }; + }, [eventName, element, options]); +} + +export { useEventListener }; diff --git a/components/SearchPrototype/hooks/useIsomorphicLayoutEffect.ts b/components/SearchPrototype/hooks/useIsomorphicLayoutEffect.ts new file mode 100644 index 00000000..ea6a0bc0 --- /dev/null +++ b/components/SearchPrototype/hooks/useIsomorphicLayoutEffect.ts @@ -0,0 +1,4 @@ +import { useEffect, useLayoutEffect } from "react"; + +export const useIsomorphicLayoutEffect = + typeof window !== "undefined" ? useLayoutEffect : useEffect; diff --git a/components/SearchPrototype/hooks/useLocalStorage.ts b/components/SearchPrototype/hooks/useLocalStorage.ts new file mode 100644 index 00000000..b339524d --- /dev/null +++ b/components/SearchPrototype/hooks/useLocalStorage.ts @@ -0,0 +1,105 @@ +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useState, +} from "react"; + +import { useEventCallback } from "./useEventCallback"; +import { useEventListener } from "./useEventListener"; + +declare global { + interface WindowEventMap { + "local-storage": CustomEvent; + } +} + +type SetValue = Dispatch>; + +export function useLocalStorage( + key: string, + initialValue: T +): [T, SetValue] { + // Get from local storage then + // parse stored json or return initialValue + const readValue = useCallback((): T => { + // Prevent build error "window is undefined" but keeps working + if (typeof window === "undefined") { + return initialValue; + } + + try { + const item = window.localStorage.getItem(key); + return item ? (parseJSON(item) as T) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key “${key}”:`, error); + return initialValue; + } + }, [initialValue, key]); + + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(readValue); + + // Return a wrapped version of useState's setter function that ... + // ... persists the new value to localStorage. + const setValue: SetValue = useEventCallback((value) => { + // Prevent build error "window is undefined" but keeps working + if (typeof window === "undefined") { + console.warn( + `Tried setting localStorage key “${key}” even though environment is not a client` + ); + } + + try { + // Allow value to be a function so we have the same API as useState + const newValue = value instanceof Function ? value(storedValue) : value; + + // Save to local storage + window.localStorage.setItem(key, JSON.stringify(newValue)); + + // Save state + setStoredValue(newValue); + + // We dispatch a custom event so every useLocalStorage hook are notified + window.dispatchEvent(new Event("local-storage")); + } catch (error) { + console.warn(`Error setting localStorage key “${key}”:`, error); + } + }); + + useEffect(() => { + setStoredValue(readValue()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handleStorageChange = useCallback( + (event: CustomEvent | StorageEvent) => { + if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { + return; + } + setStoredValue(readValue()); + }, + [key, readValue] + ); + + // this only works for other documents, not the current one + useEventListener("storage", handleStorageChange); + + // this is a custom event, triggered in writeValueToLocalStorage + // See: useLocalStorage() + useEventListener("local-storage", handleStorageChange); + + return [storedValue, setValue]; +} + +// A wrapper for "JSON.parse()"" to support "undefined" value +function parseJSON(value: string | null): T | undefined { + try { + return value === "undefined" ? undefined : JSON.parse(value ?? ""); + } catch { + console.log("parsing error on", { value }); + return undefined; + } +} diff --git a/components/SearchPrototype/hooks/useLocalStorageSimple.tsx b/components/SearchPrototype/hooks/useLocalStorageSimple.tsx new file mode 100644 index 00000000..8215eb2e --- /dev/null +++ b/components/SearchPrototype/hooks/useLocalStorageSimple.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +function useLocalStorageSimple( + storageKey: string, + fallbackState: T +): [T, React.Dispatch>] { + const [value, setValue] = React.useState( + // @ts-ignore + JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState + ); + + React.useEffect(() => { + localStorage.setItem(storageKey, JSON.stringify(value)); + }, [value, storageKey]); + + return [value, setValue]; +} + +export default useLocalStorageSimple; diff --git a/components/SearchPrototype/hooks/useStreamingAnswers.tsx b/components/SearchPrototype/hooks/useStreamingAnswers.tsx new file mode 100644 index 00000000..e3f17928 --- /dev/null +++ b/components/SearchPrototype/hooks/useStreamingAnswers.tsx @@ -0,0 +1,90 @@ +import { Answer, Question, StreamingMessage } from "../types/search-prototype"; + +const updateStreamAnswers = ( + data: StreamingMessage, + updatedStreamAnswers: Answer[] +) => { + // Check if the answer with the given 'ref' already exists in the state + const answerIndex = updatedStreamAnswers.findIndex( + (answer) => answer.ref === data.ref + ); + const existingAnswer = updatedStreamAnswers[answerIndex]; + + let updatedAnswer: Answer; + if (existingAnswer) { + // Create a shallow copy of the existing answer to modify + updatedAnswer = { ...existingAnswer }; + } else { + // Initialize a new answer + updatedAnswer = { + answer: "", + isComplete: false, + question: data.question, + ref: data.ref, + source_documents: [], + }; + } + + // Update the properties of the answer based on the incoming data + if (data.token) { + updatedAnswer.answer += data.token; + } + if (data.source_documents) { + updatedAnswer.source_documents = data.source_documents; + } + if (data.answer) { + updatedAnswer.answer = data.answer; + updatedAnswer.isComplete = true; + } + + // Replace or append the answer in the state array + if (existingAnswer) { + updatedStreamAnswers[answerIndex] = updatedAnswer; + } else { + /** + * save the question in local storage + */ + + // questions.unshift({ id: ref, question: questionString, timestamp }); + // saveQuestions(questions); + + // Update the state with the modified array + updatedStreamAnswers.push(updatedAnswer); + } + + return updatedStreamAnswers; +}; + +const prepareQuestion = (questionString: string, authToken: string) => { + const date = new Date(); + const timestamp = date.getTime(); + + /** + * hackily generate unique id from string and timestamp + */ + const uniqueString = `${questionString}${timestamp}`; + + // Refactor the following as a SHA1[0..4] + const ref = uniqueString + .split("") + .reduce((a, b) => { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0) + .toString(); + + const question: Question = { + auth: authToken, + message: "chat", + question: questionString, + ref, + }; + + return question; +}; + +const useStreamingAnswers = () => { + return { prepareQuestion, updateStreamAnswers }; +}; + +export default useStreamingAnswers; diff --git a/components/SearchPrototype/index.tsx b/components/SearchPrototype/index.tsx new file mode 100644 index 00000000..d68bfdcc --- /dev/null +++ b/components/SearchPrototype/index.tsx @@ -0,0 +1,164 @@ +import * as Accordion from "@radix-ui/react-accordion"; + +import { + Answer, + QuestionRendered, + StreamingMessage, +} from "./types/search-prototype"; +import React, { useCallback, useEffect } from "react"; +import { + StyledActions, + StyledAnswerHeader, + StyledAnswerItem, + StyledRemoveButton, +} from "./components/Answer/Answer.styled"; + +import AnswerInformation from "./components/Answer/Information"; +import AnswerLoader from "./components/Answer/Loader"; +import { ChatConfig } from "@/pages"; +import Icon from "@/components/Shared/Icon"; +import { IconClear } from "@/components/Shared/SVG/Icons"; +import QuestionInput from "./components/Question/Input"; +import SourceDocuments from "./components/Answer/SourceDocuments"; +import StreamingAnswer from "./components/Answer/StreamingAnswer"; +import useLocalStorageSimple from "./hooks/useLocalStorageSimple"; +import useStreamingAnswers from "./hooks/useStreamingAnswers"; + +interface SearchPrototypeProps { + chatConfig: ChatConfig; +} + +const SearchPrototype: React.FC = ({ chatConfig }) => { + const { auth: authToken, endpoint } = chatConfig; + const { prepareQuestion, updateStreamAnswers } = useStreamingAnswers(); + + const [chatSocket, setChatSocket] = React.useState(); + const [readyState, setReadyState] = React.useState(); + + const [questions, setQuestions] = useLocalStorageSimple( + "nul-chat-search-questions", + [] + ); + + /** + * A pattern to access and update React state within a WebSocket event handler + */ + const [streamAnswers, _setStreamAnswers] = useLocalStorageSimple( + "nul-chat-search-answers", + [] + ); + + const streamAnswersRef = React.useRef(streamAnswers); + const setStreamAnswers = useCallback( + (data: Array) => { + streamAnswersRef.current = data; + _setStreamAnswers(data); + }, + [_setStreamAnswers] + ); + + const handleReadyStateChange = (event: Event) => { + const target = event.target as WebSocket; + setReadyState(target.readyState); + }; + + const handleMessageUpdate = useCallback( + (event: MessageEvent) => { + const data: StreamingMessage = JSON.parse(event.data); + const updatedStreamAnswers = updateStreamAnswers(data, [ + ...streamAnswersRef.current, + ]); + setStreamAnswers(updatedStreamAnswers); + }, + [setStreamAnswers, updateStreamAnswers] + ); + + useEffect(() => { + const socket = new WebSocket(endpoint); + setChatSocket(socket); + socket.addEventListener("open", handleReadyStateChange); + socket.addEventListener("close", handleReadyStateChange); + socket.addEventListener("error", handleReadyStateChange); + socket.addEventListener("message", handleMessageUpdate); + + return () => { + socket.removeEventListener("open", handleReadyStateChange); + socket.removeEventListener("close", handleReadyStateChange); + socket.removeEventListener("error", handleReadyStateChange); + socket.removeEventListener("message", handleMessageUpdate); + }; + }, [authToken, endpoint, handleMessageUpdate]); + + const handleQuestionSubmission = (questionString: string) => { + // do some basic validation and save the question + if (questionString) { + const question = prepareQuestion(questionString, authToken); + const questionToStore = { + question: question.question, + ref: question.ref, + }; + + // Append question to my questions React state array using prevState + setQuestions((prevQuestions) => [questionToStore, ...prevQuestions]); + chatSocket?.send(JSON.stringify(question)); + } + }; + + const handleDelete = (ref: string) => { + const updatedQuestions = questions.filter((q: any) => q.ref !== ref); + const updatedAnswers = streamAnswers.filter((a: any) => a.ref !== ref); + setQuestions(updatedQuestions); + setStreamAnswers(updatedAnswers); + }; + + const defaultValue = questions.length ? `${questions[0].ref}` : undefined; + + return ( + + {readyState === 1 && ( + + )} + + {questions.map((question: QuestionRendered) => { + const answer = streamAnswers?.find( + (answer) => question.ref === answer.ref + ); + return ( + + + + {question?.question} + + + + {answer?.answer && } + handleDelete(question.ref)}> + + + + + + + {answer?.answer ? ( + + + + + ) : ( + + )} + + ); + })} + + ); +}; + +export default SearchPrototype; diff --git a/components/SearchPrototype/types/search-prototype.ts b/components/SearchPrototype/types/search-prototype.ts new file mode 100644 index 00000000..f9379fce --- /dev/null +++ b/components/SearchPrototype/types/search-prototype.ts @@ -0,0 +1,34 @@ +export type QuestionRendered = { + question: string; + ref: string; +}; + +export type Question = { + auth: string; + message: "chat"; + question: string; + ref: string; +}; + +export type SourceDocument = { + page_content: string; + metadata: { + [key: string]: any; + }; +}; + +export type Answer = { + answer: string; + isComplete: boolean; + question?: string; // revisit this + ref: string; + source_documents: Array; +}; + +export type StreamingMessage = { + answer?: string; + question?: string; + ref: string; + source_documents?: Array; + token?: string; +}; diff --git a/package-lock.json b/package-lock.json index 559fd58a..e55c5b34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3391,9 +3391,9 @@ "dev": true }, "node_modules/@samvera/clover-iiif": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/@samvera/clover-iiif/-/clover-iiif-2.8.3.tgz", - "integrity": "sha512-l4byTU6eAauASgoCTC+wo3hXvY3sa5tLkvTw3CvjL2pHg/aqE0f4EXIvj0SKmFBINP1rTiX9kq91tk4sNv6fpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@samvera/clover-iiif/-/clover-iiif-2.9.0.tgz", + "integrity": "sha512-Ccbq+trWgKy2cXjZ7qx/yJ+NNb2Ci0YDf+Hs5y2CJNRp7u0+mNHwOdSR65OKeX7nRtG95bhM6mTizolJ5nk0cg==", "dependencies": { "@iiif/parser": "^1.1.2", "@iiif/vault": "^0.9.22", @@ -4212,6 +4212,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -4320,6 +4321,21 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -6460,9 +6476,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.769", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.769.tgz", - "integrity": "sha512-bZu7p623NEA2rHTc9K1vykl57ektSPQYFFqQir8BOYf6EKOB+yIsbFB9Kpm7Cgt6tsLr9sRkqfqSZUw7LP1XxQ==" + "version": "1.4.770", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.770.tgz", + "integrity": "sha512-ONwOsDiVvV07CMsyH4+dEaZ9L79HMH/ODHnDS3GkIhgNqdDHJN2C18kFb0fBj0RXpQywsPJl6k2Pqg1IY4r1ig==" }, "node_modules/emittery": { "version": "0.13.1", @@ -6819,6 +6835,136 @@ } } }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.2.0", + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/typescript-estree": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz", + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz", + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "@typescript-eslint/visitor-keys": "7.2.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz", + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.2.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-next/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -7978,9 +8124,9 @@ } }, "node_modules/framer-motion": { - "version": "11.2.3", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.3.tgz", - "integrity": "sha512-SKp4jSyRKo5bUzbHp5f/TLiYLxUthh5SpO0MJ5RFtuHa9h4UZlSxQDe7ydmemj2SOYHXwJhJ8DNQ3ZRz+ydkuw==", + "version": "11.2.9", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.9.tgz", + "integrity": "sha512-gfxNSkp4dC3vpy2hGNQK3K9bNOKwfasqOhrqvmZzYxCPSJ9Tpv/9JlCkeCMgFdKefgPr8+JiouGjVmaDzu750w==", "dependencies": { "tslib": "^2.4.0" }, @@ -13025,9 +13171,9 @@ } }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -13159,6 +13305,18 @@ "react": ">=17.0.0" } }, + "node_modules/next/node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -16045,9 +16203,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", + "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16225,19 +16384,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/watchpack": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", - "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", - "peer": true, - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", diff --git a/package.json b/package.json index 88a2d7f7..e227c3f9 100644 --- a/package.json +++ b/package.json @@ -93,4 +93,4 @@ "styles" ] } -} +} \ No newline at end of file diff --git a/pages/_app.tsx b/pages/_app.tsx index 9468dcce..ccbbed80 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -106,4 +106,4 @@ function MyApp({ Component, pageProps }: MyAppProps) { ); } -export default MyApp; +export default MyApp; diff --git a/pages/index.tsx b/pages/index.tsx index f62e8beb..42a0d7df 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,55 +1,68 @@ -import { - HomepageCollections as Collections, - HomepageHero as Hero, - HomepageOverview as Overview, - HomepageWorks as Works, -} from "@/components/Homepage"; - -import Head from "next/head"; -import { HomeContextProvider } from "@/context/home-context"; -import Layout from "@/components/layout"; -import { PRODUCTION_URL } from "@/lib/constants/endpoints"; -import { buildDataLayer } from "@/lib/ga/data-layer"; -import { loadDefaultStructuredData } from "@/lib/json-ld"; +import { useEffect, useState } from "react"; + +import Container from "@/components/Shared/Container"; +import { DCAPI_ENDPOINT } from "@/lib/constants/endpoints"; +import Heading from "@/components/Heading/Heading"; +import SearchPrototype from "@/components/SearchPrototype"; +import axios from "axios"; +import { styled } from "@/stitches.config"; + +export type ChatConfig = { + auth: string; + endpoint: string; +}; const HomePage: React.FC = () => { + const [chatConfig, setChatConfig] = useState(); + + useEffect(() => { + axios({ + method: "GET", + url: `https://dcapi-prototype.rdc-staging.library.northwestern.edu/api/v2/chat-endpoint`, + withCredentials: true, + }) + .then((response) => { + console.log(`response`, response.data); + setChatConfig(response.data); + }) + .catch((error) => { + console.error(error); + }); + }, []); + return ( - <> - - {/* Google Structured Data via JSON-LD */} - - {mounted && } - - + + ); } -export default MyApp; +export default MyApp; diff --git a/pages/search.tsx b/pages/search.tsx index 954deaf6..96415ce2 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -45,7 +45,6 @@ const SearchPage: NextPage = () => { const { searchState } = useSearchState(); const { user } = React.useContext(UserContext); const showChatResponse = user?.isLoggedIn && searchState.isGenerativeAI; - console.log("showChatResponse", showChatResponse); const [requestState, setRequestState] = useState({ data: null, From 64f228914169ef76bd783f834dc6100f3133c98e Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 7 Mar 2024 10:07:51 -0600 Subject: [PATCH 09/57] Encode the URI generated when a user logs in by way of attempting to use generative AI --- hooks/useGenerativeAISearchToggle.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index 92d4faa2..af5c8036 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -32,8 +32,10 @@ export default function useGenerativeAISearchToggle() { function goToLocation() { const currentUrl = `${window.location.origin}${router.asPath}`; const url = new URL(currentUrl); + url.searchParams.set(aiQueryParam, "true"); - return url.toString(); + const encodedUri = encodeURIComponent(url.href); + return encodedUri; } function closeDialog() { From f045ec3383c800cda65aed872fad76d9d156c090 Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 29 Feb 2024 09:10:01 -0600 Subject: [PATCH 10/57] Upgrade dependencies including NextJS --- package-lock.json | 509 +++++++++++++++++++++------------------------- package.json | 10 +- 2 files changed, 237 insertions(+), 282 deletions(-) diff --git a/package-lock.json b/package-lock.json index eaccfcd5..8fe43d8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,7 @@ "axios": "^1.2.2", "framer-motion": "^11.2.3", "jsonwebtoken": "^9.0.0", - "next": "^13.5.6", + "next": "^14.1.0", "react": "^18.2.0", "react-content-loader": "^6.2.0", "react-dom": "^18.2.0", @@ -47,11 +47,11 @@ "@testing-library/react": "^15.0.2", "@testing-library/user-event": "^14.0.4", "@types/jest": "^29.4.0", - "@types/node": "^20.4.9", + "@types/node": "^20.11.23", "@types/openseadragon": "^3.0.4", - "@types/react": "^18.0.26", + "@types/react": "^18.2.61", "@types/react-sticky-el": "^1.0.3", - "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", "babel-jest": "^29.6.2", "cypress": "^12.4.0", "eslint": "^8.47.0", @@ -59,7 +59,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-testing-library": "^6.0.1", - "husky": "^8.0.3", + "husky": "^9.0.11", "jest": "^29.0.3", "jest-environment-jsdom": "^29.0.3", "next-router-mock": "^0.9.1-beta.0", @@ -1893,9 +1893,9 @@ } }, "node_modules/@next/env": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.6.tgz", - "integrity": "sha512-Yac/bV5sBGkkEXmAX5FWPS9Mmo2rthrOPRQQNfycJPkjUAUclomCPH7QFVCDQ4Mp2k2K1SSM6m0zrxYrOwtFQw==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "node_modules/@next/eslint-plugin-next": { "version": "14.2.3", @@ -1915,9 +1915,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.6.tgz", - "integrity": "sha512-5nvXMzKtZfvcu4BhtV0KH1oGv4XEW+B+jOfmBdpFI3C7FrB/MfujRpWYSBBO64+qbW8pkZiSyQv9eiwnn5VIQA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "cpu": [ "arm64" ], @@ -1929,6 +1929,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3400,11 +3520,17 @@ "react": ">= 16.3.0" } }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -4023,33 +4149,31 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", - "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", + "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/type-utils": "6.21.0", - "@typescript-eslint/utils": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4057,40 +4181,28 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/parser": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", - "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", + "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4099,16 +4211,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4116,25 +4228,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", - "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.21.0", - "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^8.56.0" }, "peerDependenciesMeta": { "typescript": { @@ -4143,12 +4255,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", "dev": true, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4156,22 +4268,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -4183,21 +4295,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -4211,53 +4308,38 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "eslint": "^8.56.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^18.18.0 || >=20.0.0" }, "funding": { "type": "opencollective", @@ -8154,7 +8236,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "peer": true }, "node_modules/global-dirs": { "version": "3.0.1", @@ -8440,15 +8523,15 @@ } }, "node_modules/husky": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", - "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", "dev": true, "bin": { - "husky": "lib/bin.js" + "husky": "bin.mjs" }, "engines": { - "node": ">=14" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/typicode" @@ -12130,37 +12213,38 @@ "peer": true }, "node_modules/next": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/next/-/next-13.5.6.tgz", - "integrity": "sha512-Y2wTcTbO4WwEsVb4A8VSnOsG1I9ok+h74q0ZdxkwM3EODqrs4pasq7O0iUxbcS9VtWMicG7f3+HAj0r1+NtKSw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "dependencies": { - "@next/env": "13.5.6", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", - "caniuse-lite": "^1.0.30001406", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1", - "watchpack": "2.4.0" + "styled-jsx": "5.1.1" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=16.14.0" + "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "13.5.6", - "@next/swc-darwin-x64": "13.5.6", - "@next/swc-linux-arm64-gnu": "13.5.6", - "@next/swc-linux-arm64-musl": "13.5.6", - "@next/swc-linux-x64-gnu": "13.5.6", - "@next/swc-linux-x64-musl": "13.5.6", - "@next/swc-win32-arm64-msvc": "13.5.6", - "@next/swc-win32-ia32-msvc": "13.5.6", - "@next/swc-win32-x64-msvc": "13.5.6" + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -12169,6 +12253,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -12184,18 +12271,6 @@ "react": ">=17.0.0" } }, - "node_modules/next/node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -15376,126 +15451,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.6.tgz", - "integrity": "sha512-6cgBfxg98oOCSr4BckWjLLgiVwlL3vlLj8hXg2b+nDgm4bC/qVXXLfpLB9FHdoDu4057hzywbxKvmYGmi7yUzA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.6.tgz", - "integrity": "sha512-txagBbj1e1w47YQjcKgSU4rRVQ7uF29YpnlHV5xuVUsgCUf2FmyfJ3CPjZUvpIeXCJAoMCFAoGnbtX86BK7+sg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.6.tgz", - "integrity": "sha512-cGd+H8amifT86ZldVJtAKDxUqeFyLWW+v2NlBULnLAdWsiuuN8TuhVBt8ZNpCqcAuoruoSWynvMWixTFcroq+Q==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.6.tgz", - "integrity": "sha512-Mc2b4xiIWKXIhBy2NBTwOxGD3nHLmq4keFk+d4/WL5fMsB8XdJRdtUlL87SqVCTSaf1BRuQQf1HvXZcy+rq3Nw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.6.tgz", - "integrity": "sha512-CFHvP9Qz98NruJiUnCe61O6GveKKHpJLloXbDSWRhqhkJdZD2zU5hG+gtVJR//tyW897izuHpM6Gtf6+sNgJPQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.6.tgz", - "integrity": "sha512-aFv1ejfkbS7PUa1qVPwzDHjQWQtknzAZWGTKYIAaS4NMtBlk3VyA6AYn593pqNanlicewqyl2jUhQAaFV/qXsg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.6.tgz", - "integrity": "sha512-XqqpHgEIlBHvzwG8sp/JXMFkLAfGLqkbVsyN+/Ih1mR8INb6YCc2x/Mbwi6hsAgUnqQztz8cvEbHJUbSl7RHDg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "13.5.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.6.tgz", - "integrity": "sha512-Cqfe1YmOS7k+5mGu92nl5ULkzpKuxJrP3+4AEuPmrpFZ3BHxTY3TnHmU1On3bFmFFs6FbTcdF58CCUProGpIGQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index 250ce321..ea71ac76 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "axios": "^1.2.2", "framer-motion": "^11.2.3", "jsonwebtoken": "^9.0.0", - "next": "^13.5.6", + "next": "^14.1.0", "react": "^18.2.0", "react-content-loader": "^6.2.0", "react-dom": "^18.2.0", @@ -58,11 +58,11 @@ "@testing-library/react": "^15.0.2", "@testing-library/user-event": "^14.0.4", "@types/jest": "^29.4.0", - "@types/node": "^20.4.9", + "@types/node": "^20.11.23", "@types/openseadragon": "^3.0.4", - "@types/react": "^18.0.26", + "@types/react": "^18.2.61", "@types/react-sticky-el": "^1.0.3", - "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/eslint-plugin": "^7.1.0", "babel-jest": "^29.6.2", "cypress": "^12.4.0", "eslint": "^8.47.0", @@ -70,7 +70,7 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-testing-library": "^6.0.1", - "husky": "^8.0.3", + "husky": "^9.0.11", "jest": "^29.0.3", "jest-environment-jsdom": "^29.0.3", "next-router-mock": "^0.9.1-beta.0", From da5249e268cff4b62716ba12c49f50e18625b79e Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 29 Feb 2024 10:09:26 -0600 Subject: [PATCH 11/57] Force npm install for Github Action test --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 75d4a993..3f9a6539 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -26,7 +26,7 @@ jobs: node-version: "lts/*" - name: Install dependencies - run: npm ci + run: npm ci --force - name: Run tests run: npm run test:ci From 9b269a2909d7a3bfc188cb3b4a4f6fc4c7a301ca Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 29 Feb 2024 13:15:42 -0600 Subject: [PATCH 12/57] Update Amplify and Github Actions build dependencies --- .github/workflows/run-tests.yml | 2 +- terraform/main.tf | 1 + terraform/variables.tf | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 3f9a6539..3e81b7f6 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 + uses: styfle/cancel-workflow-action@0.12.1 - name: Get files uses: actions/checkout@v3 diff --git a/terraform/main.tf b/terraform/main.tf index 51c5f8ce..f3c96710 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -74,6 +74,7 @@ resource "aws_amplify_app" "dc-next" { } environment_variables = { + _CUSTOM_IMAGE = var.amplify_build_custom_image ENV = var.environment_name HONEYBADGER_API_KEY = var.honeybadger_api_key HONEYBADGER_ENV = var.environment_name diff --git a/terraform/variables.tf b/terraform/variables.tf index 0d2358ce..309be582 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -3,6 +3,11 @@ variable "access_token" { type = string } +variable "amplify_build_custom_image" { + type = string + default = "amplify:al2023" +} + variable "app_username" { type = string default = "" From 4cb006852f4d19e4893bda04253980d465fcdbfd Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 7 Mar 2024 14:08:13 -0600 Subject: [PATCH 13/57] Clean up TS errors --- .../components/Answer/SourceDocuments.tsx | 3 +- components/Search/newFile.tsx | 73 ------------------- 2 files changed, 2 insertions(+), 74 deletions(-) delete mode 100644 components/Search/newFile.tsx diff --git a/components/Chat/components/Answer/SourceDocuments.tsx b/components/Chat/components/Answer/SourceDocuments.tsx index 8fb7a027..2589c381 100644 --- a/components/Chat/components/Answer/SourceDocuments.tsx +++ b/components/Chat/components/Answer/SourceDocuments.tsx @@ -11,10 +11,11 @@ const SourceDocuments: React.FC = ({ }) => { return ( + {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} {source_documents.map((document, idx) => (
{document.title} - +
// ))} diff --git a/components/Search/newFile.tsx b/components/Search/newFile.tsx deleted file mode 100644 index d78d2a27..00000000 --- a/components/Search/newFile.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - defaultUser, - withSearchProvider, - withUserProvider, -} from "./GenerativeAIToggle.test"; -import { render, screen } from "@testing-library/react"; - -import GenerativeAIToggle from "./GenerativeAIToggle"; -import React from "react"; -import userEvent from "@testing-library/user-event"; - -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() - ) - ); - - 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()); - // 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(), - nonLoggedInUser - ) - ); - - const checkbox = screen.getByRole("checkbox"); - await user.click(checkbox); - - const generativeAIDialog = screen.getByText( - "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(); - - user.click(cancelButton); - - expect; - }); - - it("renders a toggled generative ai state when a query param is set", () => {}); -}); From 8f563f22847c7177d08766ff6208385b364cb50f Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 7 Mar 2024 15:14:55 -0600 Subject: [PATCH 14/57] Update chat API endpoint to use ENV value like other endpoints. Based on backend work to support this. --- hooks/useChatSocket.ts | 3 --- hooks/useGenerativeAISearchToggle.ts | 3 --- lib/constants/endpoints.ts | 13 ++++++++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/hooks/useChatSocket.ts b/hooks/useChatSocket.ts index dcbefdf7..25184f6d 100644 --- a/hooks/useChatSocket.ts +++ b/hooks/useChatSocket.ts @@ -3,9 +3,6 @@ import { useEffect, useState } from "react"; import { DCAPI_CHAT_ENDPOINT } from "@/lib/constants/endpoints"; import axios from "axios"; -// URL which the browser seems to have to hit to authenticate with the chat endpoint -// https://api.dc.library.northwestern.edu/api/v2/auth/login?goto=https://api.dc.library.northwestern.edu/api/v2/chat-endpoint - const useChatSocket = () => { const [chatSocket, setChatSocket] = useState(null); const [authToken, setAuthToken] = useState(null); diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index af5c8036..0f8a4126 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -26,9 +26,6 @@ export default function useGenerativeAISearchToggle() { const loginUrl = `${DCAPI_ENDPOINT}/auth/login?goto=${goToLocation()}`; - // TODO: This needs to accept more than one query param when - // directing to NU SSO. We need the additional query param - // to know that user came wanting to use Generative AI function goToLocation() { const currentUrl = `${window.location.origin}${router.asPath}`; const url = new URL(currentUrl); diff --git a/lib/constants/endpoints.ts b/lib/constants/endpoints.ts index aebbc562..293cc512 100644 --- a/lib/constants/endpoints.ts +++ b/lib/constants/endpoints.ts @@ -1,14 +1,17 @@ -const DCAPI_CHAT_ENDPOINT = - "https://api.dc.library.northwestern.edu/api/v2/chat-endpoint"; -const DCAPI_ENDPOINT = process.env.NEXT_PUBLIC_DCAPI_ENDPOINT; const DCAPI_PRODUCTION_ENDPOINT = "https://api.dc.library.northwestern.edu/api/v2"; -const DC_API_SEARCH_URL = `${DCAPI_ENDPOINT}/search`; -const DC_URL = process.env.NEXT_PUBLIC_DC_URL; + const IIIF_IMAGE_SERVICE_ENDPOINT = "https://iiif.stack.rdc.library.northwestern.edu/iiif/2"; + const PRODUCTION_URL = "https://digitalcollections.library.northwestern.edu"; +const DC_URL = process.env.NEXT_PUBLIC_DC_URL; +const DCAPI_ENDPOINT = process.env.NEXT_PUBLIC_DCAPI_ENDPOINT; + +const DC_API_SEARCH_URL = `${DCAPI_ENDPOINT}/search`; +const DCAPI_CHAT_ENDPOINT = `${DCAPI_ENDPOINT}/chat-endpoint`; + export { DC_URL, DCAPI_CHAT_ENDPOINT, From 7d22cd8cccd2e5df11639c9e8bac792e483541a4 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Fri, 8 Mar 2024 14:07:15 -0500 Subject: [PATCH 15/57] Design streaming chat response. --- components/Chat/{index.tsx => Chat.tsx} | 27 +- components/Chat/Response/Images.test.tsx | 26 + components/Chat/Response/Images.tsx | 29 + components/Chat/Response/Response.styled.tsx | 123 +++ components/Chat/Response/Response.tsx | 55 + components/Chat/Response/StreamedAnswer.tsx | 20 + components/Chat/{components => }/Wrapper.tsx | 8 +- .../Chat/components/Answer/Answer.styled.tsx | 93 -- components/Chat/components/Answer/Card.tsx | 84 -- .../Chat/components/Answer/Certainity.tsx | 28 - .../Chat/components/Answer/Information.tsx | 97 -- .../components/Answer/SourceDocuments.tsx | 33 - .../components/Answer/StreamingAnswer.tsx | 52 - .../Chat/components/Feedback/Prompt.tsx | 30 - components/Chat/components/History/Dialog.tsx | 7 - components/Chat/components/Question/Input.tsx | 83 -- components/Chat/components/SVG/History.tsx | 16 - components/Chat/fixtures/mock-answer.ts | 236 ----- components/Chat/hooks/useEventCallback.ts | 17 - components/Chat/hooks/useEventListener.ts | 82 -- .../Chat/hooks/useIsomorphicLayoutEffect.ts | 4 - components/Chat/hooks/useLocalStorage.ts | 105 -- .../Chat/hooks/useLocalStorageSimple.tsx | 19 - components/Search/GenerativeAIToggle.tsx | 3 +- .../Loader.js => Shared/BouncingLoader.tsx} | 14 +- hooks/useMarkdown.tsx | 53 + lib/chat-helpers.ts | 2 +- package-lock.json | 998 +++++++++++++++++- package.json | 7 +- pages/search.tsx | 2 +- .../Chat/types => types/components}/chat.ts | 0 31 files changed, 1325 insertions(+), 1028 deletions(-) rename components/Chat/{index.tsx => Chat.tsx} (80%) create mode 100644 components/Chat/Response/Images.test.tsx create mode 100644 components/Chat/Response/Images.tsx create mode 100644 components/Chat/Response/Response.styled.tsx create mode 100644 components/Chat/Response/Response.tsx create mode 100644 components/Chat/Response/StreamedAnswer.tsx rename components/Chat/{components => }/Wrapper.tsx (54%) delete mode 100644 components/Chat/components/Answer/Answer.styled.tsx delete mode 100644 components/Chat/components/Answer/Card.tsx delete mode 100644 components/Chat/components/Answer/Certainity.tsx delete mode 100644 components/Chat/components/Answer/Information.tsx delete mode 100644 components/Chat/components/Answer/SourceDocuments.tsx delete mode 100644 components/Chat/components/Answer/StreamingAnswer.tsx delete mode 100644 components/Chat/components/Feedback/Prompt.tsx delete mode 100644 components/Chat/components/History/Dialog.tsx delete mode 100644 components/Chat/components/Question/Input.tsx delete mode 100644 components/Chat/components/SVG/History.tsx delete mode 100644 components/Chat/fixtures/mock-answer.ts delete mode 100644 components/Chat/hooks/useEventCallback.ts delete mode 100644 components/Chat/hooks/useEventListener.ts delete mode 100644 components/Chat/hooks/useIsomorphicLayoutEffect.ts delete mode 100644 components/Chat/hooks/useLocalStorage.ts delete mode 100644 components/Chat/hooks/useLocalStorageSimple.tsx rename components/{Chat/components/Answer/Loader.js => Shared/BouncingLoader.tsx} (75%) create mode 100644 hooks/useMarkdown.tsx rename {components/Chat/types => types/components}/chat.ts (100%) diff --git a/components/Chat/index.tsx b/components/Chat/Chat.tsx similarity index 80% rename from components/Chat/index.tsx rename to components/Chat/Chat.tsx index 52a6d7c2..9f199d97 100644 --- a/components/Chat/index.tsx +++ b/components/Chat/Chat.tsx @@ -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, @@ -66,19 +65,15 @@ const Chat = ({ }; }, [chatSocket, chatSocket?.url]); + if (!question) return null; + return ( - <> -

{question}

- {question && ( - <> - - - - )} - + ); }; diff --git a/components/Chat/Response/Images.test.tsx b/components/Chat/Response/Images.test.tsx new file mode 100644 index 00000000..a7123d63 --- /dev/null +++ b/components/Chat/Response/Images.test.tsx @@ -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(); + + 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 } + ); + }); + }); +}); diff --git a/components/Chat/Response/Images.tsx b/components/Chat/Response/Images.tsx new file mode 100644 index 00000000..8a1d1a5f --- /dev/null +++ b/components/Chat/Response/Images.tsx @@ -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 ( + + {sourceDocuments.slice(0, nextIndex).map((document: Work) => ( + + ))} + + ); +}; + +export default ResponseImages; diff --git a/components/Chat/Response/Response.styled.tsx b/components/Chat/Response/Response.styled.tsx new file mode 100644 index 00000000..8493dd04 --- /dev/null +++ b/components/Chat/Response/Response.styled.tsx @@ -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, +}; diff --git a/components/Chat/Response/Response.tsx b/components/Chat/Response/Response.tsx new file mode 100644 index 00000000..3ee18d9f --- /dev/null +++ b/components/Chat/Response/Response.tsx @@ -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 = ({ + isStreamingComplete, + question, + sourceDocuments, + streamedAnswer, +}) => { + return ( + + + + + {question} + {streamedAnswer ? ( + + ) : ( + + )} + + {sourceDocuments.length > 0 && ( + + + + )} + + + + ); +}; + +export default ChatResponse; diff --git a/components/Chat/Response/StreamedAnswer.tsx b/components/Chat/Response/StreamedAnswer.tsx new file mode 100644 index 00000000..91411c12 --- /dev/null +++ b/components/Chat/Response/StreamedAnswer.tsx @@ -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 {content}; +}; + +export default ResponseStreamedAnswer; diff --git a/components/Chat/components/Wrapper.tsx b/components/Chat/Wrapper.tsx similarity index 54% rename from components/Chat/components/Wrapper.tsx rename to components/Chat/Wrapper.tsx index 6b2c81e0..f579a8e5 100644 --- a/components/Chat/components/Wrapper.tsx +++ b/components/Chat/Wrapper.tsx @@ -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 = () => { @@ -9,9 +9,7 @@ const ChatWrapper = () => { if (!authToken || !chatSocket || !question) return null; return ( -
- -
+ ); }; diff --git a/components/Chat/components/Answer/Answer.styled.tsx b/components/Chat/components/Answer/Answer.styled.tsx deleted file mode 100644 index 7de21fba..00000000 --- a/components/Chat/components/Answer/Answer.styled.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import * as Accordion from "@radix-ui/react-accordion"; - -import { AnswerTooltip } from "./Information"; -import { styled } from "@/stitches.config"; - -/* eslint sort-keys: 0 */ - -const StyledActions = styled("div", { - display: "flex", - paddingLeft: "$gr5", -}); - -const StyledRemoveButton = styled("button", { - background: "transparent", - border: "none", - cursor: "pointer", - fontFamily: "$northwesternSansRegular", - fontSize: "$gr2", - padding: "0", - - svg: { - fill: "$black50 !important", - transition: "opacity 0.2s ease-in-out", - }, - - "&:active, &:hover": { - svg: { - opacity: "1 !important", - fill: "$brightRed !important", - }, - }, -}); - -const StyledAnswerHeader = styled(Accordion.Header, { - margin: "$gr2 0", - display: "flex", - justifyContent: "space-between", - alignItems: "flex-start", - - button: { - background: "transparent !important", - border: "none", - cursor: "pointer", - margin: "0", - padding: "0", - color: "$black !important", - fontSize: "$gr5 !important", - fontFamily: "$northwesternSansBold !important", - textAlign: "left", - - "&:hover": { - color: "$brightBlueB !important", - }, - }, -}); - -const StyledAnswerItem = styled(Accordion.Item, { - "&::after": { - content: "", - display: "block", - height: "1px", - margin: "$gr3 0", - width: "100%", - backgroundColor: "$gray6", - }, - - [`&[data-state=closed] ${StyledAnswerHeader}`]: { - [`button`]: { - fontFamily: "$northwesternSansRegular !important", - color: "$black50 !important", - - "&:hover": { - color: "$brightBlueB !important", - }, - }, - - [`& ${AnswerTooltip}`]: { - display: "none", - cursor: "default", - }, - }, - - "&:hover button svg": { - opacity: "1", - }, -}); - -export { - StyledActions, - StyledAnswerHeader, - StyledAnswerItem, - StyledRemoveButton, -}; diff --git a/components/Chat/components/Answer/Card.tsx b/components/Chat/components/Answer/Card.tsx deleted file mode 100644 index 3015ca76..00000000 --- a/components/Chat/components/Answer/Card.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import AnswerCertainty from "./Certainity"; -import { DCAPI_PRODUCTION_ENDPOINT } from "@/lib/constants/endpoints"; -import Image from "next/image"; -import Link from "next/link"; -import React from "react"; -import { styled } from "@/stitches.config"; - -export interface AnswerCardProps { - metadata: any; - page_content: string; -} - -const AnswerCard: React.FC = ({ metadata, page_content }) => { - const { _additional, source, work_type } = metadata; - const dcLink = `https://dc.library.northwestern.edu/items/${source}`; - const thumbnail = `${DCAPI_PRODUCTION_ENDPOINT}/works/${source}/thumbnail?aspect=square`; - - return ( - -
- - {page_content} - {_additional?.certainty && ( - - )} - - -
- {page_content} - {work_type} -
-
-
-
- ); -}; - -/* eslint sort-keys: 0 */ - -const ImageWrapper = styled("div", { - backgroundColor: "$gray6", - borderRadius: "5px", - height: "$gr8", - width: "$gr8", - position: "relative", - - img: { - color: "transparent", - borderRadius: "6px", - }, -}); - -const Context = styled("div", { - padding: "$gr2 0", - display: "flex", - justifyContent: "space-between", -}); - -const StyledAnswerCard = styled(Link, { - figcaption: { - width: "$gr8", - span: { - color: "$black50 !important", - display: "block", - fontSize: "$gr2", - whiteSpace: "wrap", - }, - - strong: { - color: "$black", - display: "block", - fontFamily: "$northwesternSansBold", - fontWeight: "400", - marginBottom: "3px", - }, - }, - - figure: { - margin: 0, - padding: 0, - }, -}); - -export default AnswerCard; diff --git a/components/Chat/components/Answer/Certainity.tsx b/components/Chat/components/Answer/Certainity.tsx deleted file mode 100644 index 089f67fd..00000000 --- a/components/Chat/components/Answer/Certainity.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { styled } from "@/stitches.config"; - -const AnswerCertainty = ({ amount }: { amount: number }) => { - return ( - {Math.floor(amount * 100)}% - ); -}; - -/* eslint sort-keys: 0 */ - -const StyledAnswerCertainty = styled("span", { - backgroundColor: "$white", - color: "$brightBlueB", - display: "flex", - fontFamily: "$northwesternSerifBold", - fontSize: "$gr2", - lineHeight: "1", - padding: "$gr1", - position: "absolute", - borderTopLeftRadius: "5px", - borderBottomRightRadius: "5px", - border: "1px solid $gray6", - right: "0", - bottom: "0", -}); - -export default AnswerCertainty; diff --git a/components/Chat/components/Answer/Information.tsx b/components/Chat/components/Answer/Information.tsx deleted file mode 100644 index ab13f902..00000000 --- a/components/Chat/components/Answer/Information.tsx +++ /dev/null @@ -1,97 +0,0 @@ -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 = ({ - timestamp, -}) => { - const date = new Date(timestamp); - const timeZone = Intl.DateTimeFormat()?.resolvedOptions()?.timeZone; - const answerDate = date.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - timeZoneName: "short", - timeZone: timeZone ? timeZone : "America/Chicago", - }); - - return ( - - - - - About this Answer - - - - - - {generativeAIWarning} - Answered on {answerDate} - - - - - - - ); -}; - -/* eslint sort-keys: 0 */ - -export const AnswerTooltip = styled("span", { - display: "inline-block", - background: "transparent", - border: "none", - cursor: "help", - fontFamily: "$northwesternSansRegular", - fontSize: "$gr2 !important", - padding: "1px 0", - margin: "0 $gr2 0 0", - textDecoration: "none", - borderBottom: "1px dotted $black20", - lineHeight: "1em", - alignSelf: "center", - color: "$black50", - whiteSpace: "nowrap", - opacity: "1", - - "&:active, &:hover": { - color: "$brightBlueB !important", - borderColor: "$brightBlueB", - }, -}); - -export const AnswerTooltipArrow = styled(Tooltip.Arrow, { - fill: "$brightBlueB", -}); - -export const AnswerTooltipContent = styled("div", { - background: "$white", - boxShadow: "0 13px 21px 0 rgba(0, 0, 0, 0.13)", - width: "450px", - lineHeight: "1.5em", - fontSize: "$gr2 !important", - fontFamily: "$northwesternSansRegular", - padding: "$gr3", - borderRadius: "6px", - borderTop: "2px solid $brightBlueB", - - em: { - color: "$black50", - marginTop: "$gr1", - display: "block", - fontSize: "$gr1", - }, -}); - -export default AnswerInformation; diff --git a/components/Chat/components/Answer/SourceDocuments.tsx b/components/Chat/components/Answer/SourceDocuments.tsx deleted file mode 100644 index 2589c381..00000000 --- a/components/Chat/components/Answer/SourceDocuments.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from "react"; -import { Work } from "@nulib/dcapi-types"; -import { styled } from "@/stitches.config"; - -interface SourceDocumentsProps { - source_documents: Work[]; -} - -const SourceDocuments: React.FC = ({ - source_documents, -}) => { - return ( - - {/* eslint-disable-next-line @typescript-eslint/no-unused-vars */} - {source_documents.map((document, idx) => ( -
- {document.title} - -
- // - ))} -
- ); -}; - -const Sources = styled("div", { - display: "flex", - gap: "$gr4", - overflowX: "scroll", - padding: "$gr3 0 0", -}); - -export default SourceDocuments; diff --git a/components/Chat/components/Answer/StreamingAnswer.tsx b/components/Chat/components/Answer/StreamingAnswer.tsx deleted file mode 100644 index 65361f3b..00000000 --- a/components/Chat/components/Answer/StreamingAnswer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React, { useRef } from "react"; - -import { keyframes } from "@/stitches.config"; -import { styled } from "@stitches/react"; - -const StreamingAnswer = ({ - answer, - isComplete, -}: { - answer: string; - isComplete: boolean; -}) => { - const answerElement = useRef(null); - - return ( - - {answer} - {!isComplete && } - - ); -}; - -/* eslint sort-keys: 0 */ - -const Answer = styled("article", { - fontSize: "$gr3", - fontFamily: "$northwesternSerifRegular !important", - lineHeight: "1.76em", - margin: "$gr3 0 $gr2", -}); - -const Blinker = keyframes({ - "50%": { - opacity: 0, - }, -}); - -const Cursor = styled("span", { - position: "relative", - marginLeft: "$gr1", - - "&::before": { - content: '""', - position: "absolute", - width: "10px", - height: "1.4em", - backgroundColor: "$black20", - animation: `${Blinker} 1s linear infinite`, - }, -}); - -export default StreamingAnswer; diff --git a/components/Chat/components/Feedback/Prompt.tsx b/components/Chat/components/Feedback/Prompt.tsx deleted file mode 100644 index b2cd388a..00000000 --- a/components/Chat/components/Feedback/Prompt.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React from "react"; -import { styled } from "@/stitches.config"; - -const FeedbackPrompt = () => { - return ( - -

- Do you have questions or feedback regarding these results?{" "} - Let us know. -

-
- ); -}; - -/* eslint sort-keys: 0 */ - -const StyledFeedbackPrompt = styled("div", { - fontSize: "$gr2", - fontFamily: "$northwesternSansRegular !important", - textAlign: "center", - color: "$black80", - padding: "$gr3 0", - - "a, a:visited": { - textDecoration: "underline", - color: "$black80", - }, -}); - -export default FeedbackPrompt; diff --git a/components/Chat/components/History/Dialog.tsx b/components/Chat/components/History/Dialog.tsx deleted file mode 100644 index ba2acfc4..00000000 --- a/components/Chat/components/History/Dialog.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -const HistoryDialog = () => { - return <>; -}; - -export default HistoryDialog; diff --git a/components/Chat/components/Question/Input.tsx b/components/Chat/components/Question/Input.tsx deleted file mode 100644 index 7fc26ab8..00000000 --- a/components/Chat/components/Question/Input.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from "react"; - -import { Icon } from "@nulib/design-system"; -import { IconArrowForward } from "@/components/Shared/SVG/Icons"; -import { styled } from "@stitches/react"; - -const QuestionInput = ({ - onQuestionSubmission, -}: { - onQuestionSubmission: (question: string) => void; -}) => { - const [question, setQuestion] = useState(""); - - const handleInputChange = (event: React.ChangeEvent) => { - setQuestion(event.target.value); - }; - - const handleQuestionSubmission = ( - event: React.FormEvent - ) => { - event.preventDefault(); - event.stopPropagation(); - onQuestionSubmission(question); - - // @ts-ignore - event.target.reset(); - }; - - return ( - - - - - ); -}; - -/* eslint sort-keys: 0 */ - -const StyledQuestionInput = styled("form", { - backgroundColor: "$white", - display: "flex", - position: "relative", - border: "1px solid $gray6", - borderBottom: "none", - boxShadow: "0 13px 21px 0 rgba(0, 0, 0, 0.13)", - margin: "0 0 $gr4", - borderRadius: "8px", - - svg: { - height: "$gr3", - color: "$brightBlueB", - }, - - input: { - flexGrow: 1, - outlineColor: "$brightBlueB", - }, - - button: { - position: "absolute", - right: "0", - cursor: "pointer", - fontFamily: "$northwesternSansBold !important", - }, - - "input, button": { - background: "transparent", - border: "none", - color: "$black80", - fontFamily: "$northwesternSansRegular", - fontSize: "$gr4", - padding: "$gr3 $gr4", - }, -}); - -export default QuestionInput; diff --git a/components/Chat/components/SVG/History.tsx b/components/Chat/components/SVG/History.tsx deleted file mode 100644 index 3e538b5d..00000000 --- a/components/Chat/components/SVG/History.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from "react"; - -const SVGHistory = () => { - return ( - <> - - - - - - - - ); -}; - -export default SVGHistory; diff --git a/components/Chat/fixtures/mock-answer.ts b/components/Chat/fixtures/mock-answer.ts deleted file mode 100644 index 4ddb5f45..00000000 --- a/components/Chat/fixtures/mock-answer.ts +++ /dev/null @@ -1,236 +0,0 @@ -// @ts-ignore - -interface MockAnswerShape { - answer: string; - question: string; - sources: any; - source_documents: any; -} - -/* eslint sort-keys: 0 */ - -const mockAnswer: MockAnswerShape = { - answer: - "There are no videos of Northwestern University playing against Michigan State. \n", - question: - "Are there any videos of Northwestern University playing against Michigan State?", - source_documents: [ - { - page_content: - "Northwestern vs. Michigan/Northwestern vs. Minnesota/Northwestern vs. Michigan State", - _additional: { - certainty: 0.8492068648338318, - }, - metadata: { - alternate_title: null, - contributor: [ - "Paynter, John P.", - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Northwestern University (Evanston, Ill.)", - "Friedmann, Pete", - "Lonis, Dale", - "Williams, Clifton, 1923-1976", - "Paynter, John P.", - "Jones, Quincy, 1933-", - "Owens, Don (Don D.)", - ], - create_date: "2022-02-21T22:16:02.505726Z", - creator: null, - date_created: [ - "October 23, 1982", - "October 9, 1982", - "November 6, 1982", - ], - description: [ - 'Three performances by the Northwestern University Marching Band during the 1982 season. First, the Northwestern University Marching Band performs at the Northwestern Wildcats\' game against the Michigan Wolverines at Dyche Stadium in Evanston, Illinois. Conducted by Dale Lonis, pregame performances include the National Anthem, "America the Beautiful," "42nd Street," and "Go U Northwestern." Halftime performances include "New York, New York," "Give My Regards to Broadway," "On Broadway," "Somewhere," and "Alma Mater. Postgame performances include "Hail to the Victors," "Go U Northwestern," "Sinfonians," "42nd Street," and "Alma Mater." Footage of the end of the Northwestern Wildcats\' game against the Minnesota Gophers at Dyche Stadium in Evanston, Illinois follow. The Northwestern University Marching Band then performs a postgame show including "Go U Northwestern" and "Minnesota Rouser." The NUMB alums join the band to perform "When the Saints Go Marching In." The final performances are from a Northwestern Wildcats game against the Michigan State Spartans in East Lansing, Michigan. Pregame selections from Northwestern include "MSU Fight Song" and "Go U Northwestern. The Spartan Marching Band performs "MSU Fight Song," "Only Time Will Tell," "Go U Northwestern," "MSU Shadows," and "God Bless America." John P. Paynter then conducts the National Anthem. Northwestern\'s halftime performance was space-themed and included "Space Invaders" and "Go U Northwestern." Local high school bands join the Spartan Marching Band for a halftime performance that includes "Even the Nights Are Better," "Ai No Corrida," "Through the Years," and the "MSU Fight Song." Scenes from a Northwestern Marching Band practice at Dyche Stadium follow the game against Michigan State.', - ], - genre: ["VHS"], - identifier: "7746ddbb-eb5b-4177-b4aa-c65de6eba2e9", - keywords: null, - language: ["English"], - location: null, - physical_description_material: null, - physical_description_size: null, - scope_and_contents: null, - source: - "https://dc.library.northwestern.edu/items/7746ddbb-eb5b-4177-b4aa-c65de6eba2e9", - style_period: null, - subject: [ - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Music", - "Marching bands", - "Conductors (Music)", - "Band directors", - "University of Michigan", - "Michigan Wolverines (Football team)", - "Michigan State University", - "Michigan State Spartans (Football team)", - "Michigan--East Lansing", - "Illinois--Evanston", - "Ryan Field (Evanston, Ill.)", - "Northwestern University (Evanston, Ill.)", - "Northwestern Wildcats (Football team)", - "Michigan State University. Spartan Marching Band", - ], - table_of_contents: null, - technique: null, - work_type: "Video", - }, - }, - { - page_content: "Northwestern University Marching Band Highlights", - _additional: { - certainty: 0.8492068648338318, - }, - metadata: { - alternate_title: null, - contributor: [ - "Paynter, John P.", - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Northwestern University (Evanston, Ill.)", - "Paynter, John P.", - ], - create_date: "2022-01-25T17:21:28.787425Z", - creator: null, - date_created: ["1958"], - description: [ - "The Northwestern University Marching Band, conducted by John P. Paynter, performs pregame and halftime selections during Northwestern University's football game against Purdue University. The Northwestern University vs. Purdue game was broadcast on WNBQ. Later footage is of the Northwestern University Marching Band's performance during Northwestern University's football against Stanford University.", - ], - genre: ["16mm (photographic film size)"], - identifier: "ed9b77e2-7928-47c0-b12b-2f3fc0cff3bc", - keywords: null, - language: null, - location: null, - physical_description_material: null, - physical_description_size: ["16mm"], - scope_and_contents: null, - source: - "https://dc.library.northwestern.edu/items/ed9b77e2-7928-47c0-b12b-2f3fc0cff3bc", - style_period: null, - subject: [ - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Music", - "Marching bands", - "Conductors (Music)", - "Band directors", - "Purdue Boilermakers (Football team)", - "Purdue University", - "Ryan Field (Evanston, Ill.)", - "Northwestern Wildcats (Football team)", - "Northwestern University (Evanston, Ill.)", - "Stanford University", - "Stanford Cardinal (Football team)", - ], - table_of_contents: null, - technique: null, - work_type: "Video", - }, - }, - { - page_content: "Northwestern University vs. Illinois", - _additional: { - certainty: 0.8492068648338318, - }, - metadata: { - alternate_title: null, - contributor: [ - "Paynter, John P.", - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Northwestern University (Evanston, Ill.)", - "Paynter, John P.", - ], - create_date: "2022-01-25T17:21:16.866290Z", - creator: null, - date_created: ["1957"], - description: [ - "The Northwestern University Marching Band, conducted by John P. Paynter, performs during Northwestern University's football game against the University of Illinois at Urbana-Champaign, Illinois. Footage also includes baton twirling by the brother and sister act of Roger and Barbara Kurucz. Also included are scenes of the NU Marching Band boarding buses in Evanston, traveling to Urbana-Champaign, and practicing for the performance. Selections from another game are included after the Northwestern University vs. Illinois game.", - ], - genre: ["16mm (photographic film size)"], - identifier: "5e741755-673e-477f-b4e6-16acf1f4cbe8", - keywords: null, - language: null, - location: null, - physical_description_material: null, - physical_description_size: ["16mm"], - scope_and_contents: null, - source: - "https://dc.library.northwestern.edu/items/5e741755-673e-477f-b4e6-16acf1f4cbe8", - style_period: null, - subject: [ - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Music", - "Marching bands", - "Conductors (Music)", - "Band directors", - "Fighting Illini (Football team)", - "University of Illinois at Urbana-Champaign", - "Illinois--Champaign", - "Northwestern Wildcats (Football team)", - "Northwestern University (Evanston, Ill.)", - ], - table_of_contents: null, - technique: null, - work_type: "Video", - }, - }, - { - page_content: "Northwestern vs. Illinois", - _additional: { - certainty: 0.8492068648338318, - }, - metadata: { - alternate_title: null, - contributor: [ - "Paynter, John P.", - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Northwestern University (Evanston, Ill.)", - "Friedmann, Pete", - ], - create_date: "2022-02-09T14:59:42.744821Z", - creator: null, - date_created: ["November 21, 1981"], - description: [ - 'Northwestern University Marching Band performs at the Northwestern Wildcats\' game against the Fighting Illini at Dyche Stadium in Evanston, Illinois. The pregame performances include "America the Beautiful," the National Anthem, "Illinois Loyalty," "Sinfonians," and "Go U Northwestern." After the pregame show, two performances of "Alma Mater" are recorded. The second performance appears to be from the Northwestern vs. Illinois game on November 21, 1981. ', - ], - genre: ["Super 8"], - identifier: "1f825787-bf84-4a94-b8cb-dae58f66776d", - keywords: null, - language: ["English"], - location: null, - physical_description_material: null, - physical_description_size: null, - scope_and_contents: null, - source: - "https://dc.library.northwestern.edu/items/1f825787-bf84-4a94-b8cb-dae58f66776d", - style_period: null, - subject: [ - "Northwestern University (Evanston, Ill.). Marching Band", - "Northwestern University (Evanston, Ill.). School of Music", - "Music", - "Marching bands", - "Conductors (Music)", - "Band directors", - "Fighting Illini (Football team)", - "University of Illinois at Urbana-Champaign", - "Ryan Field (Evanston, Ill.)", - "Northwestern Wildcats (Football team)", - "Northwestern University (Evanston, Ill.)", - ], - table_of_contents: null, - technique: null, - work_type: "Video", - }, - }, - ], - sources: - "N/A (The sources list videos of Northwestern playing against other schools, but not Michigan State.)", -}; - -export default mockAnswer; diff --git a/components/Chat/hooks/useEventCallback.ts b/components/Chat/hooks/useEventCallback.ts deleted file mode 100644 index 1afdef5b..00000000 --- a/components/Chat/hooks/useEventCallback.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { useCallback, useRef } from "react"; - -import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; - -export function useEventCallback( - fn: (...args: Args) => R -) { - const ref = useRef(() => { - throw new Error("Cannot call an event handler while rendering."); - }); - - useIsomorphicLayoutEffect(() => { - ref.current = fn; - }, [fn]); - - return useCallback((...args: Args) => ref.current(...args), [ref]); -} diff --git a/components/Chat/hooks/useEventListener.ts b/components/Chat/hooks/useEventListener.ts deleted file mode 100644 index d0ac9bb1..00000000 --- a/components/Chat/hooks/useEventListener.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { RefObject, useEffect, useRef } from "react"; - -import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect"; - -// MediaQueryList Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: MediaQueryListEventMap[K]) => void, - element: RefObject, - options?: AddEventListenerOptions | boolean -): void; - -// Window Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: WindowEventMap[K]) => void, - element?: undefined, - options?: AddEventListenerOptions | boolean -): void; - -// Element Event based useEventListener interface -function useEventListener< - K extends keyof HTMLElementEventMap, - T extends HTMLElement = HTMLDivElement ->( - eventName: K, - handler: (event: HTMLElementEventMap[K]) => void, - element: RefObject, - options?: AddEventListenerOptions | boolean -): void; - -// Document Event based useEventListener interface -function useEventListener( - eventName: K, - handler: (event: DocumentEventMap[K]) => void, - element: RefObject, - options?: AddEventListenerOptions | boolean -): void; - -function useEventListener< - KW extends keyof WindowEventMap, - KH extends keyof HTMLElementEventMap, - KM extends keyof MediaQueryListEventMap, - T extends HTMLElement | MediaQueryList | void = void ->( - eventName: KH | KM | KW, - handler: ( - event: - | Event - | HTMLElementEventMap[KH] - | MediaQueryListEventMap[KM] - | WindowEventMap[KW] - ) => void, - element?: RefObject, - options?: AddEventListenerOptions | boolean -) { - // Create a ref that stores handler - const savedHandler = useRef(handler); - - useIsomorphicLayoutEffect(() => { - savedHandler.current = handler; - }, [handler]); - - useEffect(() => { - // Define the listening target - const targetElement: T | Window = element?.current ?? window; - - if (!(targetElement && targetElement.addEventListener)) return; - - // Create event listener that calls handler function stored in ref - const listener: typeof handler = (event) => savedHandler.current(event); - - targetElement.addEventListener(eventName, listener, options); - - // Remove event listener on cleanup - return () => { - targetElement.removeEventListener(eventName, listener, options); - }; - }, [eventName, element, options]); -} - -export { useEventListener }; diff --git a/components/Chat/hooks/useIsomorphicLayoutEffect.ts b/components/Chat/hooks/useIsomorphicLayoutEffect.ts deleted file mode 100644 index ea6a0bc0..00000000 --- a/components/Chat/hooks/useIsomorphicLayoutEffect.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { useEffect, useLayoutEffect } from "react"; - -export const useIsomorphicLayoutEffect = - typeof window !== "undefined" ? useLayoutEffect : useEffect; diff --git a/components/Chat/hooks/useLocalStorage.ts b/components/Chat/hooks/useLocalStorage.ts deleted file mode 100644 index b339524d..00000000 --- a/components/Chat/hooks/useLocalStorage.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - Dispatch, - SetStateAction, - useCallback, - useEffect, - useState, -} from "react"; - -import { useEventCallback } from "./useEventCallback"; -import { useEventListener } from "./useEventListener"; - -declare global { - interface WindowEventMap { - "local-storage": CustomEvent; - } -} - -type SetValue = Dispatch>; - -export function useLocalStorage( - key: string, - initialValue: T -): [T, SetValue] { - // Get from local storage then - // parse stored json or return initialValue - const readValue = useCallback((): T => { - // Prevent build error "window is undefined" but keeps working - if (typeof window === "undefined") { - return initialValue; - } - - try { - const item = window.localStorage.getItem(key); - return item ? (parseJSON(item) as T) : initialValue; - } catch (error) { - console.warn(`Error reading localStorage key “${key}”:`, error); - return initialValue; - } - }, [initialValue, key]); - - // State to store our value - // Pass initial state function to useState so logic is only executed once - const [storedValue, setStoredValue] = useState(readValue); - - // Return a wrapped version of useState's setter function that ... - // ... persists the new value to localStorage. - const setValue: SetValue = useEventCallback((value) => { - // Prevent build error "window is undefined" but keeps working - if (typeof window === "undefined") { - console.warn( - `Tried setting localStorage key “${key}” even though environment is not a client` - ); - } - - try { - // Allow value to be a function so we have the same API as useState - const newValue = value instanceof Function ? value(storedValue) : value; - - // Save to local storage - window.localStorage.setItem(key, JSON.stringify(newValue)); - - // Save state - setStoredValue(newValue); - - // We dispatch a custom event so every useLocalStorage hook are notified - window.dispatchEvent(new Event("local-storage")); - } catch (error) { - console.warn(`Error setting localStorage key “${key}”:`, error); - } - }); - - useEffect(() => { - setStoredValue(readValue()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const handleStorageChange = useCallback( - (event: CustomEvent | StorageEvent) => { - if ((event as StorageEvent)?.key && (event as StorageEvent).key !== key) { - return; - } - setStoredValue(readValue()); - }, - [key, readValue] - ); - - // this only works for other documents, not the current one - useEventListener("storage", handleStorageChange); - - // this is a custom event, triggered in writeValueToLocalStorage - // See: useLocalStorage() - useEventListener("local-storage", handleStorageChange); - - return [storedValue, setValue]; -} - -// A wrapper for "JSON.parse()"" to support "undefined" value -function parseJSON(value: string | null): T | undefined { - try { - return value === "undefined" ? undefined : JSON.parse(value ?? ""); - } catch { - console.log("parsing error on", { value }); - return undefined; - } -} diff --git a/components/Chat/hooks/useLocalStorageSimple.tsx b/components/Chat/hooks/useLocalStorageSimple.tsx deleted file mode 100644 index 8215eb2e..00000000 --- a/components/Chat/hooks/useLocalStorageSimple.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -function useLocalStorageSimple( - storageKey: string, - fallbackState: T -): [T, React.Dispatch>] { - const [value, setValue] = React.useState( - // @ts-ignore - JSON.parse(localStorage.getItem(storageKey)) ?? fallbackState - ); - - React.useEffect(() => { - localStorage.setItem(storageKey, JSON.stringify(value)); - }, [value, storageKey]); - - return [value, setValue]; -} - -export default useLocalStorageSimple; diff --git a/components/Search/GenerativeAIToggle.tsx b/components/Search/GenerativeAIToggle.tsx index 6253bd9e..9b459c14 100644 --- a/components/Search/GenerativeAIToggle.tsx +++ b/components/Search/GenerativeAIToggle.tsx @@ -19,10 +19,11 @@ 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() { + 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.`; + return ( diff --git a/components/Chat/components/Answer/Loader.js b/components/Shared/BouncingLoader.tsx similarity index 75% rename from components/Chat/components/Answer/Loader.js rename to components/Shared/BouncingLoader.tsx index ab24e18d..4859065b 100644 --- a/components/Chat/components/Answer/Loader.js +++ b/components/Shared/BouncingLoader.tsx @@ -1,13 +1,14 @@ import { keyframes, styled } from "@/stitches.config"; + import React from "react"; -const AnswerLoader = () => { +const BouncingLoader = () => { return ( - +
-
+ ); }; @@ -20,10 +21,9 @@ const bouncingLoader = keyframes({ }, }); -const StyledAnswerLoader = styled("div", { +const StyledBouncingLoader = styled("div", { display: "flex", - justifyContent: "center", - margin: "$gr5 auto", + margin: "$gr2 auto", "& > div": { width: "$gr2", @@ -44,4 +44,4 @@ const StyledAnswerLoader = styled("div", { }, }); -export default AnswerLoader; +export default BouncingLoader; diff --git a/hooks/useMarkdown.tsx b/hooks/useMarkdown.tsx new file mode 100644 index 00000000..104c25c7 --- /dev/null +++ b/hooks/useMarkdown.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; + +import rehypeRaw from "rehype-raw"; +import rehypeStringify from "rehype-stringify"; +import remarkParse from "remark-parse"; +import remarkRehype from "remark-rehype"; +import { unified } from "unified"; + +function useMarkdown({ + markdown, + hasCursor, +}: { + markdown: string; + hasCursor: boolean; +}): { + html: string; + jsx: JSX.Element; +} { + const [html, setHtml] = useState(""); + + const cursor = ""; + const preparedMarkdown = hasCursor ? markdown + cursor : markdown; + + useEffect(() => { + (async () => { + const processor = unified() + .use(remarkParse) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeStringify); + + const result = await processor.process(preparedMarkdown); + const htmlContent = String(result); + + const cursorRegex = new RegExp(cursor, "g"); + const updatedHtml = hasCursor + ? htmlContent.replace( + cursorRegex, + `` + ) + : htmlContent; + + setHtml(updatedHtml); + })(); + }, [hasCursor, preparedMarkdown]); + + return { + html, + jsx:
, + }; +} + +export default useMarkdown; diff --git a/lib/chat-helpers.ts b/lib/chat-helpers.ts index 0c6382c1..cf205043 100644 --- a/lib/chat-helpers.ts +++ b/lib/chat-helpers.ts @@ -1,4 +1,4 @@ -import { Question } from "../components/Chat/types/chat"; +import { Question } from "../types/components/chat"; const prepareQuestion = (questionString: string, authToken: string) => { const date = new Date(); diff --git a/package-lock.json b/package-lock.json index 8fe43d8b..58a91b06 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,7 +38,12 @@ "react-error-boundary": "^4.0.13", "react-masonry-css": "^1.0.16", "react-share": "^5.0.3", - "swiper": "^9.4.1" + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "swiper": "^9.4.1", + "unified": "^11.0.4" }, "devDependencies": { "@elastic/elasticsearch": "7.17", @@ -3856,6 +3861,14 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "8.56.10", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", @@ -3918,6 +3931,14 @@ "@types/node": "*" } }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -4011,11 +4032,24 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/mdast": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", + "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, "node_modules/@types/node": { "version": "20.12.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", @@ -4123,6 +4157,11 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, + "node_modules/@types/unist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", + "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -4349,8 +4388,7 @@ "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@webassemblyjs/ast": { "version": "1.12.1", @@ -5165,6 +5203,15 @@ "@babel/core": "^7.0.0" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5410,6 +5457,15 @@ "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", "dev": true }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5433,6 +5489,33 @@ "node": ">=10" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -5646,6 +5729,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", @@ -6095,7 +6187,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -6111,8 +6202,7 @@ "node_modules/debug/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/decimal.js": { "version": "10.4.3", @@ -6120,6 +6210,18 @@ "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", "dev": true }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -6194,7 +6296,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "engines": { "node": ">=6" } @@ -6213,6 +6314,18 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -7699,8 +7812,7 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, "node_modules/extract-zip": { "version": "2.0.1", @@ -8417,6 +8529,130 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", + "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", + "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-raw": "^9.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hls.js": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.8.tgz", @@ -8454,6 +8690,15 @@ "void-elements": "3.1.0" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -8981,6 +9226,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -12068,6 +12324,61 @@ "tmpl": "1.0.5" } }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", + "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -12082,6 +12393,427 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", + "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", + "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, "node_modules/micromatch": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", @@ -12662,7 +13394,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dev": true, "dependencies": { "entities": "^4.4.0" }, @@ -12933,6 +13664,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/property-information": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", + "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -13253,6 +13993,65 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", + "integrity": "sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -13775,6 +14574,15 @@ "source-map": "^0.6.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -13988,6 +14796,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", + "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -14415,6 +15236,24 @@ "node": ">=12" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -14706,6 +15545,87 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/unified": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", + "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -14864,6 +15784,46 @@ "node": ">=0.6.0" } }, + "node_modules/vfile": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", + "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", + "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -14907,6 +15867,15 @@ "node": ">=10.13.0" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -15451,6 +16420,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index ea71ac76..da454c32 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,12 @@ "react-error-boundary": "^4.0.13", "react-masonry-css": "^1.0.16", "react-share": "^5.0.3", - "swiper": "^9.4.1" + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "swiper": "^9.4.1", + "unified": "^11.0.4" }, "devDependencies": { "@elastic/elasticsearch": "7.17", diff --git a/pages/search.tsx b/pages/search.tsx index 96415ce2..a650ddcd 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from "react"; import { ApiSearchRequestBody } from "@/types/api/request"; import { ApiSearchResponse } from "@/types/api/response"; -import ChatWrapper from "@/components/Chat/components/Wrapper"; +import ChatWrapper from "@/components/Chat/Wrapper"; import Container from "@/components/Shared/Container"; import { DC_API_SEARCH_URL } from "@/lib/constants/endpoints"; import Facets from "@/components/Facets/Facets"; diff --git a/components/Chat/types/chat.ts b/types/components/chat.ts similarity index 100% rename from components/Chat/types/chat.ts rename to types/components/chat.ts From 20cdaeb697a89664d6f0528d87352d2abae8280e Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Wed, 13 Mar 2024 13:21:48 -0500 Subject: [PATCH 16/57] Retain search query param between route searches --- components/Facets/Filter/Submit.tsx | 3 +- components/Facets/UserFacets/UserFacets.tsx | 16 ++++++-- components/Search/GenerativeAIToggle.test.tsx | 28 +++++++++++++- components/Search/Search.tsx | 24 +++++++----- context/search-context.tsx | 8 ---- hooks/useGenerativeAISearchToggle.ts | 37 ++++++------------- hooks/useQueryParams.ts | 9 ++++- lib/utils/facet-helpers.ts | 15 ++++++-- pages/search.tsx | 6 +-- types/context/search-context.ts | 1 - 10 files changed, 90 insertions(+), 57 deletions(-) diff --git a/components/Facets/Filter/Submit.tsx b/components/Facets/Filter/Submit.tsx index 3161f10a..ed0ad608 100644 --- a/components/Facets/Filter/Submit.tsx +++ b/components/Facets/Filter/Submit.tsx @@ -17,7 +17,7 @@ const FacetsFilterSubmit: React.FC = ({ const { query: { q }, } = router; - const { urlFacets } = useQueryParams(); + const { ai, urlFacets } = useQueryParams(); const { filterDispatch, @@ -34,6 +34,7 @@ const FacetsFilterSubmit: React.FC = ({ ...(q && { q }), ...urlFacets, ...userFacetsUnsubmitted, + ...(ai && { ai }), }; router.push({ diff --git a/components/Facets/UserFacets/UserFacets.tsx b/components/Facets/UserFacets/UserFacets.tsx index 31216004..9447bad3 100644 --- a/components/Facets/UserFacets/UserFacets.tsx +++ b/components/Facets/UserFacets/UserFacets.tsx @@ -1,4 +1,5 @@ import * as Dropdown from "@radix-ui/react-dropdown-menu"; + import { DropdownContent, DropdownToggle, @@ -6,6 +7,7 @@ import { } from "./UserFacets.styled"; import React, { useRef } from "react"; import { useEffect, useState } from "react"; + import FacetsCurrentUserValue from "@/components/Facets/UserFacets/Value"; import { IconChevronDown } from "@/components/Shared/SVG/Icons"; import { UrlFacets } from "@/types/context/filter-context"; @@ -29,14 +31,16 @@ const FacetsCurrentUser: React.FC = ({ screen, urlFacets, }) => { - const [currentOptions, setCurrentOptions] = useState([]); - const [isOpen, setIsOpen] = useState(false); const router = useRouter(); + const { filterDispatch, filterState } = useFilterState(); const { searchState: { searchFixed }, } = useSearchState(); + const [currentOptions, setCurrentOptions] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const dropdownRef = useRef(null); const handleOpenChange = (status: boolean) => setIsOpen(status); @@ -84,7 +88,7 @@ const FacetsCurrentUser: React.FC = ({ if (instance && facets) { const { id, value } = instance; const { - query: { q }, + query: { q, ai }, } = router; const newObj: UrlFacets = { ...facets }; @@ -93,7 +97,11 @@ const FacetsCurrentUser: React.FC = ({ if (screen === "search") router.push({ pathname: "/search", - query: { ...(q && { q }), ...newObj }, + query: { + ...(q && { q }), + ...newObj, + ...(ai && { ai }), + }, }); if (screen === "modal") diff --git a/components/Search/GenerativeAIToggle.test.tsx b/components/Search/GenerativeAIToggle.test.tsx index 65ed910e..5f3fe778 100644 --- a/components/Search/GenerativeAIToggle.test.tsx +++ b/components/Search/GenerativeAIToggle.test.tsx @@ -38,6 +38,10 @@ const withSearchProvider = ( }; describe("GenerativeAIToggle", () => { + beforeEach(() => { + mockRouter.setCurrentUrl("/"); + }); + it("renders the generative AI toggle UI and toggles state for a logged in user", async () => { const user = userEvent.setup(); render( @@ -64,7 +68,7 @@ describe("GenerativeAIToggle", () => { expect(tooltip).toBeInTheDocument(); }); - it("renders the generative AI dialog for a non-logged in user", async () => { + it("renders a login dialog for a non-logged-in user when generative ai checkbox is checked", async () => { const user = userEvent.setup(); const nonLoggedInUser = { user: { @@ -114,4 +118,26 @@ describe("GenerativeAIToggle", () => { const checkbox = screen.getByRole("checkbox"); expect(checkbox).toHaveAttribute("data-state", "checked"); }); + + it("sets a query param in the URL when generative AI checkbox is clicked", async () => { + const user = userEvent.setup(); + + mockRouter.setCurrentUrl("/"); + render( + withUserProvider( + withSearchProvider( + , + defaultSearchState + ) + ) + ); + + await user.click(screen.getByRole("checkbox")); + + expect(mockRouter).toMatchObject({ + asPath: "/?ai=true", + pathname: "/", + query: { ai: "true" }, + }); + }); }); diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index aaf4fd15..a435ddb4 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -13,8 +13,9 @@ import React, { useState, } from "react"; -import { ALL_FACETS } from "@/lib/constants/facets-model"; import GenerativeAIToggle from "./GenerativeAIToggle"; +import { UrlFacets } from "@/types/context/filter-context"; +import { getAllFacetIds } from "@/lib/utils/facet-helpers"; import useQueryParams from "@/hooks/useQueryParams"; import { useRouter } from "next/router"; @@ -24,7 +25,7 @@ interface SearchProps { const Search: React.FC = ({ isSearchActive }) => { const router = useRouter(); - const { urlFacets } = useQueryParams(); + const { ai, urlFacets } = useQueryParams(); const searchRef = useRef(null); const formRef = useRef(null); @@ -36,22 +37,27 @@ const Search: React.FC = ({ isSearchActive }) => { const handleSubmit = (e: SyntheticEvent) => { e.preventDefault(); + const updatedFacets: UrlFacets = {}; + /* Guard against searching from a page with dynamic route params */ - const facetIds = ALL_FACETS.facets.map((facet) => facet.id); + const allFacetsIds = getAllFacetIds(); // Account for the "similar" facet (comes from "View All" in sliders) - facetIds.push("similar"); + allFacetsIds.push("similar"); - const urlFacetsKeys = Object.keys(urlFacets); - urlFacetsKeys.forEach((key) => { - if (!facetIds.includes(key)) { - delete urlFacets[key]; + Object.keys(urlFacets).forEach((facetKey) => { + if (allFacetsIds.includes(facetKey)) { + updatedFacets[facetKey] = urlFacets[facetKey]; } }); router.push({ pathname: "/search", - query: { q: searchValue, ...urlFacets }, + query: { + q: searchValue, + ...(ai && { ai }), + ...updatedFacets, + }, }); }; diff --git a/context/search-context.tsx b/context/search-context.tsx index dafee93f..9bb3326d 100644 --- a/context/search-context.tsx +++ b/context/search-context.tsx @@ -7,7 +7,6 @@ type Action = type: "updateAggregations"; aggregations: ApiResponseAggregation | undefined; } - | { type: "updateGenerativeAI"; isGenerativeAI: boolean } | { type: "updateSearch"; q: string } | { type: "updateSearchFixed"; searchFixed: boolean }; @@ -20,7 +19,6 @@ type SearchProviderProps = { const defaultState: SearchContextStore = { aggregations: {}, - isGenerativeAI: false, searchFixed: false, }; @@ -36,12 +34,6 @@ function searchReducer(state: State, action: Action) { aggregations: action.aggregations, }; } - case "updateGenerativeAI": { - return { - ...state, - isGenerativeAI: action.isGenerativeAI, - }; - } case "updateSearch": { return { ...state, diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index 0f8a4126..d2615723 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -1,9 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { DCAPI_ENDPOINT } from "@/lib/constants/endpoints"; import { UserContext } from "@/context/user-context"; import { useRouter } from "next/router"; -import { useSearchState } from "@/context/search-context"; const defaultModalState = { isOpen: false, @@ -14,15 +13,11 @@ const aiQueryParam = "ai"; export default function useGenerativeAISearchToggle() { const router = useRouter(); - const effectQueryDep = router.query[aiQueryParam]; + const isChecked = router.query[aiQueryParam] === "true"; - const { searchState, searchDispatch } = useSearchState(); const { user } = React.useContext(UserContext); const [dialog, setDialog] = useState(defaultModalState); - const [isChecked, setIsChecked] = useState( - searchState.isGenerativeAI - ); const loginUrl = `${DCAPI_ENDPOINT}/auth/login?goto=${goToLocation()}`; @@ -42,12 +37,17 @@ export default function useGenerativeAISearchToggle() { function handleCheckChange(checked: boolean) { if (!user?.isLoggedIn) { setDialog({ ...dialog, isOpen: checked }); - } + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [aiQueryParam]: _, ...query } = router.query; + + if (checked) { + query[aiQueryParam] = checked.toString(); + } - if (user?.isLoggedIn) { - searchDispatch({ - isGenerativeAI: checked, - type: "updateGenerativeAI", + router.push({ + pathname: router.pathname, + query, }); } } @@ -56,19 +56,6 @@ export default function useGenerativeAISearchToggle() { router.push(loginUrl); } - useEffect(() => { - setIsChecked(searchState.isGenerativeAI); - }, [searchState.isGenerativeAI]); - - useEffect(() => { - if (effectQueryDep && user?.isLoggedIn) { - searchDispatch({ - isGenerativeAI: true, - type: "updateGenerativeAI", - }); - } - }, [effectQueryDep, searchDispatch, user?.isLoggedIn]); - return { closeDialog, dialog, diff --git a/hooks/useQueryParams.ts b/hooks/useQueryParams.ts index 8fc98a2a..009167e1 100644 --- a/hooks/useQueryParams.ts +++ b/hooks/useQueryParams.ts @@ -7,16 +7,23 @@ export default function useQueryParams() { const { isReady, query } = useRouter(); const [urlFacets, setUrlFacets] = React.useState({}); const [searchTerm, setSearchTerm] = React.useState(); + const [ai, setAi] = React.useState(); React.useEffect(() => { if (!isReady) return; const obj = parseUrlFacets(query); const q = (query.q ? query.q : "") as string; + const ai = (query.ai ? query.ai : "") as string; setUrlFacets(obj); setSearchTerm(q); + setAi(ai); }, [isReady, query]); - return { searchTerm, urlFacets }; + return { + ai, + searchTerm, + urlFacets, + }; } diff --git a/lib/utils/facet-helpers.ts b/lib/utils/facet-helpers.ts index 131f12b7..1c04148f 100644 --- a/lib/utils/facet-helpers.ts +++ b/lib/utils/facet-helpers.ts @@ -1,5 +1,6 @@ import { ALL_FACETS, FACETS } from "@/lib/constants/facets-model"; import { FacetsGroup, FacetsInstance } from "@/types/components/facets"; + import { UrlFacets } from "@/types/context/filter-context"; /** @@ -26,6 +27,10 @@ export function facetRegex(str?: string) { return `.*${pattern}.*`; } +export const getAllFacetIds = () => { + return ALL_FACETS.facets.map((facet) => facet.id); +}; + export const getFacetById = (id: string): FacetsInstance | undefined => { return ALL_FACETS.facets.find((facet) => facet.id === id); }; @@ -41,15 +46,17 @@ export const getFacetGroup = (id: string): FacetsGroup | undefined => { type NextJSRouterQuery = NodeJS.Dict; export const parseUrlFacets = (routerQuery: NextJSRouterQuery) => { if (!routerQuery) return {}; - const obj: UrlFacets = {}; + const urlFacets: UrlFacets = {}; + + const allFacetIds = getAllFacetIds(); for (const [key, value] of Object.entries(routerQuery)) { - if (!["q", "page"].includes(key)) { + if (allFacetIds.includes(key)) { if (key && value) { - obj[key] = Array.isArray(value) ? value : [value]; + urlFacets[key] = Array.isArray(value) ? value : [value]; } } } - return obj; + return urlFacets; }; diff --git a/pages/search.tsx b/pages/search.tsx index a650ddcd..cb511908 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -29,8 +29,8 @@ import { getWork } from "@/lib/work-helpers"; import { loadDefaultStructuredData } from "@/lib/json-ld"; import { parseUrlFacets } from "@/lib/utils/facet-helpers"; import { pluralize } from "@/lib/utils/count-helpers"; +import useQueryParams from "@/hooks/useQueryParams"; import { useRouter } from "next/router"; -import { useSearchState } from "@/context/search-context"; type RequestState = { data: ApiSearchResponse | null; @@ -42,9 +42,9 @@ const SearchPage: NextPage = () => { const size = 40; const router = useRouter(); - const { searchState } = useSearchState(); const { user } = React.useContext(UserContext); - const showChatResponse = user?.isLoggedIn && searchState.isGenerativeAI; + const { ai } = useQueryParams(); + const showChatResponse = user?.isLoggedIn && ai; const [requestState, setRequestState] = useState({ data: null, diff --git a/types/context/search-context.ts b/types/context/search-context.ts index 0471d455..a252ea31 100644 --- a/types/context/search-context.ts +++ b/types/context/search-context.ts @@ -2,7 +2,6 @@ import { ApiResponseAggregation } from "@/types/api/response"; export interface SearchContextStore { aggregations?: ApiResponseAggregation; - isGenerativeAI: boolean; searchFixed: boolean; } From 2e3c4dbe463d006325d0952a991fc835ab546ddf Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Fri, 22 Mar 2024 14:31:24 -0500 Subject: [PATCH 17/57] Add an AlertDialog component, and use it with Generative AI search bar toggle --- components/Search/GenerativeAI.styled.ts | 23 +-------- components/Search/GenerativeAIToggle.tsx | 35 ++++---------- components/Shared/AlertDialog.styled.ts | 61 ++++++++++++++++++++++++ components/Shared/AlertDialog.test.tsx | 32 +++++++++++++ components/Shared/AlertDialog.tsx | 61 ++++++++++++++++++++++++ package-lock.json | 29 +++++++++++ package.json | 1 + 7 files changed, 194 insertions(+), 48 deletions(-) create mode 100644 components/Shared/AlertDialog.styled.ts create mode 100644 components/Shared/AlertDialog.test.tsx create mode 100644 components/Shared/AlertDialog.tsx diff --git a/components/Search/GenerativeAI.styled.ts b/components/Search/GenerativeAI.styled.ts index b31f937d..33698c81 100644 --- a/components/Search/GenerativeAI.styled.ts +++ b/components/Search/GenerativeAI.styled.ts @@ -28,20 +28,6 @@ const GenerativeAIToggleWrapper = styled("div", { }, }); -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", @@ -51,11 +37,4 @@ const TooltipContent = styled(Tooltip.Content, { zIndex: 2, }); -export { - DialogButtonRow, - FlexBody, - GenerativeAIDialogMessage, - GenerativeAIToggleWrapper, - TooltipContent, - TooltipTrigger, -}; +export { GenerativeAIToggleWrapper, TooltipContent, TooltipTrigger }; diff --git a/components/Search/GenerativeAIToggle.tsx b/components/Search/GenerativeAIToggle.tsx index 9b459c14..12be0c66 100644 --- a/components/Search/GenerativeAIToggle.tsx +++ b/components/Search/GenerativeAIToggle.tsx @@ -5,20 +5,16 @@ import { 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 SharedAlertDialog from "../Shared/AlertDialog"; import useGenerativeAISearchToggle from "@/hooks/useGenerativeAISearchToggle"; function GenerativeAITooltip() { @@ -69,27 +65,14 @@ export default function GenerativeAIToggle({ -
- - - - You must be logged in with a Northwestern NetID to use the - Generative AI search feature. - - - - - - - -
+ + You must be logged in with a Northwestern NetID to use the Generative AI + search feature. + ); } diff --git a/components/Shared/AlertDialog.styled.ts b/components/Shared/AlertDialog.styled.ts new file mode 100644 index 00000000..941b2808 --- /dev/null +++ b/components/Shared/AlertDialog.styled.ts @@ -0,0 +1,61 @@ +import * as AlertDialog from "@radix-ui/react-alert-dialog"; + +import { styled } from "@/stitches.config"; + +/* eslint sort-keys: 0 */ + +const AlertDialogOverlay = styled(AlertDialog.Overlay, { + background: "rgba(0 0 0 / 0.382)", + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + display: "grid", + placeItems: "center", + overflowY: "auto", + zIndex: "1", +}); + +const AlertDialogContent = styled(AlertDialog.Content, { + backgroundColor: "white", + borderRadius: 6, + boxShadow: + "hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px", + position: "fixed", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + width: "90vw", + maxWidth: "500px", + maxHeight: "85vh", + padding: 25, + zIndex: "2", + + "&:focus": { outline: "none" }, +}); + +const AlertDialogTitle = styled(AlertDialog.Title, { + fontSize: "$gr4", + lineHeight: "1.5rem", + padding: "0", + margin: "0", + color: "$black50", + fontWeight: "400", +}); + +const AlertDialogButtonRow = styled("div", { + display: "flex", + justifyContent: "flex-end", + + "& > *:not(:last-child)": { + marginRight: "$gr3", + }, +}); + +export { + AlertDialogButtonRow, + AlertDialogContent, + AlertDialogOverlay, + AlertDialogTitle, +}; diff --git a/components/Shared/AlertDialog.test.tsx b/components/Shared/AlertDialog.test.tsx new file mode 100644 index 00000000..ac18aca0 --- /dev/null +++ b/components/Shared/AlertDialog.test.tsx @@ -0,0 +1,32 @@ +import { render, screen } from "@testing-library/react"; + +import React from "react"; +import SharedAlertDialog from "./AlertDialog"; + +const defaultProps = { + action: { label: "Login", onClick: jest.fn() }, + cancel: { label: "Cancel", onClick: jest.fn() }, + isOpen: true, + title: "Title", +}; + +describe("SharedAlertDialog", () => { + it("renders the primary components", () => { + render(Content); + + expect(screen.getByText("Content")).toBeInTheDocument(); + expect(screen.getByRole("heading")).toHaveTextContent(defaultProps.title); + expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Login" })).toBeInTheDocument(); + }); + + it("calls the cancel and action callback functions", () => { + render(Content); + + screen.getByRole("button", { name: "Cancel" }).click(); + expect(defaultProps.cancel.onClick).toHaveBeenCalled(); + + screen.getByRole("button", { name: "Login" }).click(); + expect(defaultProps.action.onClick).toHaveBeenCalled(); + }); +}); diff --git a/components/Shared/AlertDialog.tsx b/components/Shared/AlertDialog.tsx new file mode 100644 index 00000000..a92f4e58 --- /dev/null +++ b/components/Shared/AlertDialog.tsx @@ -0,0 +1,61 @@ +import * as AlertDialog from "@radix-ui/react-alert-dialog"; + +import { + AlertDialogButtonRow, + AlertDialogContent, + AlertDialogOverlay, + AlertDialogTitle, +} from "@/components/Shared/AlertDialog.styled"; + +import { Button } from "@nulib/design-system"; +import { ReactNode } from "react"; + +type DialogButton = { + label: string; + onClick: () => void; +}; + +interface SharedAlertDialogProps { + children: ReactNode; + action: DialogButton; + cancel?: DialogButton; + isOpen: boolean; + size?: "small" | "large"; + title?: string; +} + +export default function SharedAlertDialog({ + children, + action, + cancel, + isOpen, + title, +}: SharedAlertDialogProps) { + const cancelLabel = cancel?.label || "Cancel"; + + return ( + + + + + {title && {title}} + + {children} + + {cancel && ( + + )} + + + + + + + + + ); +} diff --git a/package-lock.json b/package-lock.json index 58a91b06..a237547e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nulib/design-system": "^1.6.2", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-accordion": "^1.0.1", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.1", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", @@ -2174,6 +2175,34 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.5.tgz", + "integrity": "sha512-OrVIOcZL0tl6xibeuGt5/+UxoT2N27KCFOPjFyfXMnchxSHZ/OW7cCX2nGlIYJrbHK/fczPcFzAwvNBB6XBNMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dialog": "1.0.5", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", diff --git a/package.json b/package.json index da454c32..28a9853a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@nulib/design-system": "^1.6.2", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-accordion": "^1.0.1", + "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-aspect-ratio": "^1.0.1", "@radix-ui/react-checkbox": "^1.0.1", "@radix-ui/react-dialog": "^1.0.2", From ffe28be578a292a3cff006562001affd624963af Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Mon, 29 Apr 2024 11:54:57 -0500 Subject: [PATCH 18/57] Refactor useChatSocket hook and implementation --- components/Chat/Chat.tsx | 71 +++++++++++-------------------------- components/Chat/Wrapper.tsx | 16 --------- hooks/useChatSocket.ts | 61 +++++++++++++++++++++++++------ pages/search.tsx | 4 +-- 4 files changed, 73 insertions(+), 79 deletions(-) delete mode 100644 components/Chat/Wrapper.tsx diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 9f199d97..133a8219 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -1,69 +1,40 @@ import React, { useEffect, useState } from "react"; 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 useChatSocket from "@/hooks/useChatSocket"; +import useQueryParams from "@/hooks/useQueryParams"; + +const Chat = () => { + const { searchTerm: question } = useQueryParams(); + const { authToken, isConnected, message, sendMessage } = useChatSocket(); -const Chat = ({ - authToken, - chatSocket, - question, -}: { - authToken: string; - chatSocket?: WebSocket; - question?: string; -}) => { - const [isReadyStateOpen, setIsReadyStateOpen] = useState(false); const [isStreamingComplete, setIsStreamingComplete] = useState(false); const [sourceDocuments, setSourceDocuments] = useState([]); const [streamedAnswer, setStreamedAnswer] = useState(""); - const handleReadyStateChange = () => { - setIsReadyStateOpen(chatSocket?.readyState === 1); - }; - - // Handle web socket stream updates - const handleMessageUpdate = (event: MessageEvent) => { - const data: StreamingMessage = JSON.parse(event.data); - // console.log("handleMessageUpdate", data); - - if (data.source_documents) { - setSourceDocuments(data.source_documents); - } else if (data.token) { - setStreamedAnswer((prev) => { - return prev + data.token; - }); - } else if (data.answer) { - setStreamedAnswer(data.answer); - setIsStreamingComplete(true); - } - }; - useEffect(() => { - if (question && isReadyStateOpen && chatSocket) { + if (question && isConnected && authToken) { const preparedQuestion = prepareQuestion(question, authToken); - chatSocket?.send(JSON.stringify(preparedQuestion)); + sendMessage(preparedQuestion); } - }, [chatSocket, isReadyStateOpen, prepareQuestion]); + }, [authToken, isConnected, question, sendMessage]); useEffect(() => { - if (chatSocket) { - chatSocket.addEventListener("message", handleMessageUpdate); - chatSocket.addEventListener("open", handleReadyStateChange); - chatSocket.addEventListener("close", handleReadyStateChange); - chatSocket.addEventListener("error", handleReadyStateChange); - } + if (!message) return; - return () => { - if (chatSocket) { - chatSocket.removeEventListener("message", handleMessageUpdate); - chatSocket.removeEventListener("open", handleReadyStateChange); - chatSocket.removeEventListener("close", handleReadyStateChange); - chatSocket.removeEventListener("error", handleReadyStateChange); - } - }; - }, [chatSocket, chatSocket?.url]); + if (message.source_documents) { + setSourceDocuments(message.source_documents); + } else if (message.token) { + setStreamedAnswer((prev) => { + return prev + message.token; + }); + } else if (message.answer) { + setStreamedAnswer(message.answer); + setIsStreamingComplete(true); + } + }, [message]); if (!question) return null; diff --git a/components/Chat/Wrapper.tsx b/components/Chat/Wrapper.tsx deleted file mode 100644 index f579a8e5..00000000 --- a/components/Chat/Wrapper.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Chat from "@/components/Chat/Chat"; -import useChatSocket from "@/hooks/useChatSocket"; -import useQueryParams from "@/hooks/useQueryParams"; - -const ChatWrapper = () => { - const { searchTerm: question } = useQueryParams(); - const { authToken, chatSocket } = useChatSocket(); - - if (!authToken || !chatSocket || !question) return null; - - return ( - - ); -}; - -export default ChatWrapper; diff --git a/hooks/useChatSocket.ts b/hooks/useChatSocket.ts index 25184f6d..91457db0 100644 --- a/hooks/useChatSocket.ts +++ b/hooks/useChatSocket.ts @@ -1,11 +1,17 @@ -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { DCAPI_CHAT_ENDPOINT } from "@/lib/constants/endpoints"; +import { StreamingMessage } from "@/types/components/chat"; import axios from "axios"; const useChatSocket = () => { - const [chatSocket, setChatSocket] = useState(null); const [authToken, setAuthToken] = useState(null); + const [url, setUrl] = useState(null); + + const socketRef = useRef(null); + const [message, setMessage] = useState(); + + const [isConnected, setIsConnected] = useState(false); useEffect(() => { axios({ @@ -15,24 +21,57 @@ const useChatSocket = () => { }) .then((response) => { const { auth: authToken, endpoint } = response.data; - if (!authToken || !endpoint) return; - const socket = new WebSocket(endpoint); - setAuthToken(authToken); - setChatSocket(socket); - - return () => { - if (socket) socket.close(); - }; + setUrl(endpoint); }) .catch((error) => { console.error(error); }); }, []); - return { authToken, chatSocket }; + useEffect(() => { + if (!url) return; + + socketRef.current = new WebSocket(url); + + socketRef.current.onopen = () => { + console.log("WebSocket connected"); + setIsConnected(true); + }; + + socketRef.current.onclose = () => { + console.log("WebSocket disconnected"); + setIsConnected(false); + }; + + socketRef.current.onerror = (error) => { + console.error("WebSocket error", error); + }; + + socketRef.current.onmessage = (event: MessageEvent) => { + const data = JSON.parse(event.data); + setMessage(data); + }; + + return () => { + socketRef.current?.close(); + }; + }, [url]); + + const sendMessage = useCallback((data: object) => { + if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { + socketRef.current.send(JSON.stringify(data)); + } + }, []); + + return { + authToken, + isConnected, + message, + sendMessage, + }; }; export default useChatSocket; diff --git a/pages/search.tsx b/pages/search.tsx index cb511908..3fabc93d 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -7,7 +7,7 @@ import React, { useEffect, useState } from "react"; import { ApiSearchRequestBody } from "@/types/api/request"; import { ApiSearchResponse } from "@/types/api/response"; -import ChatWrapper from "@/components/Chat/Wrapper"; +import Chat from "@/components/Chat/Chat"; import Container from "@/components/Shared/Container"; import { DC_API_SEARCH_URL } from "@/lib/constants/endpoints"; import Facets from "@/components/Facets/Facets"; @@ -189,7 +189,7 @@ const SearchPage: NextPage = () => { /> )} - {showChatResponse && } + {showChatResponse && } From 099a2f2fa28af6880e428587a05769aa1944ee39 Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Mon, 6 May 2024 14:16:43 -0500 Subject: [PATCH 19/57] Use Opensearch hybrid search when interacting with Chat AI search --- components/Facets/Filter/Modal.tsx | 2 ++ lib/queries/builder.ts | 46 ++++++++++++++++++++++++++---- lib/utils/get-url-search-params.ts | 5 ++++ pages/search.tsx | 13 +++++++-- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/components/Facets/Filter/Modal.tsx b/components/Facets/Filter/Modal.tsx index ef4403af..4cd1759c 100644 --- a/components/Facets/Filter/Modal.tsx +++ b/components/Facets/Filter/Modal.tsx @@ -1,4 +1,5 @@ import * as Dialog from "@radix-ui/react-dialog"; + import { FilterBody, FilterBodyInner, @@ -7,6 +8,7 @@ import { FilterHeader, } from "@/components/Facets/Filter/Filter.styled"; import React, { useEffect, useState } from "react"; + import { ApiSearchRequestBody } from "@/types/api/request"; import { ApiSearchResponse } from "@/types/api/response"; import { DC_API_SEARCH_URL } from "@/lib/constants/endpoints"; diff --git a/lib/queries/builder.ts b/lib/queries/builder.ts index b410bc13..07a41829 100644 --- a/lib/queries/builder.ts +++ b/lib/queries/builder.ts @@ -6,6 +6,7 @@ import { QueryDslQueryContainer } from "@elastic/elasticsearch/api/types"; import { UrlFacets } from "@/types/context/filter-context"; import { buildAggs } from "@/lib/queries/aggs"; import { buildFacetFilters } from "@/lib/queries/facet"; +import { isAiChatActive } from "../utils/get-url-search-params"; type BuildQueryProps = { aggs?: FacetsInstance[]; @@ -18,7 +19,11 @@ type BuildQueryProps = { export function buildQuery(obj: BuildQueryProps) { const { aggs, aggsFilterValue, size, term, urlFacets } = obj; const must: QueryDslQueryContainer[] = []; + let queryValue; + const isAiChat = isAiChatActive(); + + // Build the "must" part of the query if (term) must.push(buildSearchPart(term)); if (Object.keys(urlFacets).length > 0) { @@ -29,14 +34,43 @@ export function buildQuery(obj: BuildQueryProps) { } } + // User facets exist and we are not in AI chat mode + if (must.length > 0 && !isAiChat) { + queryValue = { + bool: { + must, + }, + }; + } + + // We are in AI chat mode and a search term exists + if (isAiChat && term) { + queryValue = { + hybrid: { + queries: [ + { + bool: { + must, + }, + }, + { + neural: { + embedding: { + k: size || 20, + model_id: process.env.NEXT_PUBLIC_OPENSEARCH_MODEL_ID, + query_text: term, // if term has no value, the API returns a 400 error + }, + }, + }, + ], + }, + }; + } + return { ...querySearchTemplate, - ...(must.length > 0 && { - query: { - bool: { - must: must, - }, - }, + ...(queryValue && { + query: queryValue, }), ...(aggs && { aggs: buildAggs(aggs, aggsFilterValue, urlFacets) }), ...(typeof size !== "undefined" && { size: size }), diff --git a/lib/utils/get-url-search-params.ts b/lib/utils/get-url-search-params.ts index c8e066f9..285edacd 100644 --- a/lib/utils/get-url-search-params.ts +++ b/lib/utils/get-url-search-params.ts @@ -15,3 +15,8 @@ export function getUrlSearchParams(url: string) { return paramsObj; } + +export function isAiChatActive() { + const urlParams = new URLSearchParams(window.location.search); + return urlParams.get("ai") === "true"; +} diff --git a/pages/search.tsx b/pages/search.tsx index 3fabc93d..53097ed2 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -64,7 +64,7 @@ const SearchPage: NextPage = () => { }); /** - * Post request to the search API endpoint for query and facet parameters + * Make requests to the search API endpoint */ useEffect(() => { if (!router.isReady) return; @@ -72,6 +72,8 @@ const SearchPage: NextPage = () => { try { const { page, q } = router.query; const urlFacets = parseUrlFacets(router.query); + const requestUrl = new URL(DC_API_SEARCH_URL); + const pipeline = process.env.NEXT_PUBLIC_OPENSEARCH_PIPELINE; // If there is a "similar" facet, get the work and set the state if (urlFacets?.similar) { @@ -93,9 +95,14 @@ const SearchPage: NextPage = () => { urlFacets, }); + // Request as a "hybrid" OpensSearch query + // @ts-expect-error - 'hybrid' is not in Elasticsearch package types + if (!!body?.query?.hybrid && pipeline) { + requestUrl.searchParams.append("search_pipeline", pipeline); + } const response = await apiPostRequest({ - body: body, - url: DC_API_SEARCH_URL, + body, + url: requestUrl.toString(), }); /** From e0fc0dd48af2e290fa31b06a81c4b7800e447e6c Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 16 May 2024 08:33:21 -0500 Subject: [PATCH 20/57] Prompt user login when ai chat query param is in the url and user is not logged in; mainly targets new page views or link sharing --- components/Search/GenerativeAIToggle.test.tsx | 22 +-- hooks/useGenerativeAISearchToggle.ts | 15 +- package-lock.json | 170 +++--------------- package.json | 2 +- 4 files changed, 47 insertions(+), 162 deletions(-) diff --git a/components/Search/GenerativeAIToggle.test.tsx b/components/Search/GenerativeAIToggle.test.tsx index 5f3fe778..be214d10 100644 --- a/components/Search/GenerativeAIToggle.test.tsx +++ b/components/Search/GenerativeAIToggle.test.tsx @@ -84,24 +84,17 @@ describe("GenerativeAIToggle", () => { ) ); - const checkbox = screen.getByRole("checkbox"); + const checkbox = await screen.findByRole("checkbox"); await user.click(checkbox); - const generativeAIDialog = screen.queryByText( + const generativeAIDialog = await screen.findByText( "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", () => { + it("renders a toggled generative ai state when a query param is set and user is logged in", () => { const activeSearchState = { ...defaultSearchState, isGenerativeAI: true, @@ -109,9 +102,11 @@ describe("GenerativeAIToggle", () => { mockRouter.setCurrentUrl("/search?ai=true"); render( - withSearchProvider( - , - activeSearchState + withUserProvider( + withSearchProvider( + , + activeSearchState + ) ) ); @@ -123,6 +118,7 @@ describe("GenerativeAIToggle", () => { const user = userEvent.setup(); mockRouter.setCurrentUrl("/"); + render( withUserProvider( withSearchProvider( diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index d2615723..eab92bdb 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { DCAPI_ENDPOINT } from "@/lib/constants/endpoints"; import { UserContext } from "@/context/user-context"; @@ -13,14 +13,23 @@ const aiQueryParam = "ai"; export default function useGenerativeAISearchToggle() { const router = useRouter(); - const isChecked = router.query[aiQueryParam] === "true"; + const [dialog, setDialog] = useState(defaultModalState); const { user } = React.useContext(UserContext); - const [dialog, setDialog] = useState(defaultModalState); + const isAiQueryParam = router.query[aiQueryParam] === "true"; + const isChecked = isAiQueryParam && user?.isLoggedIn; const loginUrl = `${DCAPI_ENDPOINT}/auth/login?goto=${goToLocation()}`; + // If the "ai" query param is present and the user is not logged in on + // initial load, show the dialog + useEffect(() => { + if (isAiQueryParam && !user?.isLoggedIn) { + setDialog((prevDialog) => ({ ...prevDialog, isOpen: true })); + } + }, [isAiQueryParam, user?.isLoggedIn]); + function goToLocation() { const currentUrl = `${window.location.origin}${router.asPath}`; const url = new URL(currentUrl); diff --git a/package-lock.json b/package-lock.json index a237547e..f064212b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@elastic/elasticsearch": "7.17", "@iiif/presentation-3": "^1.1.3", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^15.0.2", + "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.0.4", "@types/jest": "^29.4.0", "@types/node": "^20.11.23", @@ -1935,126 +1935,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", - "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", - "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", - "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", - "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", - "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", - "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", - "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4062,9 +3942,9 @@ "dev": true }, "node_modules/@types/mdast": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", - "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", "dependencies": { "@types/unist": "*" } @@ -8590,9 +8470,9 @@ } }, "node_modules/hast-util-raw": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", - "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.3.tgz", + "integrity": "sha512-ICWvVOF2fq4+7CMmtCPD5CM4QKjPbHpPotE6+8tDooV0ZuyJVUzHsrNX+O5NaRbieTf0F7FfeBOMAwi6Td0+yQ==", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", @@ -8614,9 +8494,9 @@ } }, "node_modules/hast-util-to-html": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", - "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.1.tgz", + "integrity": "sha512-hZOofyZANbyWo+9RP75xIDV/gq+OUKx+T46IlwERnKmfpwp81XBFbT9mi26ws+SJchA4RVUQwIBJpqEOBhMzEQ==", "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", @@ -12354,9 +12234,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", - "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", @@ -12457,9 +12337,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", - "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", "funding": [ { "type": "GitHub Sponsors", @@ -12793,9 +12673,9 @@ } }, "node_modules/micromark-util-subtokenize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", - "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", "funding": [ { "type": "GitHub Sponsors", @@ -13694,9 +13574,9 @@ "dev": true }, "node_modules/property-information": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", - "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -14826,9 +14706,9 @@ } }, "node_modules/stringify-entities": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", - "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" diff --git a/package.json b/package.json index 28a9853a..0f94b03f 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@elastic/elasticsearch": "7.17", "@iiif/presentation-3": "^1.1.3", "@testing-library/jest-dom": "^6.1.2", - "@testing-library/react": "^15.0.2", + "@testing-library/react": "^15.0.7", "@testing-library/user-event": "^14.0.4", "@types/jest": "^29.4.0", "@types/node": "^20.11.23", From 90a47a2b20de3bf3c847ef95e77b1fd4220dfcee Mon Sep 17 00:00:00 2001 From: mat Date: Fri, 17 May 2024 09:42:07 -0400 Subject: [PATCH 21/57] Tune hybrid search results to meet expected results. (#327) * Update hybrid search to include AND as default operator. * Add facet filters to neural. --- lib/queries/builder.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/queries/builder.ts b/lib/queries/builder.ts index 07a41829..77dadcee 100644 --- a/lib/queries/builder.ts +++ b/lib/queries/builder.ts @@ -49,8 +49,19 @@ export function buildQuery(obj: BuildQueryProps) { hybrid: { queries: [ { - bool: { - must, + query_string: { + /** + * Reference available index keys/vars: + * https://github.com/nulib/meadow/blob/deploy/staging/app/priv/elasticsearch/v2/settings/work.json + */ + fields: [ + "title^5", + // "all_text", // we feel like neural should handle the all_text part + "all_controlled_labels", + "all_ids^5", // boost the all_ids field + ], + query: term, + default_operator: "AND", }, }, { @@ -59,6 +70,11 @@ export function buildQuery(obj: BuildQueryProps) { k: size || 20, model_id: process.env.NEXT_PUBLIC_OPENSEARCH_MODEL_ID, query_text: term, // if term has no value, the API returns a 400 error + filter: { + bool: { + filter: buildFacetFilters(urlFacets), + }, + }, }, }, }, From 27d7a5790e1ed53c5e85348f3df9e0ac58dcd76e Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Tue, 21 May 2024 15:14:34 -0500 Subject: [PATCH 22/57] Facets filter button becomes fixed only after scrolling past chat response UI elements --- components/Facets/Facets.tsx | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/components/Facets/Facets.tsx b/components/Facets/Facets.tsx index 32726385..fb4453f8 100644 --- a/components/Facets/Facets.tsx +++ b/components/Facets/Facets.tsx @@ -1,24 +1,39 @@ import { FacetExtras, StyledFacets, Width, Wrapper } from "./Facets.styled"; -import React, { useRef } from "react"; +import React, { useEffect, useRef, useState } from "react"; import Container from "@/components/Shared/Container"; import FacetsFilter from "@/components/Facets/Filter/Filter"; import SearchPublicOnlyWorks from "@/components/Search/PublicOnlyWorks"; import WorkType from "@/components/Facets/WorkType/WorkType"; -import { useSearchState } from "@/context/search-context"; const Facets: React.FC = () => { - const { - searchState: { searchFixed }, - } = useSearchState(); + const wrapperRef = useRef(null); + const [isFixed, setIsFixed] = useState(false); const facetsRef = useRef(null); + useEffect(() => { + const handleResize = () => { + if (wrapperRef.current) { + const rect = wrapperRef.current.getBoundingClientRect(); + setIsFixed(rect.top < 70); + } + }; + + handleResize(); + + window.addEventListener("scroll", handleResize); + + return () => { + window.removeEventListener("scroll", handleResize); + }; + }, []); + return ( - + From 19242a8a19c8512fe8cb8ad337664fd0d789c4d6 Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Fri, 24 May 2024 09:17:34 -0500 Subject: [PATCH 23/57] Fix bug in ai alert dialog after login --- hooks/useGenerativeAISearchToggle.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hooks/useGenerativeAISearchToggle.ts b/hooks/useGenerativeAISearchToggle.ts index eab92bdb..76f9abfc 100644 --- a/hooks/useGenerativeAISearchToggle.ts +++ b/hooks/useGenerativeAISearchToggle.ts @@ -25,10 +25,11 @@ export default function useGenerativeAISearchToggle() { // If the "ai" query param is present and the user is not logged in on // initial load, show the dialog useEffect(() => { - if (isAiQueryParam && !user?.isLoggedIn) { - setDialog((prevDialog) => ({ ...prevDialog, isOpen: true })); + if (!user) return; + if (isAiQueryParam) { + setDialog((prevDialog) => ({ ...prevDialog, isOpen: !user?.isLoggedIn })); } - }, [isAiQueryParam, user?.isLoggedIn]); + }, [isAiQueryParam, user]); function goToLocation() { const currentUrl = `${window.location.origin}${router.asPath}`; From e09a6ae790ec3abba0b5953d1b4b76bff539fd70 Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 30 May 2024 14:05:15 -0500 Subject: [PATCH 24/57] Quick linting error fixes --- lib/queries/builder.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/queries/builder.ts b/lib/queries/builder.ts index 77dadcee..2486d8a0 100644 --- a/lib/queries/builder.ts +++ b/lib/queries/builder.ts @@ -50,31 +50,32 @@ export function buildQuery(obj: BuildQueryProps) { queries: [ { query_string: { + default_operator: "AND", + /** * Reference available index keys/vars: * https://github.com/nulib/meadow/blob/deploy/staging/app/priv/elasticsearch/v2/settings/work.json */ fields: [ - "title^5", - // "all_text", // we feel like neural should handle the all_text part + "title^5", // "all_text", // we feel like neural should handle the all_text part "all_controlled_labels", - "all_ids^5", // boost the all_ids field + "all_ids^5", ], query: term, - default_operator: "AND", }, }, { neural: { embedding: { - k: size || 20, - model_id: process.env.NEXT_PUBLIC_OPENSEARCH_MODEL_ID, - query_text: term, // if term has no value, the API returns a 400 error + // if term has no value, the API returns a 400 error filter: { bool: { filter: buildFacetFilters(urlFacets), }, }, + k: size || 20, + model_id: process.env.NEXT_PUBLIC_OPENSEARCH_MODEL_ID, + query_text: term, }, }, }, From a96806945bdb2760acf5c8733de249d8f9071448 Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 30 May 2024 14:09:38 -0500 Subject: [PATCH 25/57] Remove husky and pre-commit hooks from local dev --- .husky/pre-commit | 5 -- package-lock.json | 136 ++++++++++++++++++++++++++++++++++++++++------ package.json | 3 - 3 files changed, 120 insertions(+), 24 deletions(-) delete mode 100755 .husky/pre-commit diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index a0a80fa3..00000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -# npm run ts-lint-commit-hook -# npm run lint && npm run test:ci diff --git a/package-lock.json b/package-lock.json index f064212b..09a64ba1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,7 +65,6 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-testing-library": "^6.0.1", - "husky": "^9.0.11", "jest": "^29.0.3", "jest-environment-jsdom": "^29.0.3", "next-router-mock": "^0.9.1-beta.0", @@ -8676,21 +8675,6 @@ "node": ">=8.12.0" } }, - "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", - "dev": true, - "bin": { - "husky": "bin.mjs" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/typicode" - } - }, "node_modules/i18next": { "version": "23.11.4", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.4.tgz", @@ -16338,6 +16322,126 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/package.json b/package.json index 0f94b03f..f9b121db 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,7 @@ "export": "next build && next export", "lint": "next lint", "ts-lint": "tsc --noEmit --incremental --watch", - "ts-lint-commit-hook": "tsc --noEmit", "start": "next start", - "prepare": "husky install", "test": "jest --watch", "test:ci": "jest" }, @@ -76,7 +74,6 @@ "eslint-config-prettier": "^9.0.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-testing-library": "^6.0.1", - "husky": "^9.0.11", "jest": "^29.0.3", "jest-environment-jsdom": "^29.0.3", "next-router-mock": "^0.9.1-beta.0", From bee7bba74e2bdb21a12ea1248c9275acce23d04b Mon Sep 17 00:00:00 2001 From: Adam Joseph Arling Date: Thu, 13 Jun 2024 14:25:12 -0500 Subject: [PATCH 26/57] Support Github flavored markdown with new NPM useMarkdown hook --- components/Chat/Response/StreamedAnswer.tsx | 24 +- hooks/useMarkdown.tsx | 53 -- package-lock.json | 595 ++++++++++++++++---- package.json | 1 + 4 files changed, 494 insertions(+), 179 deletions(-) delete mode 100644 hooks/useMarkdown.tsx diff --git a/components/Chat/Response/StreamedAnswer.tsx b/components/Chat/Response/StreamedAnswer.tsx index 91411c12..7f55135f 100644 --- a/components/Chat/Response/StreamedAnswer.tsx +++ b/components/Chat/Response/StreamedAnswer.tsx @@ -1,6 +1,8 @@ import React from "react"; import { StyledStreamedAnswer } from "@/components/Chat/Response/Response.styled"; -import useMarkdown from "@/hooks/useMarkdown"; +import useMarkdown from "@nulib/use-markdown"; + +const cursor = ""; const ResponseStreamedAnswer = ({ isStreamingComplete, @@ -9,12 +11,22 @@ const ResponseStreamedAnswer = ({ isStreamingComplete: boolean; streamedAnswer: string; }) => { - const { jsx: content } = useMarkdown({ - hasCursor: !isStreamingComplete, - markdown: streamedAnswer, - }); + const preparedMarkdown = !isStreamingComplete + ? streamedAnswer + cursor + : streamedAnswer; + + const { html } = useMarkdown(preparedMarkdown); + + const cursorRegex = new RegExp(cursor, "g"); + const updatedHtml = !isStreamingComplete + ? html.replace(cursorRegex, ``) + : html; - return {content}; + return ( + +
+ + ); }; export default ResponseStreamedAnswer; diff --git a/hooks/useMarkdown.tsx b/hooks/useMarkdown.tsx deleted file mode 100644 index 104c25c7..00000000 --- a/hooks/useMarkdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState } from "react"; - -import rehypeRaw from "rehype-raw"; -import rehypeStringify from "rehype-stringify"; -import remarkParse from "remark-parse"; -import remarkRehype from "remark-rehype"; -import { unified } from "unified"; - -function useMarkdown({ - markdown, - hasCursor, -}: { - markdown: string; - hasCursor: boolean; -}): { - html: string; - jsx: JSX.Element; -} { - const [html, setHtml] = useState(""); - - const cursor = ""; - const preparedMarkdown = hasCursor ? markdown + cursor : markdown; - - useEffect(() => { - (async () => { - const processor = unified() - .use(remarkParse) - .use(remarkRehype, { allowDangerousHtml: true }) - .use(rehypeRaw) - .use(rehypeStringify); - - const result = await processor.process(preparedMarkdown); - const htmlContent = String(result); - - const cursorRegex = new RegExp(cursor, "g"); - const updatedHtml = hasCursor - ? htmlContent.replace( - cursorRegex, - `` - ) - : htmlContent; - - setHtml(updatedHtml); - })(); - }, [hasCursor, preparedMarkdown]); - - return { - html, - jsx:
, - }; -} - -export default useMarkdown; diff --git a/package-lock.json b/package-lock.json index 09a64ba1..9d0a3650 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@next/font": "^14.0.3", "@nulib/dcapi-types": "^2.3.1", "@nulib/design-system": "^1.6.2", + "@nulib/use-markdown": "^0.2.1", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-accordion": "^1.0.1", "@radix-ui/react-alert-dialog": "^1.0.5", @@ -1934,6 +1935,126 @@ "node": ">= 10" } }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1987,6 +2108,23 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@nulib/use-markdown": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@nulib/use-markdown/-/use-markdown-0.2.1.tgz", + "integrity": "sha512-gez/Hd3nku/MZi1ZOx6huwcMDkBpfuSIQX6gMclpf8N+w8QKvFt1sGPowp19H2aEv8FI9f5ao/9HID5jYeNk3A==", + "dependencies": { + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.0", + "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "unified": "^11.0.4" + }, + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -12146,6 +12284,16 @@ "node": ">=8" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -12217,6 +12365,44 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", @@ -12240,6 +12426,121 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", @@ -12260,6 +12561,26 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/mdast-util-to-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", @@ -12353,6 +12674,127 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", + "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", + "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/micromark-factory-destination": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", @@ -13914,6 +14356,24 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -13945,6 +14405,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -16322,126 +16797,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", - "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", - "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", - "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", - "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", - "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", - "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", - "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } } } } diff --git a/package.json b/package.json index f9b121db..7141bd89 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@next/font": "^14.0.3", "@nulib/dcapi-types": "^2.3.1", "@nulib/design-system": "^1.6.2", + "@nulib/use-markdown": "^0.2.1", "@radix-ui/colors": "^3.0.0", "@radix-ui/react-accordion": "^1.0.1", "@radix-ui/react-alert-dialog": "^1.0.5", From 4c136af4e6c0671b853a477cd5566ec878552445 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Thu, 13 Jun 2024 16:03:15 -0400 Subject: [PATCH 27/57] Split chat responses and legacy responses on /search screen. --- components/Chat/Chat.tsx | 39 ++-- components/Chat/Response/Images.tsx | 15 +- components/Chat/Response/Response.styled.tsx | 23 +-- components/Chat/Response/Response.tsx | 13 +- components/Facets/Filter/Clear.styled.ts | 2 +- components/Facets/Filter/Clear.tsx | 2 +- components/Facets/Filter/Filter.styled.ts | 13 +- components/Facets/index.tsx | 12 ++ components/Search/Options.styled.tsx | 183 +++++++++++++++++ components/Search/Options.tsx | 56 ++++++ components/Search/Search.styled.ts | 17 ++ components/Search/Search.tsx | 8 + components/Shared/Loader.styled.ts | 17 +- components/Shared/SVG/Icons.tsx | 11 ++ context/search-context.tsx | 31 ++- hooks/useGenerativeAISearchToggle.ts | 7 +- lib/queries/builder.ts | 11 +- package-lock.json | 96 ++++----- pages/search.tsx | 195 ++++++++++++++----- types/context/search-context.ts | 9 + 20 files changed, 610 insertions(+), 150 deletions(-) create mode 100644 components/Facets/index.tsx create mode 100644 components/Search/Options.styled.tsx create mode 100644 components/Search/Options.tsx diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 133a8219..9945464d 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -5,21 +5,26 @@ import { Work } from "@nulib/dcapi-types"; 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 { searchTerm: question } = useQueryParams(); + const { searchTerm = "" } = useQueryParams(); const { authToken, isConnected, message, sendMessage } = useChatSocket(); + const { searchDispatch, searchState } = useSearchState(); + const { chat } = searchState; + const { answer, documents = [], question } = chat; + + const sameQuestionExists = !!question && searchTerm === question; - const [isStreamingComplete, setIsStreamingComplete] = useState(false); const [sourceDocuments, setSourceDocuments] = useState([]); const [streamedAnswer, setStreamedAnswer] = useState(""); useEffect(() => { - if (question && isConnected && authToken) { - const preparedQuestion = prepareQuestion(question, authToken); + if (!sameQuestionExists && isConnected && authToken) { + const preparedQuestion = prepareQuestion(searchTerm, authToken); sendMessage(preparedQuestion); } - }, [authToken, isConnected, question, sendMessage]); + }, [authToken, isConnected, sameQuestionExists, searchTerm, sendMessage]); useEffect(() => { if (!message) return; @@ -31,21 +36,27 @@ const Chat = () => { return prev + message.token; }); } else if (message.answer) { - setStreamedAnswer(message.answer); - setIsStreamingComplete(true); + searchDispatch({ + chat: { + answer: message.answer, + documents: sourceDocuments, + question: searchTerm || "", + }, + type: "updateChat", + }); } - }, [message]); + }, [message, searchTerm, sourceDocuments, searchDispatch]); - if (!question) return null; + if (!searchTerm) return null; return ( ); }; -export default Chat; +export default React.memo(Chat); diff --git a/components/Chat/Response/Images.tsx b/components/Chat/Response/Images.tsx index 8a1d1a5f..1c7acbb0 100644 --- a/components/Chat/Response/Images.tsx +++ b/components/Chat/Response/Images.tsx @@ -4,10 +4,21 @@ 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 ResponseImages = ({ + isStreamingComplete, + sourceDocuments, +}: { + isStreamingComplete: boolean; + sourceDocuments: Work[]; +}) => { const [nextIndex, setNextIndex] = useState(0); useEffect(() => { + if (isStreamingComplete) { + setNextIndex(sourceDocuments.length); + return; + } + if (nextIndex < sourceDocuments.length) { const timer = setTimeout(() => { setNextIndex(nextIndex + 1); @@ -15,7 +26,7 @@ const ResponseImages = ({ sourceDocuments }: { sourceDocuments: Work[] }) => { return () => clearTimeout(timer); } - }, [nextIndex, sourceDocuments.length]); + }, [isStreamingComplete, nextIndex, sourceDocuments.length]); return ( diff --git a/components/Chat/Response/Response.styled.tsx b/components/Chat/Response/Response.styled.tsx index 8493dd04..48066d2d 100644 --- a/components/Chat/Response/Response.styled.tsx +++ b/components/Chat/Response/Response.styled.tsx @@ -13,14 +13,9 @@ const StyledResponse = styled("section", { position: "relative", gap: "$gr5", zIndex: "0", - margin: "0 $gr4", - "@xl": { - margin: "0 $gr4", - }, - - "@lg": { - margin: "0", + "h1, h2, h3, h4, h5, h6, strong": { + fontFamily: "$northwesternSansBold", }, }); @@ -39,9 +34,7 @@ const StyledResponseContent = styled("div", { }); const StyledResponseWrapper = styled("div", { - background: - "linear-gradient(0deg, $white calc(100% - 100px), $brightBlueB calc(100% + 100px))", - padding: "$gr6 0 $gr4", + padding: "0", }); const StyledImages = styled("div", { @@ -78,9 +71,10 @@ const StyledImages = styled("div", { }); const StyledQuestion = styled("h3", { - fontFamily: "$northwesternDisplayBold", + fontFamily: "$northwesternSansBold", fontWeight: "400", fontSize: "$gr6", + letterSpacing: "-0.012em", lineHeight: "1.35em", margin: "0", padding: "0 0 $gr3 0", @@ -112,8 +106,15 @@ const StyledStreamedAnswer = styled("article", { }, }); +const StyledResponseActions = styled("div", { + display: "flex", + gap: "$gr2", + padding: "$gr5 0 0", +}); + export { StyledResponse, + StyledResponseActions, StyledResponseAside, StyledResponseContent, StyledResponseWrapper, diff --git a/components/Chat/Response/Response.tsx b/components/Chat/Response/Response.tsx index 3ee18d9f..74beb57c 100644 --- a/components/Chat/Response/Response.tsx +++ b/components/Chat/Response/Response.tsx @@ -15,23 +15,23 @@ import { Work } from "@nulib/dcapi-types"; interface ChatResponseProps { isStreamingComplete: boolean; - question: string; + searchTerm: string; sourceDocuments: Work[]; streamedAnswer?: string; } const ChatResponse: React.FC = ({ isStreamingComplete, - question, + searchTerm, sourceDocuments, streamedAnswer, }) => { return ( - + - {question} + {searchTerm} {streamedAnswer ? ( = ({ {sourceDocuments.length > 0 && ( - + )} diff --git a/components/Facets/Filter/Clear.styled.ts b/components/Facets/Filter/Clear.styled.ts index 7075e488..7b712739 100644 --- a/components/Facets/Filter/Clear.styled.ts +++ b/components/Facets/Filter/Clear.styled.ts @@ -3,7 +3,7 @@ import { styled } from "@/stitches.config"; /* eslint sort-keys: 0 */ const FilterClearStyled = styled("button", { - background: "$white", + background: "transparent", border: "none", borderRadius: "50px", color: "$black50", diff --git a/components/Facets/Filter/Clear.tsx b/components/Facets/Filter/Clear.tsx index 2e2d49c6..98a5c9af 100644 --- a/components/Facets/Filter/Clear.tsx +++ b/components/Facets/Filter/Clear.tsx @@ -38,7 +38,7 @@ const FilterClear: React.FC = ({ isModal = false }) => { isFixed={searchFixed} isModal={isModal} > - Clear All + Reset ); }; diff --git a/components/Facets/Filter/Filter.styled.ts b/components/Facets/Filter/Filter.styled.ts index 5692fbe5..6e417227 100644 --- a/components/Facets/Filter/Filter.styled.ts +++ b/components/Facets/Filter/Filter.styled.ts @@ -1,4 +1,5 @@ import * as Dialog from "@radix-ui/react-dialog"; + import { IconStyled } from "@/components/Shared/Icon"; import { ValueWrapper } from "@/components/Facets/UserFacets/UserFacets.styled"; import { styled } from "@/stitches.config"; @@ -15,11 +16,12 @@ const FilterActivate = styled(Dialog.Trigger, { backgroundColor: "$purple", border: "0", color: "$white", - fontFamily: "$northwesternSansBold", + fontFamily: "$northwesternSansRegular", fontSize: "$gr3", borderRadius: "50px", transition: "$dcAll", padding: "0 $gr3 0 $gr1", + boxShadow: "0px 3px 15px #0002", [`& ${IconStyled}`]: { color: "$purple60", @@ -29,7 +31,6 @@ const FilterActivate = styled(Dialog.Trigger, { "&:hover": { backgroundColor: "$purple120", color: "$white", - boxShadow: "2px 2px 2px #0002", [`& ${IconStyled}`]: { color: "$white", @@ -46,14 +47,6 @@ const FilterFloating = styled("div", { boxShadow: "2px 2px 5px #0002", borderRadius: "50px", transition: "$dcAll", - - [`& ${FilterActivate}`]: { - boxShadow: "2px 2px 5px #0002", - }, - - "&:hover": { - boxShadow: "2px 2px 5px #0004", - }, }); const FilterClose = styled(Dialog.Close, {}); diff --git a/components/Facets/index.tsx b/components/Facets/index.tsx new file mode 100644 index 00000000..06c22d15 --- /dev/null +++ b/components/Facets/index.tsx @@ -0,0 +1,12 @@ +import FacetsFilter from "@/components/Facets/Filter/Filter"; +import { FilterProvider } from "@/context/filter-context"; + +const Facets: React.FC = () => { + return ( + + + + ); +}; + +export default Facets; diff --git a/components/Search/Options.styled.tsx b/components/Search/Options.styled.tsx new file mode 100644 index 00000000..caffd236 --- /dev/null +++ b/components/Search/Options.styled.tsx @@ -0,0 +1,183 @@ +import { IconStyled } from "../Shared/Icon"; +import { Wrapper as WorkTypeWrapper } from "@/components/Facets/WorkType/WorkType.styled"; +import { styled } from "@/stitches.config"; + +/* eslint sort-keys: 0 */ + +const StyledOptionsBar = styled("div", { + display: "flex", + justifyContent: "space-between", + position: "relative", + left: "0", + transition: "$dcScrollLeft", + zIndex: "1", + gap: "$gr3", + + [`& ${WorkTypeWrapper}`]: { + borderRight: "1px solid $black10", + paddingRight: "$gr2", + transition: "$dcWidth", + + "@sm": { + marginTop: "$gr3", + borderRight: "none", + paddingRight: "0", + }, + }, + + "@sm": { + padding: "$gr4 0", + flexDirection: "column", + alignItems: "center", + }, +}); + +const StyledOptionsWidth = styled("span", { + position: "absolute", + width: "100%", +}); + +const StyledOptionsFacets = styled("div", { + variants: { + isTabResults: { + false: { + display: "none", + }, + true: { + display: "flex", + width: "100%", + justifyContent: "space-between", + }, + }, + }, +}); + +const StyledOptionsExtras = styled("div", { + display: "flex", + + "@sm": { + marginTop: "$gr4", + flexDirection: "column-reverse", + alignItems: "center", + }, +}); + +const StyledOptionsTabs = styled("div", { + [`div[role="tablist"]`]: { + display: "flex", + flexShrink: "0", + flexGrow: "0", + flexWrap: "nowrap", + height: "38px", + boxShadow: "0px 3px 15px #0002", + borderRadius: "50px", + overflow: "hidden", + + button: { + cursor: "pointer", + backgroundColor: "$purple", + border: "0", + color: "$white", + fontFamily: "$northwesternSansRegular", + fontSize: "$gr3", + padding: "0 $gr3", + transition: "$dcAll", + whiteSpace: "nowrap", + display: "flex", + alignItems: "center", + + "&[data-state=active]": { + backgroundColor: "$purple", + + [`& ${IconStyled}`]: { + color: "$purple30", + fill: "$purple30", + }, + }, + + "&[data-state=inactive]": { + backgroundColor: "$white", + color: "$black50", + + [`& ${IconStyled}`]: { + color: "$black20", + fill: "$black20", + }, + }, + + "&:first-child": { + paddingLeft: "$gr1", + }, + + "&:hover": { + backgroundColor: "$purple120", + color: "$white", + + [`& ${IconStyled}`]: { + color: "$white", + fill: "$white", + }, + }, + }, + + "@sm": { + marginBottom: "$gr3", + }, + }, +}); + +const StyledOptions = styled("div", { + height: "38px", + transition: "$dcScrollHeight", + padding: "$gr4 0 0", + margin: "0 0 $gr6", + + "@sm": { + backgroundColor: "$gray6", + height: "225px", + }, + + ".facets-ui-container": { + transition: "$dcAll", + }, + + "&[data-filter-fixed='true']": { + flexGrow: "0", + flexShrink: "1", + height: "38px", + + "@sm": { + backgroundColor: "transparent", + margin: "0", + }, + + [`& ${StyledOptionsExtras}`]: { + width: "0", + opacity: "0", + }, + + [`& ${StyledOptionsBar}`]: { + position: "fixed", + margin: "0", + top: "$gr6", + left: "50%", + zIndex: "1", + transform: "translate(-50%)", + backfaceVisibility: "hidden", + webkitFontSmoothing: "subpixel-antialiased", + + "@sm": { + top: "$gr5", + }, + }, + }, +}); + +export { + StyledOptions, + StyledOptionsBar, + StyledOptionsExtras, + StyledOptionsFacets, + StyledOptionsTabs, + StyledOptionsWidth, +}; diff --git a/components/Search/Options.tsx b/components/Search/Options.tsx new file mode 100644 index 00000000..1d2be981 --- /dev/null +++ b/components/Search/Options.tsx @@ -0,0 +1,56 @@ +import React, { useRef } from "react"; +import { + StyledOptions, + StyledOptionsBar, + StyledOptionsExtras, + StyledOptionsFacets, + StyledOptionsTabs, + StyledOptionsWidth, +} from "@/components/Search/Options.styled"; + +import Container from "@/components/Shared/Container"; +import Facets from "@/components/Facets"; +import FacetsWorkType from "@/components/Facets/WorkType/WorkType"; +import SearchPublicOnlyWorks from "@/components/Search/PublicOnlyWorks"; +import { useSearchState } from "@/context/search-context"; + +interface SearchOptionsProps { + tabs: React.ReactNode; + activeTab?: string; + renderTabList?: boolean; +} + +const SearchOptions: React.FC = ({ + tabs, + activeTab, + renderTabList, +}) => { + const { + searchState: { searchFixed }, + } = useSearchState(); + + const optionsRef = useRef(null); + + return ( + + + + {renderTabList && {tabs}} + + + + + + + + + + + + ); +}; + +export default SearchOptions; diff --git a/components/Search/Search.styled.ts b/components/Search/Search.styled.ts index a7f5e0c6..3d134613 100644 --- a/components/Search/Search.styled.ts +++ b/components/Search/Search.styled.ts @@ -141,6 +141,22 @@ const ResultsWrapper = styled("div", { minHeight: "80vh", }); +const StyledResponseWrapper = styled("div", { + padding: "0 0 $gr6", + + variants: { + isAiResponse: { + true: { + background: + "linear-gradient(-5deg, $white calc(100% - 150px), $brightBlueB calc(100% + 100px))", + }, + false: { + background: "inherit", + }, + }, + }, +}); + export { Button, Clear, @@ -149,4 +165,5 @@ export { ResultsMessage, ResultsWrapper, SearchStyled, + StyledResponseWrapper, }; diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index a435ddb4..a5ca71e1 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -18,6 +18,7 @@ import { UrlFacets } from "@/types/context/filter-context"; import { getAllFacetIds } from "@/lib/utils/facet-helpers"; import useQueryParams from "@/hooks/useQueryParams"; import { useRouter } from "next/router"; +import { useSearchState } from "@/context/search-context"; interface SearchProps { isSearchActive: (value: boolean) => void; @@ -26,6 +27,7 @@ interface SearchProps { const Search: React.FC = ({ isSearchActive }) => { const router = useRouter(); const { ai, urlFacets } = useQueryParams(); + const { searchDispatch } = useSearchState(); const searchRef = useRef(null); const formRef = useRef(null); @@ -51,6 +53,11 @@ const Search: React.FC = ({ isSearchActive }) => { } }); + searchDispatch({ + type: "updateActiveTab", + activeTab: ai ? "stream" : "results", + }); + router.push({ pathname: "/search", query: { @@ -103,6 +110,7 @@ const Search: React.FC = ({ isSearchActive }) => { data-testid="search-ui-component" > ( ); +const IconSparkles: React.FC = () => ( + + + +); + const IconVideo: React.FC = () => ( { - if (!user) return; - if (isAiQueryParam) { - setDialog((prevDialog) => ({ ...prevDialog, isOpen: !user?.isLoggedIn })); + if (isAiQueryParam && !user?.isLoggedIn) { + setDialog((prevDialog) => ({ ...prevDialog, isOpen: true })); } - }, [isAiQueryParam, user]); + }, [isAiQueryParam, user?.isLoggedIn]); function goToLocation() { const currentUrl = `${window.location.origin}${router.asPath}`; diff --git a/lib/queries/builder.ts b/lib/queries/builder.ts index 2486d8a0..ee1e487f 100644 --- a/lib/queries/builder.ts +++ b/lib/queries/builder.ts @@ -50,16 +50,16 @@ export function buildQuery(obj: BuildQueryProps) { queries: [ { query_string: { - default_operator: "AND", - /** * Reference available index keys/vars: * https://github.com/nulib/meadow/blob/deploy/staging/app/priv/elasticsearch/v2/settings/work.json */ + default_operator: "AND", fields: [ - "title^5", // "all_text", // we feel like neural should handle the all_text part + "title^5", + // "all_text", // we feel like neural should handle the all_text part "all_controlled_labels", - "all_ids^5", + "all_ids^5", // boost the all_ids field ], query: term, }, @@ -67,7 +67,6 @@ export function buildQuery(obj: BuildQueryProps) { { neural: { embedding: { - // if term has no value, the API returns a 400 error filter: { bool: { filter: buildFacetFilters(urlFacets), @@ -75,7 +74,7 @@ export function buildQuery(obj: BuildQueryProps) { }, k: size || 20, model_id: process.env.NEXT_PUBLIC_OPENSEARCH_MODEL_ID, - query_text: term, + query_text: term, // if term has no value, the API returns a 400 error }, }, }, diff --git a/package-lock.json b/package-lock.json index 09a64ba1..deea5f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4096,16 +4096,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz", - "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz", + "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/type-utils": "7.11.0", - "@typescript-eslint/utils": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/type-utils": "7.9.0", + "@typescript-eslint/utils": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4129,16 +4129,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz", - "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz", + "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==", "dev": true, "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "debug": "^4.3.4" }, "engines": { @@ -4158,13 +4158,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", - "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz", + "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0" + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4175,13 +4175,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", - "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz", + "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.11.0", - "@typescript-eslint/utils": "7.11.0", + "@typescript-eslint/typescript-estree": "7.9.0", + "@typescript-eslint/utils": "7.9.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4202,9 +4202,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", - "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz", + "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4215,13 +4215,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", - "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz", + "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/visitor-keys": "7.11.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/visitor-keys": "7.9.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -4255,15 +4255,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", - "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz", + "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.11.0", - "@typescript-eslint/types": "7.11.0", - "@typescript-eslint/typescript-estree": "7.11.0" + "@typescript-eslint/scope-manager": "7.9.0", + "@typescript-eslint/types": "7.9.0", + "@typescript-eslint/typescript-estree": "7.9.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4277,12 +4277,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", - "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz", + "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/types": "7.9.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -8010,9 +8010,9 @@ } }, "node_modules/framer-motion": { - "version": "11.2.9", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.9.tgz", - "integrity": "sha512-gfxNSkp4dC3vpy2hGNQK3K9bNOKwfasqOhrqvmZzYxCPSJ9Tpv/9JlCkeCMgFdKefgPr8+JiouGjVmaDzu750w==", + "version": "11.2.10", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.10.tgz", + "integrity": "sha512-/gr3PLZUVFCc86a9MqCUboVrALscrdluzTb3yew+2/qKBU8CX6nzs918/SRBRCqaPbx0TZP10CB6yFgK2C5cYQ==", "dependencies": { "tslib": "^2.4.0" }, @@ -12218,9 +12218,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", - "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", + "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", diff --git a/pages/search.tsx b/pages/search.tsx index 53097ed2..b120dc50 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -1,25 +1,34 @@ +import * as Tabs from "@radix-ui/react-tabs"; + import { NoResultsMessage, ResultsMessage, ResultsWrapper, + StyledResponseWrapper, } from "@/components/Search/Search.styled"; import React, { useEffect, useState } from "react"; +import { ActiveTab } from "@/types/context/search-context"; import { ApiSearchRequestBody } from "@/types/api/request"; import { ApiSearchResponse } from "@/types/api/response"; +import { Button } from "@nulib/design-system"; import Chat from "@/components/Chat/Chat"; import Container from "@/components/Shared/Container"; import { DC_API_SEARCH_URL } from "@/lib/constants/endpoints"; -import Facets from "@/components/Facets/Facets"; import Grid from "@/components/Grid/Grid"; import { HEAD_META } from "@/lib/constants/head-meta"; import Head from "next/head"; import Heading from "@/components/Heading/Heading"; +import Icon from "@/components/Shared/Icon"; +import { IconSparkles } from "@/components/Shared/SVG/Icons"; import Layout from "@/components/layout"; import { NextPage } from "next"; import { PRODUCTION_URL } from "@/lib/constants/endpoints"; import PaginationAltCounts from "@/components/Search/PaginationAltCounts"; +import SearchOptions from "@/components/Search/Options"; import SearchSimilar from "@/components/Search/Similar"; +import { SpinLoader } from "@/components/Shared/Loader.styled"; +import { StyledResponseActions } from "@/components/Chat/Response/Response.styled"; import { UserContext } from "@/context/user-context"; import { apiPostRequest } from "@/lib/dc-api"; import axios from "axios"; @@ -31,6 +40,7 @@ import { parseUrlFacets } from "@/lib/utils/facet-helpers"; import { pluralize } from "@/lib/utils/count-helpers"; import useQueryParams from "@/hooks/useQueryParams"; import { useRouter } from "next/router"; +import { useSearchState } from "@/context/search-context"; type RequestState = { data: ApiSearchResponse | null; @@ -43,9 +53,18 @@ const SearchPage: NextPage = () => { const router = useRouter(); const { user } = React.useContext(UserContext); - const { ai } = useQueryParams(); - const showChatResponse = user?.isLoggedIn && ai; + const queryParams = useQueryParams(); + const { searchTerm, ai } = queryParams; + const { searchState, searchDispatch } = useSearchState(); + const { + activeTab, + chat: { question }, + } = searchState; + + const showStreamedResponse = Boolean(user?.isLoggedIn && ai); + const checkScrollRef = React.useRef<() => void>(); + const [isStreamComplete, setIsStreamComplete] = useState(false); const [requestState, setRequestState] = useState({ data: null, error: "", @@ -63,6 +82,10 @@ const SearchPage: NextPage = () => { }, }); + useEffect(() => { + setIsStreamComplete(question === searchTerm); + }, [question, searchTerm]); + /** * Make requests to the search API endpoint */ @@ -165,6 +188,42 @@ const SearchPage: NextPage = () => { }); } + function handleResultsTab() { + if (window.scrollY === 0) { + searchDispatch({ type: "updateActiveTab", activeTab: "results" }); + return; + } + + window.scrollTo({ behavior: "instant", top: 0 }); + + const checkScroll = () => { + if (window.scrollY === 0) { + searchDispatch({ type: "updateActiveTab", activeTab: "results" }); + window.removeEventListener("scroll", checkScroll); + } + }; + + checkScrollRef.current = checkScroll; + window.addEventListener("scroll", checkScroll); + } + + function handleNewQuestion() { + const input = document.getElementById("dc-search") as HTMLInputElement; + if (input) { + input.focus(); + input.value = ""; + } + } + + useEffect(() => { + return () => { + // Clean up the event listener when the component unmounts + if (checkScrollRef.current) { + window.removeEventListener("scroll", checkScrollRef.current); + } + }; + }, []); + const { data: apiData, error, loading } = requestState; const totalResults = requestState.data?.pagination.total_hits; @@ -186,47 +245,95 @@ const SearchPage: NextPage = () => { data-testid="search-page-wrapper" title={HEAD_META["SEARCH"].title} > - - Northwestern - - {similarTo?.visible && ( - - )} - - {showChatResponse && } - - - - - - {loading && <>} - {error &&

{error}

} - {apiData && ( - <> - {totalResults ? ( - - {pluralize("result", totalResults)} - - ) : ( - - Your search did not match any results.{" "} - Please try broadening your search terms or adjusting your - filters. - - )} - - {totalResults ? ( - - ) : ( - <> - )} - - )} - -
+ + + Northwestern + + {similarTo?.visible && ( + + )} + + + searchDispatch({ + type: "updateActiveTab", + activeTab: value as ActiveTab, + }) + } + > + + + + + + AI Response + + + {Number.isInteger(totalResults) ? ( + <>View {pluralize("Result", totalResults || 0)} + ) : ( + + )} + + + } + activeTab={activeTab} + renderTabList={showStreamedResponse} + /> + + + {isStreamComplete && ( + + + + + + + )} + + + + + {loading && <>} + {error &&

{error}

} + {apiData && ( + <> + {totalResults ? ( + + {pluralize("Result", totalResults)} + + ) : ( + + + Your search did not match any results. + {" "} + Please try broadening your search terms or adjusting + your filters. + + )} + + {totalResults ? ( + + ) : ( + <> + )} + + )} +
+
+
+
+
); diff --git a/types/context/search-context.ts b/types/context/search-context.ts index a252ea31..1d2fb3ab 100644 --- a/types/context/search-context.ts +++ b/types/context/search-context.ts @@ -1,7 +1,16 @@ import { ApiResponseAggregation } from "@/types/api/response"; +import { Work } from "@nulib/dcapi-types"; + +export type ActiveTab = "stream" | "results"; export interface SearchContextStore { + activeTab: ActiveTab; aggregations?: ApiResponseAggregation; + chat: { + answer: string; + documents: Work[]; + question: string; + }; searchFixed: boolean; } From a10af13206c8d38f1d6989afa02a8a0765408ad9 Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Mon, 17 Jun 2024 11:37:32 -0400 Subject: [PATCH 28/57] Address linting issues. --- components/Search/Search.tsx | 2 +- pages/search.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/components/Search/Search.tsx b/components/Search/Search.tsx index a5ca71e1..3be215eb 100644 --- a/components/Search/Search.tsx +++ b/components/Search/Search.tsx @@ -54,8 +54,8 @@ const Search: React.FC = ({ isSearchActive }) => { }); searchDispatch({ - type: "updateActiveTab", activeTab: ai ? "stream" : "results", + type: "updateActiveTab", }); router.push({ diff --git a/pages/search.tsx b/pages/search.tsx index b120dc50..42c2333b 100644 --- a/pages/search.tsx +++ b/pages/search.tsx @@ -190,7 +190,7 @@ const SearchPage: NextPage = () => { function handleResultsTab() { if (window.scrollY === 0) { - searchDispatch({ type: "updateActiveTab", activeTab: "results" }); + searchDispatch({ activeTab: "results", type: "updateActiveTab" }); return; } @@ -198,7 +198,7 @@ const SearchPage: NextPage = () => { const checkScroll = () => { if (window.scrollY === 0) { - searchDispatch({ type: "updateActiveTab", activeTab: "results" }); + searchDispatch({ activeTab: "results", type: "updateActiveTab" }); window.removeEventListener("scroll", checkScroll); } }; @@ -260,8 +260,8 @@ const SearchPage: NextPage = () => { value={activeTab} onValueChange={(value) => searchDispatch({ - type: "updateActiveTab", activeTab: value as ActiveTab, + type: "updateActiveTab", }) } > From 3248e8271f6c887ca7ad5fe208f3cdab258656cc Mon Sep 17 00:00:00 2001 From: Mat Jordan Date: Fri, 21 Jun 2024 13:27:06 -0400 Subject: [PATCH 29/57] Add feedback form for chat responses. --- components/Chat/Chat.tsx | 62 ++++++++-- components/Chat/Feedback/Feedback.tsx | 116 +++++++++++++++++++ components/Chat/Feedback/OptIn.test.tsx | 39 +++++++ components/Chat/Feedback/OptIn.tsx | 23 ++++ components/Chat/Feedback/Option.test.tsx | 21 ++++ components/Chat/Feedback/Option.tsx | 88 ++++++++++++++ components/Chat/Feedback/TextArea.tsx | 27 +++++ components/Chat/Response/Response.styled.tsx | 2 +- pages/search.tsx | 72 +----------- 9 files changed, 376 insertions(+), 74 deletions(-) create mode 100644 components/Chat/Feedback/Feedback.tsx create mode 100644 components/Chat/Feedback/OptIn.test.tsx create mode 100644 components/Chat/Feedback/OptIn.tsx create mode 100644 components/Chat/Feedback/Option.test.tsx create mode 100644 components/Chat/Feedback/Option.tsx create mode 100644 components/Chat/Feedback/TextArea.tsx diff --git a/components/Chat/Chat.tsx b/components/Chat/Chat.tsx index 9945464d..66b6f607 100644 --- a/components/Chat/Chat.tsx +++ b/components/Chat/Chat.tsx @@ -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(); @@ -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 ( - + <> + + {answer && ( + <> + + + + + + + + + )} + ); }; diff --git a/components/Chat/Feedback/Feedback.tsx b/components/Chat/Feedback/Feedback.tsx new file mode 100644 index 00000000..bb7fe701 --- /dev/null +++ b/components/Chat/Feedback/Feedback.tsx @@ -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 ( + + + + + + {isSubmitted ? ( + + Solid. Thanks for submitting! + + ) : ( + + + + + + + + + + + + + )} + + + ); +}; + +/* 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; diff --git a/components/Chat/Feedback/OptIn.test.tsx b/components/Chat/Feedback/OptIn.test.tsx new file mode 100644 index 00000000..69344483 --- /dev/null +++ b/components/Chat/Feedback/OptIn.test.tsx @@ -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: "foo@bar.com", + sub: "123", + isLoggedIn: true, + isReadingRoom: false, + }, +}; + +describe("ChatFeedbackOptIn", () => { + it("renders a checkbox input with the user email value", () => { + render( + + + + ); + + const checkbox = screen.getByRole("checkbox"); + expect(checkbox).toHaveAttribute("value", "foo@bar.com"); + expect(checkbox).toBeInTheDocument(); + }); + + it("renders a label", () => { + render( + + + + ); + const label = screen.getByText(/please follow up with me/i); + expect(label).toBeInTheDocument(); + }); +}); diff --git a/components/Chat/Feedback/OptIn.tsx b/components/Chat/Feedback/OptIn.tsx new file mode 100644 index 00000000..724a22e1 --- /dev/null +++ b/components/Chat/Feedback/OptIn.tsx @@ -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 ( + + Please follow + up with me regarding this issue. + + ); +}; + +/* eslint-disable sort-keys */ +const StyledChatFeedbackOptIn = styled("label", { + display: "block", + margin: "$gr3 0", + fontSize: "$gr2", +}); + +export default ChatFeedbackOptIn; diff --git a/components/Chat/Feedback/Option.test.tsx b/components/Chat/Feedback/Option.test.tsx new file mode 100644 index 00000000..dcd0c3ea --- /dev/null +++ b/components/Chat/Feedback/Option.test.tsx @@ -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(); + 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(); + const label = screen.getByText(/this is a test/i); + expect(label).toBeInTheDocument(); + }); +}); diff --git a/components/Chat/Feedback/Option.tsx b/components/Chat/Feedback/Option.tsx new file mode 100644 index 00000000..a5fb0ea3 --- /dev/null +++ b/components/Chat/Feedback/Option.tsx @@ -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(null); + + function handleOnChange() { + setIsChecked(inputRef?.current?.checked ?? false); + } + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === " ") { + event.preventDefault(); + setIsChecked(!isChecked); + } + }; + + const handleClick = () => { + setIsChecked(!isChecked); + }; + + return ( + + + {label} + + ); +}; + +/* 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; diff --git a/components/Chat/Feedback/TextArea.tsx b/components/Chat/Feedback/TextArea.tsx new file mode 100644 index 00000000..36846755 --- /dev/null +++ b/components/Chat/Feedback/TextArea.tsx @@ -0,0 +1,27 @@ +import { styled } from "@/stitches.config"; + +const ChatFeedbackTextArea = () => { + return ( + + Add additional specific details (optional) +