From 4681e4d280c2677a8997309811c7c1b088609706 Mon Sep 17 00:00:00 2001 From: Tom Wise <79859203+tomwisecodes@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:23:20 +0100 Subject: [PATCH 01/18] chore: inital work --- apps/nextjs/package.json | 2 +- .../Chat/chat-lessonPlanDisplay.tsx | 2 +- .../Chat/drop-down-section/action-button.tsx | 61 ++++++++ .../Chat/drop-down-section/chat-section.tsx | 34 +++++ .../drop-down-form-wrapper.tsx | 93 ++++++++++++ .../Chat/drop-down-section/flag-button.tsx | 139 ++++++++++++++++++ .../index.tsx} | 74 +++++----- .../Chat/drop-down-section/modify-button.tsx | 81 ++++++++++ packages/api/src/router/appSessions.ts | 4 + pnpm-lock.yaml | 129 ++++++++++++++-- 10 files changed, 564 insertions(+), 55 deletions(-) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/action-button.tsx create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/chat-section.tsx create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/drop-down-form-wrapper.tsx create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/flag-button.tsx rename apps/nextjs/src/components/AppComponents/Chat/{chat-dropdownsection.tsx => drop-down-section/index.tsx} (75%) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 5dfa01c00..fd8c6cca1 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -37,7 +37,7 @@ "@oakai/exports": "*", "@oakai/logger": "*", "@oakai/prettier-config": "*", - "@oaknational/oak-components": "^1.0.0", + "@oaknational/oak-components": "^1.26.0", "@oaknational/oak-consent-client": "^2.1.0", "@prisma/client": "^5.13.0", "@prisma/extension-accelerate": "^1.0.0", diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.tsx index f0dc7278e..990b42b35 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.tsx @@ -7,7 +7,7 @@ import { cva } from "class-variance-authority"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import Skeleton from "../common/Skeleton"; -import DropDownSection from "./chat-dropdownsection"; +import DropDownSection from "./drop-down-section"; import { GuidanceRequired } from "./guidance-required"; // @todo move these somewhere more sensible diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/action-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/action-button.tsx new file mode 100644 index 000000000..4caa288c3 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/action-button.tsx @@ -0,0 +1,61 @@ +import { Children, useState } from "react"; + +import { + OakBox, + OakSpan, + OakTertiaryButton, + OakTooltip, +} from "@oaknational/oak-components"; + +const ActionButton = ({ + children, + onClick, + tooltip, +}: { + children: React.ReactNode; + onClick: () => void; + tooltip: string; +}) => { + const [showTooltip, setShowTooltip] = useState(false); + + return ( + +
{ + await waitThenExecute(200).then(() => { + setShowTooltip(true); + }); + }} + onMouseLeave={() => { + setShowTooltip(false); + }} + > + + + {children} + + +
+
+ ); +}; + +async function waitThenExecute(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + +export default ActionButton; diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/chat-section.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/chat-section.tsx new file mode 100644 index 000000000..ac6dd5aa1 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/chat-section.tsx @@ -0,0 +1,34 @@ +import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; +import { humanizeCamelCaseString } from "@oakai/core/src/utils/humanizeCamelCaseString"; +import { OakFlex } from "@oaknational/oak-components"; +import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; + +import { MemoizedReactMarkdownWithStyles } from "../markdown"; +import FlagButton from "./flag-button"; +import ModifyButton from "./modify-button"; + +const ChatSection = ({ + objectKey, + value, +}: { + objectKey: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + value: any; +}) => { + return ( + + + + + + + + ); +}; + +export default ChatSection; diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/drop-down-form-wrapper.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/drop-down-form-wrapper.tsx new file mode 100644 index 000000000..1d2910b08 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/drop-down-form-wrapper.tsx @@ -0,0 +1,93 @@ +import { useEffect, useRef } from "react"; + +import { + OakBox, + OakFlex, + OakP, + OakSmallPrimaryButton, +} from "@oaknational/oak-components"; + +export const DropDownFormWrapper = ({ + children, + onClickActions, + setIsOpen, + selectedRadio, + title, + buttonText, + isOpen, + dropdownRef, +}: { + children: React.ReactNode; + onClickActions: (option: string) => void; + setIsOpen: (value: boolean) => void; + selectedRadio: string | null; + title: string; + buttonText: string; + isOpen: boolean; + dropdownRef: React.RefObject; +}) => { + const firstButtonRef = useRef(null); + useEffect(() => { + if (isOpen && firstButtonRef.current) { + firstButtonRef.current.focus(); + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setIsOpen(false); + } + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setIsOpen(false); + } + }; + + document.addEventListener("keydown", handleKeyDown); + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen]); + + return ( +
{ + e.preventDefault(); + if (selectedRadio) { + onClickActions(selectedRadio); + } + setIsOpen(false); + }} + > + + {title} + {children} + + {buttonText} + + +
+ ); +}; + +export default DropDownFormWrapper; diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/flag-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/flag-button.tsx new file mode 100644 index 000000000..4704d3586 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/flag-button.tsx @@ -0,0 +1,139 @@ +import { useEffect, useRef, useState } from "react"; + +import { + OakBox, + OakP, + OakRadioButton, + OakRadioGroup, + OakSpan, + OakTertiaryButton, + OakTextInput, +} from "@oaknational/oak-components"; +import styled from "styled-components"; + +import { trpc } from "@/utils/trpc"; + +import ActionButton from "./action-button"; +import { DropDownFormWrapper } from "./drop-down-form-wrapper"; + +const flagOptions = [ + "Inappropriate", + "Inaccurate", + "Too hard", + "Too easy", + "Other", +]; + +type FlagButtonOptions = (typeof flagOptions)[number]; + +const FlagButton = ({}) => { + const dropdownRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [selectedRadio, setSelectedRadio] = useState(null); + const [displayTextBox, setDisplayTextBox] = + useState(null); + const [userFeedbackText, setUserFeedbackText] = useState(""); + + const { error, data, isLoading } = + trpc.chat.appSessions.flagSection.useQuery(); + + async function flagSectionContent(value: string) { + const userFeedback = { + flat: selectedRadio, + feedback: userFeedbackText, + }; + console.log("Flag content", userFeedback); + } + + useEffect(() => { + !isOpen && setDisplayTextBox(null); + }, [isOpen, setDisplayTextBox]); + + return ( + + setIsOpen(!isOpen)} + > + Flag + + + {isOpen && ( + + + {flagOptions.map((option) => ( + + ))} + + + )} + + ); +}; + +const FlagButtonFormItem = ({ + option, + setSelectedRadio, + setDisplayTextBox, + displayTextBox, + setUserFeedbackText, +}: { + option: string; + setSelectedRadio: (value: string) => void; + setDisplayTextBox: (value: FlagButtonOptions | null) => void; + displayTextBox: FlagButtonOptions | null; + setUserFeedbackText: (value: string) => void; +}) => { + return ( + <> + { + setDisplayTextBox(option); + setSelectedRadio(option); + }} + /> + {displayTextBox === option && ( + <> + Provide details below: +