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: + setUserFeedbackText(e.target.value)} /> + > + )} + > + ); +}; + +const TextArea = styled.textarea` + width: 100%; + height: 70px; + padding: 12px; + border: 2px solid black; + border-opacity: 0.5; + resize: none; + border-radius: 4px; +`; + +export default FlagButton; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx similarity index 75% rename from apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx rename to apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx index f8c599961..4b35f65a8 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx @@ -1,14 +1,19 @@ import { useEffect, useRef, useState } from "react"; import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; +import { OakBox, OakFlex, OakP } from "@oaknational/oak-components"; import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; +import styled from "styled-components"; import { Icon } from "@/components/Icon"; import LoadingWheel from "@/components/LoadingWheel"; import { scrollToRef } from "@/utils/scrollToRef"; -import Skeleton from "../common/Skeleton"; -import { MemoizedReactMarkdownWithStyles } from "./markdown"; +import Skeleton from "../../common/Skeleton"; +import { MemoizedReactMarkdownWithStyles } from "../markdown"; +import ChatSection from "./chat-section"; +import FlagButton from "./flag-button"; +import ModifyButton from "./modify-button"; const DropDownSection = ({ objectKey, @@ -79,34 +84,31 @@ const DropDownSection = ({ ]); return ( - - - + + {status === "empty" && } {status === "isStreaming" && } {status === "isLoaded" && } - + - setIsOpen(!isOpen)} - className="flex w-full justify-between" - > - - {humanizeCamelCaseString(objectKey)} - - - - + setIsOpen(!isOpen)}> + + {humanizeCamelCaseString(objectKey)} + + + + {isOpen && ( {status === "isLoaded" ? ( - <> - - > + ) : ( Loading @@ -114,27 +116,7 @@ const DropDownSection = ({ )} )} - - ); -}; - -const ChatSection = ({ - objectKey, - value, -}: { - objectKey: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; -}) => { - return ( - - - + ); }; @@ -162,4 +144,14 @@ export function humanizeCamelCaseString(str: string) { }); } +const FullWidthButton = styled.button` + width: 100%; +`; + +const DropDownSectionWrapper = styled(OakBox)` + &:first-child { + border-top: 2px solid black; + } +`; + export default DropDownSection; diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx new file mode 100644 index 000000000..eb8def970 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx @@ -0,0 +1,81 @@ +import { useRef, useState } from "react"; + +import { + OakBox, + OakRadioButton, + OakRadioGroup, + OakSpan, + OakTertiaryButton, +} from "@oaknational/oak-components"; + +import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; + +import ActionButton from "./action-button"; +import { DropDownFormWrapper } from "./drop-down-form-wrapper"; + +const ModifyButton = ({ section }: { section: string }) => { + const dropdownRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [selectedRadio, setSelectedRadio] = useState(null); + const modifyOptions = [ + "Make it easier", + "Make it harder", + "Shorten content", + "Add more detail", + "Other", + ]; + const chat = useLessonChat(); + const { append } = chat; + + async function appendToChat(value: string) { + await append({ + id: "lessonPlan", + content: `For the ${section}, ${value}`, + role: "user", + }); + } + + return ( + + setIsOpen(!isOpen)} + tooltip="Aila can help improve this section" + > + Modify {section} + + + {isOpen && ( + + + {modifyOptions.map((option) => ( + { + setSelectedRadio(option); + }} + /> + ))} + + + )} + + ); +}; + +export default ModifyButton; diff --git a/packages/api/src/router/appSessions.ts b/packages/api/src/router/appSessions.ts index 0d054145c..418fce8b4 100644 --- a/packages/api/src/router/appSessions.ts +++ b/packages/api/src/router/appSessions.ts @@ -344,4 +344,8 @@ export const appSessionsRouter = router({ return chat; }), + flagSection: protectedProcedure.query(async ({ ctx }) => { + console.log("flagging section"); + return "flagged"; + }), }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 241ce1c13..5e0132823 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,8 +125,8 @@ importers: specifier: '*' version: link:../../packages/prettier-config '@oaknational/oak-components': - specifier: ^1.0.0 - version: 1.0.0(@types/jest@29.5.12)(jest@29.7.0)(next-cloudinary@5.20.0)(next@14.2.5)(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.7)(styled-components@5.3.11) + specifier: ^1.26.0 + version: 1.26.0(next-cloudinary@5.20.0)(next@14.2.5)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11) '@oaknational/oak-consent-client': specifier: ^2.1.0 version: 2.1.0(react-dom@18.2.0)(react@18.2.0)(zod@3.23.5) @@ -786,6 +786,7 @@ packages: /@adobe/css-tools@4.4.0: resolution: {integrity: sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==} + dev: true /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -1294,6 +1295,7 @@ packages: dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.7 + dev: true /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.5): resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} @@ -1379,6 +1381,7 @@ packages: dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.24.7 + dev: true /@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.24.5): resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} @@ -2390,6 +2393,7 @@ packages: /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + dev: true /@chromatic-com/storybook@1.6.1(react@18.2.0): resolution: {integrity: sha512-x1x1NB3j4xpfeSWKr96emc+7ZvfsvH+/WVb3XCjkB24PPbT8VZXb3mJSAQMrSzuQ8+eQE9kDogYHH9Fj3tb/Cw==} @@ -3223,10 +3227,12 @@ packages: get-package-type: 0.1.0 js-yaml: 3.14.1 resolve-from: 5.0.0 + dev: true /@istanbuljs/schema@0.1.3: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + dev: true /@jest/console@29.7.0: resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} @@ -3238,6 +3244,7 @@ packages: jest-message-util: 29.7.0 jest-util: 29.7.0 slash: 3.0.0 + dev: true /@jest/core@29.7.0(ts-node@10.9.2): resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} @@ -3280,6 +3287,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /@jest/environment@29.7.0: resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} @@ -3289,6 +3297,7 @@ packages: '@jest/types': 29.6.3 '@types/node': 18.18.5 jest-mock: 29.7.0 + dev: true /@jest/expect-utils@29.7.0: resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} @@ -3304,6 +3313,7 @@ packages: jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color + dev: true /@jest/fake-timers@29.7.0: resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} @@ -3315,6 +3325,7 @@ packages: jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 + dev: true /@jest/globals@29.7.0: resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} @@ -3326,6 +3337,7 @@ packages: jest-mock: 29.7.0 transitivePeerDependencies: - supports-color + dev: true /@jest/reporters@29.7.0: resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} @@ -3362,6 +3374,7 @@ packages: v8-to-istanbul: 9.3.0 transitivePeerDependencies: - supports-color + dev: true /@jest/schemas@29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} @@ -3376,6 +3389,7 @@ packages: '@jridgewell/trace-mapping': 0.3.25 callsites: 3.1.0 graceful-fs: 4.2.11 + dev: true /@jest/test-result@29.7.0: resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} @@ -3385,6 +3399,7 @@ packages: '@jest/types': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 collect-v8-coverage: 1.0.2 + dev: true /@jest/test-sequencer@29.7.0: resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} @@ -3394,6 +3409,7 @@ packages: graceful-fs: 4.2.11 jest-haste-map: 29.7.0 slash: 3.0.0 + dev: true /@jest/transform@29.7.0: resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} @@ -3416,6 +3432,7 @@ packages: write-file-atomic: 4.0.2 transitivePeerDependencies: - supports-color + dev: true /@jest/types@29.6.3: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} @@ -3930,8 +3947,8 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - /@oaknational/oak-components@1.0.0(@types/jest@29.5.12)(jest@29.7.0)(next-cloudinary@5.20.0)(next@14.2.5)(react-dom@18.2.0)(react@18.2.0)(storybook@8.2.7)(styled-components@5.3.11): - resolution: {integrity: sha512-LBjIV9e0SF7dByOTumDEQd5iJXx+ob5h1dlSFHMpaoiDM3yoQoQzH4GCAf1EIjf4ZBdW4ly5SJgIcdJjrK84fA==} + /@oaknational/oak-components@1.26.0(next-cloudinary@5.20.0)(next@14.2.5)(react-dom@18.2.0)(react@18.2.0)(styled-components@5.3.11): + resolution: {integrity: sha512-V/Z4Ceq5E3W0S/ed9D1PJeCMuEw41D9v3TeIRjRuznzTtqNd/cS0M5BIJUBK3Ok2WdF/e97ElaiHTp92gEIZgA==} peerDependencies: next: 13.4.4 - 14 next-cloudinary: ^5.20.0 @@ -3939,19 +3956,11 @@ packages: react-dom: ^18.2.0 styled-components: ^5.3.11 dependencies: - '@storybook/test': 8.2.7(@types/jest@29.5.12)(jest@29.7.0)(storybook@8.2.7) next: 14.2.5(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(@playwright/test@1.45.0)(react-dom@18.2.0)(react@18.2.0) next-cloudinary: 5.20.0(next@14.2.5)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) styled-components: 5.3.11(@babel/core@7.24.5)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0) - transitivePeerDependencies: - - '@jest/globals' - - '@types/bun' - - '@types/jest' - - jest - - storybook - - vitest dev: false /@oaknational/oak-consent-client@2.1.0(react-dom@18.2.0)(react@18.2.0)(zod@3.23.5): @@ -6690,11 +6699,13 @@ packages: resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} dependencies: type-detect: 4.0.8 + dev: true /@sinonjs/fake-timers@10.3.0: resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} dependencies: '@sinonjs/commons': 3.0.1 + dev: true /@slack/logger@4.0.0: resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} @@ -7078,6 +7089,7 @@ packages: /@storybook/global@5.0.0: resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==} + dev: true /@storybook/icons@1.2.10(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-310apKdDcjbbX2VSLWPwhEwAgjxTzVagrwucVZIdGPErwiAppX8KvBuWZgPo+rQLVrtH8S+pw1dbUwjcE6d7og==} @@ -7099,6 +7111,7 @@ packages: '@vitest/utils': 1.6.0 storybook: 8.2.7 util: 0.12.5 + dev: true /@storybook/manager-api@8.2.7(storybook@8.2.7): resolution: {integrity: sha512-BXjz6eNl1GyFcMwzRQTIokslcIY71AYblJUscPcy03X93oqI0GjFVa1xuSMwYw/oXWn7SHhKmqtqEG19lvBGRQ==} @@ -7346,6 +7359,7 @@ packages: - '@types/jest' - jest - vitest + dev: true /@storybook/testing-react@2.0.1(@storybook/client-logger@7.6.20)(@storybook/preview-api@7.6.20)(@storybook/react@8.2.7)(@storybook/types@7.6.20)(react@18.2.0): resolution: {integrity: sha512-D0fT7f0TUU8+usR+0NSyCYQCYDWernQqGZou32ISsoBkY9IiH1JMY4hQE8zUaGozu9eMlaa2wieWIMsjWovpdw==} @@ -7443,6 +7457,7 @@ packages: dom-accessibility-api: 0.5.16 lz-string: 1.5.0 pretty-format: 27.5.1 + dev: true /@testing-library/jest-dom@6.4.5(@types/jest@29.5.12)(jest@29.7.0): resolution: {integrity: sha512-AguB9yvTXmCnySBP1lWjfNNUwpbElsaQ567lt2VdGqAdHtpieLgjmcVyv1q7PMIvLbgpDdkWV5Ydv3FEejyp2A==} @@ -7475,6 +7490,7 @@ packages: jest: 29.7.0(@types/node@18.18.5)(ts-node@10.9.2) lodash: 4.17.21 redent: 3.0.0 + dev: true /@testing-library/jest-dom@6.4.8: resolution: {integrity: sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==} @@ -7520,6 +7536,7 @@ packages: '@testing-library/dom': '>=7.21.4' dependencies: '@testing-library/dom': 10.1.0 + dev: true /@tootallnate/once@2.0.0: resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} @@ -7617,6 +7634,7 @@ packages: /@types/aria-query@5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + dev: true /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -7626,22 +7644,26 @@ packages: '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.20.6 + dev: true /@types/babel__generator@7.6.8: resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} dependencies: '@babel/types': 7.24.5 + dev: true /@types/babel__template@7.4.4: resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} dependencies: '@babel/parser': 7.24.5 '@babel/types': 7.24.5 + dev: true /@types/babel__traverse@7.20.6: resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} dependencies: '@babel/types': 7.24.5 + dev: true /@types/body-parser@1.19.5: resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} @@ -7737,6 +7759,7 @@ packages: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: '@types/node': 18.18.5 + dev: true /@types/hast@2.3.10: resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} @@ -8244,11 +8267,13 @@ packages: '@vitest/spy': 1.6.0 '@vitest/utils': 1.6.0 chai: 4.4.1 + dev: true /@vitest/spy@1.6.0: resolution: {integrity: sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==} dependencies: tinyspy: 2.2.1 + dev: true /@vitest/utils@1.6.0: resolution: {integrity: sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==} @@ -8257,6 +8282,7 @@ packages: estree-walker: 3.0.3 loupe: 2.3.7 pretty-format: 29.7.0 + dev: true /@vue/compiler-core@3.4.26: resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==} @@ -8689,6 +8715,7 @@ packages: engines: {node: '>=8'} dependencies: type-fest: 0.21.3 + dev: true /ansi-escapes@6.2.0: resolution: {integrity: sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==} @@ -8935,6 +8962,7 @@ packages: /assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true /ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -9072,6 +9100,7 @@ packages: slash: 3.0.0 transitivePeerDependencies: - supports-color + dev: true /babel-loader@9.1.3(@babel/core@7.24.5)(webpack@5.93.0): resolution: {integrity: sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==} @@ -9097,6 +9126,7 @@ packages: test-exclude: 6.0.0 transitivePeerDependencies: - supports-color + dev: true /babel-plugin-jest-hoist@29.6.3: resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} @@ -9106,6 +9136,7 @@ packages: '@babel/types': 7.24.5 '@types/babel__core': 7.20.5 '@types/babel__traverse': 7.20.6 + dev: true /babel-plugin-polyfill-corejs2@0.4.11(@babel/core@7.24.5): resolution: {integrity: sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==} @@ -9174,6 +9205,7 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.5) '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.5) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.5) + dev: true /babel-preset-jest@29.6.3(@babel/core@7.24.5): resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} @@ -9184,6 +9216,7 @@ packages: '@babel/core': 7.24.5 babel-plugin-jest-hoist: 29.6.3 babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.5) + dev: true /bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -9426,6 +9459,7 @@ packages: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: node-int64: 0.4.0 + dev: true /buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -9579,6 +9613,7 @@ packages: loupe: 2.3.7 pathval: 1.1.1 type-detect: 4.0.8 + dev: true /chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -9609,6 +9644,7 @@ packages: /char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + dev: true /character-entities-legacy@1.1.4: resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} @@ -9638,6 +9674,7 @@ packages: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: get-func-name: 2.0.2 + dev: true /cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -9880,6 +9917,7 @@ packages: /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + dev: true /code-block-writer@11.0.3: resolution: {integrity: sha512-NiujjUFB4SwScJq2bwbYUtXbZhBSlY6vYzm++3Q6oC+U+injTqfPYFK8wS9COOmb2lueqp0ZRB4nK1VYeHgNyw==} @@ -9909,6 +9947,7 @@ packages: /collect-v8-coverage@1.0.2: resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + dev: true /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -10266,6 +10305,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -10402,6 +10442,7 @@ packages: /css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + dev: true /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} @@ -10539,12 +10580,14 @@ packages: peerDependenciesMeta: babel-plugin-macros: optional: true + dev: true /deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} dependencies: type-detect: 4.0.8 + dev: true /deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} @@ -10584,6 +10627,7 @@ packages: /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + dev: true /default-browser-id@3.0.0: resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} @@ -10714,6 +10758,7 @@ packages: /detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} + dev: true /detect-newline@4.0.1: resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==} @@ -10794,9 +10839,11 @@ packages: /dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + dev: true /dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dev: true /dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -10955,6 +11002,7 @@ packages: /emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + dev: true /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -11702,6 +11750,7 @@ packages: /exit@0.1.2: resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} engines: {node: '>= 0.8.0'} + dev: true /expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} @@ -11843,6 +11892,7 @@ packages: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} dependencies: bser: 2.1.1 + dev: true /fd-package-json@1.2.0: resolution: {integrity: sha512-45LSPmWf+gC5tdCQMNH4s9Sr00bIkiD9aN7dc5hqkrEw1geRYyDQS1v1oMHAW3ysfxfndqGsrDREHHjNNbKUfA==} @@ -12273,6 +12323,7 @@ packages: /get-func-name@2.0.2: resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: true /get-intrinsic@1.2.4: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} @@ -12292,6 +12343,7 @@ packages: /get-package-type@0.1.0: resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} engines: {node: '>=8.0.0'} + dev: true /get-stdin@9.0.0: resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} @@ -12838,6 +12890,7 @@ packages: /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + dev: true /html-minifier-terser@6.1.0: resolution: {integrity: sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==} @@ -13094,6 +13147,7 @@ packages: dependencies: pkg-dir: 4.2.0 resolve-cwd: 3.0.0 + dev: true /imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} @@ -13431,6 +13485,7 @@ packages: /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} + dev: true /is-generator-function@1.0.10: resolution: {integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==} @@ -13672,6 +13727,7 @@ packages: /istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} + dev: true /istanbul-lib-instrument@5.2.1: resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} @@ -13684,6 +13740,7 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true /istanbul-lib-instrument@6.0.2: resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} @@ -13696,6 +13753,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - supports-color + dev: true /istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} @@ -13704,6 +13762,7 @@ packages: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 + dev: true /istanbul-lib-source-maps@4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} @@ -13714,6 +13773,7 @@ packages: source-map: 0.6.1 transitivePeerDependencies: - supports-color + dev: true /istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} @@ -13721,6 +13781,7 @@ packages: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + dev: true /iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -13755,6 +13816,7 @@ packages: execa: 5.1.1 jest-util: 29.7.0 p-limit: 3.1.0 + dev: true /jest-circus@29.7.0: resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} @@ -13783,6 +13845,7 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color + dev: true /jest-cli@29.7.0(@types/node@18.18.5)(ts-node@10.9.2): resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} @@ -13810,6 +13873,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /jest-config@29.7.0(@types/node@18.18.5)(ts-node@10.9.2): resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} @@ -13850,6 +13914,7 @@ packages: transitivePeerDependencies: - babel-plugin-macros - supports-color + dev: true /jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} @@ -13865,6 +13930,7 @@ packages: engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: detect-newline: 3.1.0 + dev: true /jest-each@29.7.0: resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} @@ -13875,6 +13941,7 @@ packages: jest-get-type: 29.6.3 jest-util: 29.7.0 pretty-format: 29.7.0 + dev: true /jest-environment-jsdom@29.7.0: resolution: {integrity: sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==} @@ -13909,6 +13976,7 @@ packages: '@types/node': 18.18.5 jest-mock: 29.7.0 jest-util: 29.7.0 + dev: true /jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} @@ -13931,6 +13999,7 @@ packages: walker: 1.0.8 optionalDependencies: fsevents: 2.3.3 + dev: true /jest-leak-detector@29.7.0: resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} @@ -13938,6 +14007,7 @@ packages: dependencies: jest-get-type: 29.6.3 pretty-format: 29.7.0 + dev: true /jest-matcher-utils@29.7.0: resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} @@ -13969,6 +14039,7 @@ packages: '@jest/types': 29.6.3 '@types/node': 18.18.5 jest-util: 29.7.0 + dev: true /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} @@ -13980,10 +14051,12 @@ packages: optional: true dependencies: jest-resolve: 29.7.0 + dev: true /jest-regex-util@29.6.3: resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true /jest-resolve-dependencies@29.7.0: resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} @@ -13993,6 +14066,7 @@ packages: jest-snapshot: 29.7.0 transitivePeerDependencies: - supports-color + dev: true /jest-resolve@29.7.0: resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} @@ -14007,6 +14081,7 @@ packages: resolve: 1.22.8 resolve.exports: 2.0.2 slash: 3.0.0 + dev: true /jest-runner@29.7.0: resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} @@ -14035,6 +14110,7 @@ packages: source-map-support: 0.5.13 transitivePeerDependencies: - supports-color + dev: true /jest-runtime@29.7.0: resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} @@ -14064,6 +14140,7 @@ packages: strip-bom: 4.0.0 transitivePeerDependencies: - supports-color + dev: true /jest-snapshot@29.7.0: resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} @@ -14091,6 +14168,7 @@ packages: semver: 7.6.2 transitivePeerDependencies: - supports-color + dev: true /jest-transform-stub@2.0.0: resolution: {integrity: sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==} @@ -14117,6 +14195,7 @@ packages: jest-get-type: 29.6.3 leven: 3.1.0 pretty-format: 29.7.0 + dev: true /jest-watcher@29.7.0: resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} @@ -14130,6 +14209,7 @@ packages: emittery: 0.13.1 jest-util: 29.7.0 string-length: 4.0.2 + dev: true /jest-worker@27.5.1: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} @@ -14147,6 +14227,7 @@ packages: jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 + dev: true /jest@29.7.0(@types/node@18.18.5)(ts-node@10.9.2): resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} @@ -14167,6 +14248,7 @@ packages: - babel-plugin-macros - supports-color - ts-node + dev: true /jiti@1.19.2: resolution: {integrity: sha512-UgPQy6IN/a/5DRAVkNRgoyVQBC4BqjN/ALFKnTlCR6mJIBtEj0/X52QCMckszSNbY7RbSNa1PzhhxQzROiGHcA==} @@ -15090,6 +15172,7 @@ packages: resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: get-func-name: 2.0.2 + dev: true /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -15133,6 +15216,7 @@ packages: /lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + dev: true /magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -15164,6 +15248,7 @@ packages: engines: {node: '>=10'} dependencies: semver: 7.6.2 + dev: true /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} @@ -15172,6 +15257,7 @@ packages: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} dependencies: tmpl: 1.0.5 + dev: true /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} @@ -16142,6 +16228,7 @@ packages: /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + dev: true /node-mocks-http@1.13.0: resolution: {integrity: sha512-lArD6sJMPJ53WF50GX0nJ89B1nkV1TdMvNwq8WXXFrUXF80ujSyye1T30mgiHh4h2It0/svpF3C4kZ2OAONVlg==} @@ -16959,6 +17046,7 @@ packages: /pathval@1.1.1: resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true /pbkdf2@3.1.2: resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} @@ -17101,6 +17189,7 @@ packages: engines: {node: '>=8'} dependencies: find-up: 4.1.0 + dev: true /pkg-dir@7.0.0: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} @@ -17444,6 +17533,7 @@ packages: ansi-regex: 5.0.1 ansi-styles: 5.2.0 react-is: 17.0.2 + dev: true /pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} @@ -17579,6 +17669,7 @@ packages: /pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true /q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} @@ -17782,6 +17873,7 @@ packages: /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: true /react-is@18.1.0: resolution: {integrity: sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==} @@ -18227,6 +18319,7 @@ packages: engines: {node: '>=8'} dependencies: resolve-from: 5.0.0 + dev: true /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} @@ -18254,6 +18347,7 @@ packages: /resolve.exports@2.0.2: resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} engines: {node: '>=10'} + dev: true /resolve@1.22.4: resolution: {integrity: sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==} @@ -18902,6 +18996,7 @@ packages: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: true /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -19120,6 +19215,7 @@ packages: dependencies: char-regex: 1.0.2 strip-ansi: 6.0.1 + dev: true /string-width@2.1.1: resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} @@ -19244,6 +19340,7 @@ packages: /strip-bom@4.0.0: resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} engines: {node: '>=8'} + dev: true /strip-filename-increment@2.0.1: resolution: {integrity: sha512-+v5xsiTTsdYqkPj7qz1zlngIsjZedhHDi3xp/9bMurV8kXe9DAr732gNVqtt4X8sI3hOqS3nlFfps5gyVcux6w==} @@ -19595,6 +19692,7 @@ packages: '@istanbuljs/schema': 0.1.3 glob: 7.2.3 minimatch: 3.1.2 + dev: true /text-extensions@2.4.0: resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} @@ -19653,6 +19751,7 @@ packages: /tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} + dev: true /titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} @@ -19668,6 +19767,7 @@ packages: /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + dev: true /to-arraybuffer@1.0.1: resolution: {integrity: sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==} @@ -19990,6 +20090,7 @@ packages: /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + dev: true /type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} @@ -20003,6 +20104,7 @@ packages: /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + dev: true /type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} @@ -20480,6 +20582,7 @@ packages: '@jridgewell/trace-mapping': 0.3.25 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + dev: true /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -20547,6 +20650,7 @@ packages: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} dependencies: makeerror: 1.0.12 + dev: true /watchpack@2.4.1: resolution: {integrity: sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==} @@ -20816,6 +20920,7 @@ packages: dependencies: imurmurhash: 0.1.4 signal-exit: 3.0.7 + dev: true /write-json-file@5.0.0: resolution: {integrity: sha512-ddSsCLa4aQ3kI21BthINo4q905/wfhvQ3JL3774AcRjBaiQmfn5v4rw77jQ7T6CmAit9VOQO+FsLyPkwxoB1fw==} From fee7a3c72b69f17023ba9ebbb46d10684ff1ad17 Mon Sep 17 00:00:00 2001 From: Tom Wise <79859203+tomwisecodes@users.noreply.github.com> Date: Tue, 20 Aug 2024 14:29:23 +0100 Subject: [PATCH 02/18] chore: linting --- .../AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx index 55256f80e..98aaffe4d 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx @@ -4,7 +4,6 @@ import { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; -import { humanizeCamelCaseString } from "./chat-dropdownsection"; import { notEmpty } from "./chat-lessonPlanDisplay"; import { MemoizedReactMarkdownWithStyles } from "./markdown"; From bb787a7be494eab34457bed06c9a5f1ecb67495d Mon Sep 17 00:00:00 2001 From: Tom Wise <79859203+tomwisecodes@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:13:30 +0100 Subject: [PATCH 03/18] chore: update import --- .../AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx index 98aaffe4d..b0dcf254c 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx @@ -2,6 +2,7 @@ import { useRef } from "react"; import { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; +import { humanizeCamelCaseString } from "@oakai/core/src/utils/humanizeCamelCaseString"; import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; import { notEmpty } from "./chat-lessonPlanDisplay"; From 4f27bf017417d296c771a15c0210946fa3282a56 Mon Sep 17 00:00:00 2001 From: Tom Wise <79859203+tomwisecodes@users.noreply.github.com> Date: Wed, 21 Aug 2024 16:57:21 +0100 Subject: [PATCH 04/18] feat: link up back end and front end for flag --- .../drop-down-form-wrapper.tsx | 3 +- .../Chat/drop-down-section/flag-button.tsx | 32 +++++++++++------ .../Chat/drop-down-section/modify-button.tsx | 4 +-- packages/api/src/router/appSessions.ts | 35 ++++++++++++++++--- packages/db/prisma/schema.prisma | 4 +-- 5 files changed, 59 insertions(+), 19 deletions(-) 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 index 1d2910b08..b8a7944ad 100644 --- 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 @@ -4,6 +4,7 @@ import { OakBox, OakFlex, OakP, + OakPrimaryButton, OakSmallPrimaryButton, } from "@oaknational/oak-components"; @@ -83,7 +84,7 @@ export const DropDownFormWrapper = ({ {title} {children} - {buttonText} + {buttonText} 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 index 4704d3586..e623a52c4 100644 --- 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 @@ -1,5 +1,6 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { AilaUserFlagType } from "@oakai/db"; import { OakBox, OakP, @@ -11,6 +12,7 @@ import { } from "@oaknational/oak-components"; import styled from "styled-components"; +import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import { trpc } from "@/utils/trpc"; import ActionButton from "./action-button"; @@ -32,18 +34,28 @@ const FlagButton = ({}) => { const [selectedRadio, setSelectedRadio] = useState(null); const [displayTextBox, setDisplayTextBox] = useState(null); + const [userFeedbackText, setUserFeedbackText] = useState(""); + const chat = useLessonChat(); + + const { id } = chat; - const { error, data, isLoading } = - trpc.chat.appSessions.flagSection.useQuery(); + const { mutateAsync, isLoading, error } = + trpc.chat.appSessions.flagSection.useMutation(); - async function flagSectionContent(value: string) { - const userFeedback = { - flat: selectedRadio, - feedback: userFeedbackText, - }; - console.log("Flag content", userFeedback); - } + const flagSectionContent = useCallback(async () => { + if (selectedRadio) { + const payload = { + chatId: id, + messageId: "asdf", + flagType: selectedRadio + .toUpperCase() + .replace(" ", "_") as AilaUserFlagType, + userComment: userFeedbackText, + }; + await mutateAsync(payload); + } + }, [selectedRadio, userFeedbackText, mutateAsync, id]); useEffect(() => { !isOpen && setDisplayTextBox(null); diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx index eb8def970..c66bb5c76 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx @@ -27,7 +27,7 @@ const ModifyButton = ({ section }: { section: string }) => { const chat = useLessonChat(); const { append } = chat; - async function appendToChat(value: string) { + async function modifySection(value: string) { await append({ id: "lessonPlan", content: `For the ${section}, ${value}`, @@ -46,7 +46,7 @@ const ModifyButton = ({ section }: { section: string }) => { {isOpen && ( { + // return "hello"; + const { chatId, messageId, flagType, userComment } = input; + const { userId } = ctx.auth; + const response = await ctx.prisma.ailaUserFlag.create({ + data: { + userId, + chatId, + messageId, + flagType, + userComment, + }, + }); + + return response; + }), getSidebarChats: protectedProcedure.query(async ({ ctx }) => { const { userId } = ctx.auth; @@ -344,8 +375,4 @@ export const appSessionsRouter = router({ return chat; }), - flagSection: protectedProcedure.query(async ({ ctx }) => { - console.log("flagging section"); - return "flagged"; - }), }); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 012ac0d15..422f8a26d 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -132,8 +132,8 @@ model AilaUserFlag { } enum AilaUserFlagType { - INAPPROPRIATE_CONTENT - INACCURATE_CONTENT + INAPPROPRIATE + INACCURATE TOO_HARD TOO_EASY OTHER From cf4853c66f10f1bac6c43bceffd9f015749665bb Mon Sep 17 00:00:00 2001 From: Tom Wise <79859203+tomwisecodes@users.noreply.github.com> Date: Thu, 22 Aug 2024 15:39:29 +0100 Subject: [PATCH 05/18] chore: latest --- apps/nextjs/src/app/layout.tsx | 2 + .../Chat/drop-down-section/action-button.tsx | 31 +++++------ .../Chat/drop-down-section/chat-section.tsx | 7 ++- .../drop-down-form-wrapper.tsx | 3 +- .../Chat/drop-down-section/flag-button.tsx | 5 +- .../Chat/drop-down-section/index.tsx | 3 - .../Chat/drop-down-section/modify-button.tsx | 40 ++++++++++++-- .../drop-down-section/small-radio-button.tsx | 8 +++ packages/api/src/router/appSessions.ts | 55 +++++++++++++++---- .../migration.sql | 18 ++++++ packages/db/prisma/schema.prisma | 1 + 11 files changed, 134 insertions(+), 39 deletions(-) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/drop-down-section/small-radio-button.tsx create mode 100644 packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql diff --git a/apps/nextjs/src/app/layout.tsx b/apps/nextjs/src/app/layout.tsx index d1a5d397c..2041bb4f9 100644 --- a/apps/nextjs/src/app/layout.tsx +++ b/apps/nextjs/src/app/layout.tsx @@ -93,6 +93,8 @@ export default function RootLayout({ children }: Readonly) { - { - await waitThenExecute(200).then(() => { + await waitThenExecute(0).then(() => { setShowTooltip(true); }); }} @@ -36,22 +37,20 @@ const ActionButton = ({ setShowTooltip(false); }} > - - - {children} - - - + + {children} + + ); }; +const OakSmallSecondaryWithOpaqueBorder = styled(OakSmallSecondaryButton)` + button { + border-color: rgba(0, 0, 0, 0.3); + } +`; + async function waitThenExecute(ms: number) { return new Promise((resolve) => { setTimeout(resolve, ms); 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 index ac6dd5aa1..e83e17de0 100644 --- 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 @@ -24,7 +24,12 @@ const ChatSection = ({ markdown={`${sectionToMarkdown(objectKey, value)}`} /> - + 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 index b8a7944ad..1d2910b08 100644 --- 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 @@ -4,7 +4,6 @@ import { OakBox, OakFlex, OakP, - OakPrimaryButton, OakSmallPrimaryButton, } from "@oaknational/oak-components"; @@ -84,7 +83,7 @@ export const DropDownFormWrapper = ({ {title} {children} - {buttonText} + {buttonText} 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 index e623a52c4..a23266127 100644 --- 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 @@ -17,6 +17,7 @@ import { trpc } from "@/utils/trpc"; import ActionButton from "./action-button"; import { DropDownFormWrapper } from "./drop-down-form-wrapper"; +import { SmallRadioButton } from "./small-radio-button"; const flagOptions = [ "Inappropriate", @@ -83,7 +84,7 @@ const FlagButton = ({}) => { {flagOptions.map((option) => ( @@ -118,7 +119,7 @@ const FlagButtonFormItem = ({ }) => { return ( <> - { +const ModifyButton = ({ + section, + sectionContent, +}: { + section: string; + sectionContent: string; +}) => { const dropdownRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [selectedRadio, setSelectedRadio] = useState(null); @@ -27,12 +37,32 @@ const ModifyButton = ({ section }: { section: string }) => { const chat = useLessonChat(); const { append } = chat; + const { id } = chat; + + const { mutateAsync, isLoading, error } = + trpc.chat.appSessions.modifySection.useMutation(); + + const recordUserModifySectionContent = useCallback(async () => { + if (selectedRadio) { + const payload = { + chatId: id, + messageId: "asdf", + textForMod: sectionContent, + action: selectedRadio + .toUpperCase() + .replace(" ", "_") as AilaUserModificationAction, + }; + await mutateAsync(payload); + } + }, [selectedRadio, sectionContent, mutateAsync, id]); + async function modifySection(value: string) { await append({ id: "lessonPlan", content: `For the ${section}, ${value}`, role: "user", }); + await recordUserModifySectionContent(); } return ( @@ -41,7 +71,7 @@ const ModifyButton = ({ section }: { section: string }) => { onClick={() => setIsOpen(!isOpen)} tooltip="Aila can help improve this section" > - Modify {section} + Modify {isOpen && ( @@ -57,11 +87,11 @@ const ModifyButton = ({ section }: { section: string }) => { {modifyOptions.map((option) => ( - { + const { userId } = ctx.auth; + try { + const response = await ctx.prisma.ailaUserModification.create({ + data: { + userId, + ...input, + }, + }); + return response; + } catch (error) { + console.log(error); + return error; + } + }), flagSection: protectedProcedure .input( z.object({ @@ -242,17 +272,22 @@ export const appSessionsRouter = router({ // return "hello"; const { chatId, messageId, flagType, userComment } = input; const { userId } = ctx.auth; - const response = await ctx.prisma.ailaUserFlag.create({ - data: { - userId, - chatId, - messageId, - flagType, - userComment, - }, - }); + try { + const response = await ctx.prisma.ailaUserFlag.create({ + data: { + userId, + chatId, + messageId, + flagType, + userComment, + }, + }); - return response; + return response; + } catch (error) { + console.log(error); + return error; + } }), getSidebarChats: protectedProcedure.query(async ({ ctx }) => { const { userId } = ctx.auth; diff --git a/packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql b/packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql new file mode 100644 index 000000000..0aaaa2b12 --- /dev/null +++ b/packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - The values [INAPPROPRIATE_CONTENT,INACCURATE_CONTENT] on the enum `AilaUserFlagType` will be removed. If these variants are still used in the database, this will fail. + - Added the required column `text_for_mod` to the `aila_user_modifications` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "AilaUserFlagType_new" AS ENUM ('INAPPROPRIATE', 'INACCURATE', 'TOO_HARD', 'TOO_EASY', 'OTHER'); +ALTER TABLE "chat_user_flags" ALTER COLUMN "flag_type" TYPE "AilaUserFlagType_new" USING ("flag_type"::text::"AilaUserFlagType_new"); +ALTER TYPE "AilaUserFlagType" RENAME TO "AilaUserFlagType_old"; +ALTER TYPE "AilaUserFlagType_new" RENAME TO "AilaUserFlagType"; +DROP TYPE "AilaUserFlagType_old"; +COMMIT; + +-- AlterTable +ALTER TABLE "aila_user_modifications" ADD COLUMN "text_for_mod" TEXT NOT NULL; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 422f8a26d..a860bea47 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -147,6 +147,7 @@ model AilaUserModification { userId String @map("user_id") chatId String @map("chat_id") messageId String @map("message_id") + textForMod String @map("text_for_mod") action AilaUserModificationAction @map("action") @@map("aila_user_modifications") From 02d87a3fed55b35f867c03b918f920914032e8c0 Mon Sep 17 00:00:00 2001 From: mantagen Date: Tue, 27 Aug 2024 15:00:52 +0200 Subject: [PATCH 06/18] fix: message ids from client hook --- apps/nextjs/src/app/actions.ts | 64 +++---------------- .../AppComponents/Chat/chat-message/index.tsx | 3 +- .../AppComponents/Chat/chat-panel.tsx | 1 - .../AppComponents/Chat/chat-quick-buttons.tsx | 1 - .../Chat/chat-start-accordion.tsx | 4 +- .../Chat/drop-down-section/modify-button.tsx | 4 +- .../Chat/empty-screen-accordian.tsx | 4 +- .../ContextProviders/ChatProvider.tsx | 58 ++++------------- .../src/features/moderation/AilaModeration.ts | 3 +- .../aila/src/helpers/chat/generateChatId.ts | 5 ++ .../src/helpers/chat/generateMessageId.ts | 5 ++ .../aila/src/helpers/chat/getMessageId.ts | 15 ----- packages/api/src/router/appSessions.ts | 45 +------------ 13 files changed, 43 insertions(+), 169 deletions(-) create mode 100644 packages/aila/src/helpers/chat/generateChatId.ts create mode 100644 packages/aila/src/helpers/chat/generateMessageId.ts delete mode 100644 packages/aila/src/helpers/chat/getMessageId.ts diff --git a/apps/nextjs/src/app/actions.ts b/apps/nextjs/src/app/actions.ts index 84f734d52..b22900c06 100644 --- a/apps/nextjs/src/app/actions.ts +++ b/apps/nextjs/src/app/actions.ts @@ -1,35 +1,9 @@ "use server"; import { auth } from "@clerk/nextjs/server"; -import { - AilaPersistedChat, - AilaPersistedChatWithMissingMessageIds, - chatSchema, - chatSchemaWithMissingMessageIds, -} from "@oakai/aila/src/protocol/schema"; +import { AilaPersistedChat, chatSchema } from "@oakai/aila/src/protocol/schema"; import { Prisma, prisma } from "@oakai/db"; import * as Sentry from "@sentry/nextjs"; -import { nanoid } from "nanoid"; - -function assertChatMessageIdsAreUniqueWithinTheScopeOfThisChat( - chat: AilaPersistedChatWithMissingMessageIds, -): boolean { - let updated = false; - const usedIds = new Set(); - chat.messages = chat.messages.map((message) => { - if (!message.id || usedIds.has(message.id)) { - let newId = nanoid(16); - while (usedIds.has(newId)) { - newId = nanoid(16); - } - message.id = newId; - updated = true; - } - usedIds.add(message.id); - return message; - }); - return updated; -} function parseChatAndReportError({ sessionOutput, @@ -39,11 +13,11 @@ function parseChatAndReportError({ sessionOutput: Prisma.JsonValue; id: string; userId: string; -}) { +}): AilaPersistedChat | undefined { if (typeof sessionOutput !== "object") { throw new Error(`sessionOutput is not an object`); } - const parseResult = chatSchemaWithMissingMessageIds.safeParse({ + const parseResult = chatSchema.safeParse({ ...sessionOutput, userId, id, @@ -67,9 +41,6 @@ function parseChatAndReportError({ export async function getChatById( id: string, ): Promise { - let chat: AilaPersistedChatWithMissingMessageIds | undefined = undefined; - let chatWithMessageIds: AilaPersistedChat | undefined = undefined; - const session = await prisma?.appSession.findUnique({ where: { id }, }); @@ -78,28 +49,13 @@ export async function getChatById( return null; } - chat = parseChatAndReportError({ - id, - sessionOutput: session.output, - userId: session.userId, - }); - - if (chat) { - // Check if messages have IDs. If not, assign them and update the chat. - // This is a migration step that should be removed after all chats have unique IDs. - const updated = assertChatMessageIdsAreUniqueWithinTheScopeOfThisChat(chat); - - chatWithMessageIds = chatSchema.parse(chat); - if (updated) { - await prisma?.appSession.update({ - where: { id }, - data: { output: chatWithMessageIds }, - }); - } - return chatWithMessageIds; - } - - return null; + return ( + parseChatAndReportError({ + id, + sessionOutput: session.output, + userId: session.userId, + }) || null + ); } export async function getChatForAuthenticatedUser( diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx index bef434a45..811f4637e 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx @@ -2,7 +2,6 @@ // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx import { ReactNode, useState } from "react"; -import { getMessageId } from "@oakai/aila/src/helpers/chat/getMessageId"; import { ActionDocument, BadDocument, @@ -62,7 +61,7 @@ export function ChatMessage({ | PersistedModerationBase | undefined; - const messageId = getMessageId(message); + const messageId = message.id; const matchingPersistedModeration: PersistedModerationBase | undefined = persistedModerations.find((m) => m.messageId === messageId); diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx index 3e762fda9..2ff03ef77 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx @@ -41,7 +41,6 @@ export function ChatPanel({ }); await append({ - id, content: value, role: "user", }); diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx index b344b347c..4280c9c07 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx @@ -75,7 +75,6 @@ const QuickActionButtons = ({ isEmptyScreen }: QuickActionButtonsProps) => { trackEvent("chat:continue"); lessonPlanTracking.onClickContinue(); await append({ - id, content: "Continue", role: "user", }); diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx index 74a535932..1abf29955 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx @@ -128,7 +128,7 @@ const ChatStartAccordion = () => { - + 1 worksheet @@ -174,7 +174,7 @@ const AccordionTrigger = React.forwardRef< {...props} className="flex w-full items-center justify-between " > - + {children} diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx index 78be7f929..8c855a8e8 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx @@ -39,8 +39,7 @@ const ModifyButton = ({ const { id } = chat; - const { mutateAsync, isLoading, error } = - trpc.chat.appSessions.modifySection.useMutation(); + const { mutateAsync } = trpc.chat.appSessions.modifySection.useMutation(); const recordUserModifySectionContent = useCallback(async () => { if (selectedRadio) { @@ -58,7 +57,6 @@ const ModifyButton = ({ async function modifySection(value: string) { await append({ - id: "lessonPlan", content: `For the ${section}, ${value}`, role: "user", }); diff --git a/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx b/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx index b5f767af9..2058e9546 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx @@ -118,7 +118,7 @@ const EmptyScreenAccordion = () => { - + 1 worksheet @@ -164,7 +164,7 @@ const AccordionTrigger = React.forwardRef< {...props} className="flex w-full items-center justify-between " > - + {children} diff --git a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx index 29c62cb67..840d8a1bb 100644 --- a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx +++ b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx @@ -10,13 +10,14 @@ import React, { import { toast } from "react-hot-toast"; import { usePathname, useRouter } from "#next/navigation"; +import { generateMessageId } from "@oakai/aila/src/helpers/chat/generateMessageId"; import { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import { isToxic } from "@oakai/core/src/utils/ailaModeration/helpers"; import { PersistedModerationBase } from "@oakai/core/src/utils/ailaModeration/moderationSchema"; import { Moderation } from "@oakai/db"; import * as Sentry from "@sentry/nextjs"; import { Message } from "ai"; -import { ChatRequestOptions, CreateMessage, nanoid } from "ai"; +import { ChatRequestOptions, CreateMessage } from "ai"; import { useChat } from "ai/react"; import { deepClone } from "fast-json-patch"; import { useTemporaryLessonPlanWithStreamingEdits } from "hooks/useTemporaryLessonPlanWithStreamingEdits"; @@ -125,8 +126,7 @@ export function ChatProvider({ const chatAreaRef = useRef(null); const [hasFinished, setHasFinished] = useState(true); - const [hasAppendedInitialMessage, setHasAppendedInitialMessage] = - useState(false); + const hasAppendedInitialMessage = useRef(false); /******************* Functions *******************/ @@ -153,7 +153,9 @@ export function ChatProvider({ setInput, setMessages, } = useChat({ + sendExtraMessageFields: true, initialMessages, + generateId: generateMessageId, id, body: { id, @@ -218,32 +220,6 @@ export function ChatProvider({ }, }); - const messageIdsRef = useRef>(new Map()); - - const messagesWithIds = useMemo(() => { - return messages.map((msg, index) => { - if (!messageIdsRef.current.has(index)) { - messageIdsRef.current.set(index, nanoid(16)); - } - return { ...msg, id: messageIdsRef.current.get(index)! }; - }); - }, [messages]); - - const appendWithId = useCallback( - (message, chatRequestOptions) => { - const newId = nanoid(16); - messageIdsRef.current.set(messages.length, newId); - return append( - { - ...message, - id: newId, - }, - chatRequestOptions, - ); - }, - [append, messages.length], - ); - const { tempLessonPlan } = useTemporaryLessonPlanWithStreamingEdits({ lessonPlan, messages, @@ -266,22 +242,16 @@ export function ChatProvider({ }, [initialLessonPlan, setLessonPlanWithLogging]); useEffect(() => { - if (startingMessage && !hasAppendedInitialMessage) { + if (startingMessage && !hasAppendedInitialMessage.current) { + console.log("Appending starting message", startingMessage); + append({ content: startingMessage, role: "user", - id: nanoid(16), }); - setHasAppendedInitialMessage(true); + hasAppendedInitialMessage.current = true; } - }, [ - startingMessage, - append, - router, - path, - hasAppendedInitialMessage, - setHasAppendedInitialMessage, - ]); + }, [startingMessage, append, router, path, hasAppendedInitialMessage]); // Clear the hash cache each completed message useEffect(() => { @@ -337,8 +307,8 @@ export function ChatProvider({ hasFinished, hasAppendedInitialMessage, chatAreaRef, - append: appendWithId, - messages: messagesWithIds, + append, + messages, ailaStreamingStatus, isLoading, isStreaming: !hasFinished, @@ -358,8 +328,7 @@ export function ChatProvider({ hasFinished, hasAppendedInitialMessage, chatAreaRef, - appendWithId, - messagesWithIds, + messages, ailaStreamingStatus, isLoading, lastModeration, @@ -367,6 +336,7 @@ export function ChatProvider({ stop, input, setInput, + append, ], ); diff --git a/packages/aila/src/features/moderation/AilaModeration.ts b/packages/aila/src/features/moderation/AilaModeration.ts index d03794851..65a74083c 100644 --- a/packages/aila/src/features/moderation/AilaModeration.ts +++ b/packages/aila/src/features/moderation/AilaModeration.ts @@ -16,7 +16,6 @@ import invariant from "tiny-invariant"; import { AilaServices } from "../../core"; import { Message } from "../../core/chat"; import { AilaPluginContext } from "../../core/plugins/types"; -import { getMessageId } from "../../helpers/chat/getMessageId"; import { ModerationDocument } from "../../protocol/jsonPatchProtocol"; import { LooseLessonPlan } from "../../protocol/schema"; import { AilaModerationFeature } from "../types"; @@ -69,7 +68,7 @@ export class AilaModeration implements AilaModerationFeature { const moderation = await this._moderations.create({ userId, appSessionId: chatId, - messageId: getMessageId(lastUserMessage), + messageId: lastUserMessage.id, categories: moderationResult.categories, justification: moderationResult.justification, lesson: lessonPlan, diff --git a/packages/aila/src/helpers/chat/generateChatId.ts b/packages/aila/src/helpers/chat/generateChatId.ts new file mode 100644 index 000000000..e2e6bd1a1 --- /dev/null +++ b/packages/aila/src/helpers/chat/generateChatId.ts @@ -0,0 +1,5 @@ +import { nanoid } from "nanoid"; + +export function generateChatId() { + return `chat-${nanoid(16)}`; +} diff --git a/packages/aila/src/helpers/chat/generateMessageId.ts b/packages/aila/src/helpers/chat/generateMessageId.ts new file mode 100644 index 000000000..0e1eaf9db --- /dev/null +++ b/packages/aila/src/helpers/chat/generateMessageId.ts @@ -0,0 +1,5 @@ +import { nanoid } from "nanoid"; + +export function generateMessageId() { + return `message-${nanoid(16)}`; +} diff --git a/packages/aila/src/helpers/chat/getMessageId.ts b/packages/aila/src/helpers/chat/getMessageId.ts deleted file mode 100644 index 04cce373e..000000000 --- a/packages/aila/src/helpers/chat/getMessageId.ts +++ /dev/null @@ -1,15 +0,0 @@ -import crypto from "crypto"; - -/** - * Returns or generates a unique message ID based on the message content. - */ -export function getMessageId(message: { content: string; id?: string }) { - if (message.id) { - return message.id; - } - return crypto - .createHash("sha256") - .update(message.content) - .digest("hex") - .slice(0, 16); -} diff --git a/packages/api/src/router/appSessions.ts b/packages/api/src/router/appSessions.ts index 207442b6d..bac04479e 100644 --- a/packages/api/src/router/appSessions.ts +++ b/packages/api/src/router/appSessions.ts @@ -13,9 +13,7 @@ import { z } from "zod"; import { getSessionModerations } from "../../../aila/src/features/moderation/getSessionModerations"; import { AilaPersistedChat, - AilaPersistedChatWithMissingMessageIds, chatSchema, - chatSchemaWithMissingMessageIds, } from "../../../aila/src/protocol/schema"; import { protectedProcedure } from "../middleware/auth"; import { router } from "../trpc"; @@ -36,7 +34,7 @@ function parseChatAndReportError({ if (typeof sessionOutput !== "object") { throw new Error(`sessionOutput is not an object`); } - const parseResult = chatSchemaWithMissingMessageIds.safeParse({ + const parseResult = chatSchema.safeParse({ ...sessionOutput, userId, id, @@ -79,27 +77,6 @@ async function checkMutationPermissions(userId: string) { } } -function assertChatMessageIdsAreUniqueWithinTheScopeOfThisChat( - chat: AilaPersistedChatWithMissingMessageIds, -) { - let updated = false; - const usedIds = new Set(); - chat.messages = chat.messages.map((message) => { - if (!message.id || usedIds.has(message.id)) { - let newId = nanoid(16); - while (usedIds.has(newId)) { - newId = nanoid(16); - } - message.id = newId; - updated = true; - } - usedIds.add(message.id); - return message; - }); - const updatedChat = chatSchema.parse(chat); - return { updated, updatedChat }; -} - export async function getChat(id: string, prisma: PrismaClientWithAccelerate) { const chatRecord = await prisma.appSession.findUnique({ where: { @@ -110,29 +87,11 @@ export async function getChat(id: string, prisma: PrismaClientWithAccelerate) { return undefined; } - const validatedChat = parseChatAndReportError({ + return parseChatAndReportError({ id, userId: chatRecord.userId, sessionOutput: chatRecord.output, }); - - if (!validatedChat) { - return undefined; - } - - // Check if messages have IDs. If not, assign them and update the chat. - // This is a migration step that should be removed after all chats have unique IDs. - const { updated, updatedChat } = - assertChatMessageIdsAreUniqueWithinTheScopeOfThisChat(validatedChat); - - if (updated) { - await prisma?.appSession.update({ - where: { id }, - data: { output: updatedChat }, - }); - } - - return updatedChat; } export const appSessionsRouter = router({ From dc422c0cdeeef182b5584f219a7c7f5f7155d8a3 Mon Sep 17 00:00:00 2001 From: mantagen Date: Tue, 27 Aug 2024 15:05:16 +0200 Subject: [PATCH 07/18] chore: gen id fns --- packages/aila/src/helpers/chat/generateChatId.ts | 2 +- packages/aila/src/helpers/chat/generateMessageId.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/aila/src/helpers/chat/generateChatId.ts b/packages/aila/src/helpers/chat/generateChatId.ts index e2e6bd1a1..7b4d9c1f5 100644 --- a/packages/aila/src/helpers/chat/generateChatId.ts +++ b/packages/aila/src/helpers/chat/generateChatId.ts @@ -1,5 +1,5 @@ import { nanoid } from "nanoid"; export function generateChatId() { - return `chat-${nanoid(16)}`; + return `${nanoid(16)}`; } diff --git a/packages/aila/src/helpers/chat/generateMessageId.ts b/packages/aila/src/helpers/chat/generateMessageId.ts index 0e1eaf9db..6f7302850 100644 --- a/packages/aila/src/helpers/chat/generateMessageId.ts +++ b/packages/aila/src/helpers/chat/generateMessageId.ts @@ -1,5 +1,5 @@ import { nanoid } from "nanoid"; export function generateMessageId() { - return `message-${nanoid(16)}`; + return `${nanoid(16)}`; } From 8b78d975e6bef924b20dbe290033c85eccb13247 Mon Sep 17 00:00:00 2001 From: mantagen Date: Wed, 28 Aug 2024 17:46:46 +0200 Subject: [PATCH 08/18] add other id generate functions -- with role prefix --- .../src/components/ContextProviders/ChatProvider.tsx | 2 +- packages/aila/src/core/chat/AilaChat.ts | 8 ++++---- packages/aila/src/helpers/chat/generateMessageId.ts | 8 ++++++-- packages/api/src/router/appSessions.ts | 10 +++++----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx index 840d8a1bb..2b68a9eba 100644 --- a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx +++ b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx @@ -155,7 +155,7 @@ export function ChatProvider({ } = useChat({ sendExtraMessageFields: true, initialMessages, - generateId: generateMessageId, + generateId: () => generateMessageId({ role: "user" }), id, body: { id, diff --git a/packages/aila/src/core/chat/AilaChat.ts b/packages/aila/src/core/chat/AilaChat.ts index f0e671720..e320040cb 100644 --- a/packages/aila/src/core/chat/AilaChat.ts +++ b/packages/aila/src/core/chat/AilaChat.ts @@ -3,7 +3,6 @@ import { unsupportedSubjects, subjectWarnings, } from "@oakai/core/src/utils/subjects"; -import { nanoid } from "nanoid"; import invariant from "tiny-invariant"; import { AilaChatService, AilaServices } from "../.."; @@ -12,6 +11,7 @@ import { AilaGeneration, AilaGenerationStatus, } from "../../features/generation"; +import { generateMessageId } from "../../helpers/chat/generateMessageId"; import { JsonPatchDocumentOptional } from "../../protocol/jsonPatchProtocol"; import { LLMService } from "../llm/LLMService"; import { OpenAIService } from "../llm/OpenAIService"; @@ -123,7 +123,7 @@ export class AilaChat implements AilaChatService { public async systemMessage() { invariant(this._generation?.systemPrompt, "System prompt not initialised"); return { - id: nanoid(16), + id: generateMessageId({ role: "system" }), content: this._generation?.systemPrompt, role: "system" as const, }; @@ -139,7 +139,7 @@ export class AilaChat implements AilaChatService { if (this._aila?.lesson.hasSetInitialState) { applicableMessages.push({ - id: nanoid(16), + id: generateMessageId({ role: "user" }), role: "user", content: "Now that you have the title, key stage and subject, let's start planning the lesson. Could you tell me if there are Oak lessons I could base my lesson on, or if there are none available let's get going with the first step of the lesson plan creation process!", @@ -256,7 +256,7 @@ export class AilaChat implements AilaChatService { return; } const assistantMessage: Message = { - id: nanoid(16), + id: generateMessageId({ role: "assistant" }), role: "assistant", content, }; diff --git a/packages/aila/src/helpers/chat/generateMessageId.ts b/packages/aila/src/helpers/chat/generateMessageId.ts index 6f7302850..79340d5b4 100644 --- a/packages/aila/src/helpers/chat/generateMessageId.ts +++ b/packages/aila/src/helpers/chat/generateMessageId.ts @@ -1,5 +1,9 @@ import { nanoid } from "nanoid"; -export function generateMessageId() { - return `${nanoid(16)}`; +export function generateMessageId({ + role, +}: { + role: "user" | "assistant" | "system"; +}) { + return `${role[0]}-${nanoid(16)}`; } diff --git a/packages/api/src/router/appSessions.ts b/packages/api/src/router/appSessions.ts index bac04479e..1c48d67ed 100644 --- a/packages/api/src/router/appSessions.ts +++ b/packages/api/src/router/appSessions.ts @@ -6,11 +6,11 @@ import { RateLimitExceededError } from "@oakai/core/src/utils/rateLimiting/userB import { Prisma, PrismaClientWithAccelerate } from "@oakai/db"; import * as Sentry from "@sentry/nextjs"; import { TRPCError } from "@trpc/server"; -import { nanoid } from "nanoid"; import { isTruthy } from "remeda"; import { z } from "zod"; import { getSessionModerations } from "../../../aila/src/features/moderation/getSessionModerations"; +import { generateChatId } from "../../../aila/src/helpers/chat/generateChatId"; import { AilaPersistedChat, chatSchema, @@ -135,11 +135,11 @@ export const appSessionsRouter = router({ await checkMutationPermissions(userId); - const id = nanoid(16); + const chatId = generateChatId(); const output: AilaPersistedChat = { - id, - path: `/aila/${id}`, + id: chatId, + path: `/aila/${chatId}`, title: "", topic: "", userId, @@ -154,7 +154,7 @@ export const appSessionsRouter = router({ const appSession = { data: { - id: nanoid(16), + id: chatId, appId, userId, output, From 26671a8818125e307182bbee4637951811b65d0c Mon Sep 17 00:00:00 2001 From: mantagen Date: Wed, 28 Aug 2024 18:10:27 +0200 Subject: [PATCH 09/18] fix lint and sonar issues --- .../Chat/drop-down-section/action-button.tsx | 1 - .../Chat/drop-down-section/flag-button.tsx | 15 +++------------ .../Chat/drop-down-section/index.tsx | 12 +----------- .../Chat/drop-down-section/modify-button.tsx | 9 +-------- 4 files changed, 5 insertions(+), 32 deletions(-) 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 index 255c8d6f7..686a6a594 100644 --- 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 @@ -3,7 +3,6 @@ import { useState } from "react"; import { OakBox, OakSmallSecondaryButton, - OakSpan, OakTooltip, } from "@oaknational/oak-components"; import styled from "styled-components"; 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 index a23266127..85943a3b4 100644 --- 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 @@ -1,15 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { AilaUserFlagType } from "@oakai/db"; -import { - OakBox, - OakP, - OakRadioButton, - OakRadioGroup, - OakSpan, - OakTertiaryButton, - OakTextInput, -} from "@oaknational/oak-components"; +import { OakBox, OakP, OakRadioGroup } from "@oaknational/oak-components"; import styled from "styled-components"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; @@ -29,7 +21,7 @@ const flagOptions = [ type FlagButtonOptions = (typeof flagOptions)[number]; -const FlagButton = ({}) => { +const FlagButton = () => { const dropdownRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [selectedRadio, setSelectedRadio] = useState(null); @@ -41,8 +33,7 @@ const FlagButton = ({}) => { const { id } = chat; - const { mutateAsync, isLoading, error } = - trpc.chat.appSessions.flagSection.useMutation(); + const { mutateAsync } = trpc.chat.appSessions.flagSection.useMutation(); const flagSectionContent = useCallback(async () => { if (selectedRadio) { diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx index 5a1cdd6b1..3a78d3499 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.tsx @@ -1,8 +1,6 @@ import { useEffect, useRef, useState } from "react"; -import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; import { OakBox, OakFlex, OakP } from "@oaknational/oak-components"; -import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; import styled from "styled-components"; import { Icon } from "@/components/Icon"; @@ -121,15 +119,7 @@ const valuesAreEqual = ( val1: Record, val2: Record, ): boolean => { - if (typeof val1 !== typeof val2) return false; - if (typeof val1 === "object" && val1 !== null && val2 !== null) { - if (Array.isArray(val1) && Array.isArray(val2)) { - return JSON.stringify(val1) === JSON.stringify(val2); - } else { - return JSON.stringify(val1) === JSON.stringify(val2); - } - } - return val1 === val2; + return JSON.stringify(val1) === JSON.stringify(val2); }; export function humanizeCamelCaseString(str: string) { diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx index 8c855a8e8..1b89d12cb 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx @@ -1,14 +1,7 @@ import { useCallback, useRef, useState } from "react"; -import { - OakBox, - OakRadioButton, - OakRadioGroup, - OakSpan, - OakTertiaryButton, -} from "@oaknational/oak-components"; +import { OakBox, OakRadioGroup } from "@oaknational/oak-components"; import { AilaUserModificationAction } from "@prisma/client"; -import styled from "styled-components"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import { trpc } from "@/utils/trpc"; From d71aeb826247e20d0e29a8f83c6f02f336434e79 Mon Sep 17 00:00:00 2001 From: mantagen Date: Wed, 28 Aug 2024 18:13:29 +0200 Subject: [PATCH 10/18] remove log --- apps/nextjs/src/components/ContextProviders/ChatProvider.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx index 2b68a9eba..0d26aa860 100644 --- a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx +++ b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx @@ -243,8 +243,6 @@ export function ChatProvider({ useEffect(() => { if (startingMessage && !hasAppendedInitialMessage.current) { - console.log("Appending starting message", startingMessage); - append({ content: startingMessage, role: "user", From 1745f07572f62591e4b228c5f378a3b925ac9697 Mon Sep 17 00:00:00 2001 From: mantagen Date: Thu, 29 Aug 2024 13:02:29 +0200 Subject: [PATCH 11/18] error reporting --- packages/api/src/router/appSessions.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/api/src/router/appSessions.ts b/packages/api/src/router/appSessions.ts index 1c48d67ed..35ba7c4e7 100644 --- a/packages/api/src/router/appSessions.ts +++ b/packages/api/src/router/appSessions.ts @@ -207,9 +207,13 @@ export const appSessionsRouter = router({ }, }); return response; - } catch (error) { - console.log(error); - return error; + } catch (cause) { + const err = new Error("Failed to create user modification"); + err.cause = cause; + Sentry.captureException(err, { + extra: input, + }); + return err; } }), flagSection: protectedProcedure @@ -228,7 +232,6 @@ export const appSessionsRouter = router({ }), ) .mutation(async ({ ctx, input }) => { - // return "hello"; const { chatId, messageId, flagType, userComment } = input; const { userId } = ctx.auth; try { @@ -243,9 +246,13 @@ export const appSessionsRouter = router({ }); return response; - } catch (error) { - console.log(error); - return error; + } catch (cause) { + const err = new Error("Failed to flag section"); + err.cause = cause; + Sentry.captureException(err, { + extra: { chatId, messageId, flagType }, + }); + return err; } }), getSidebarChats: protectedProcedure.query(async ({ ctx }) => { From 3651bb15a42851abdffb400b924fc95c79d588a6 Mon Sep 17 00:00:00 2001 From: mantagen Date: Thu, 29 Aug 2024 18:18:39 +0200 Subject: [PATCH 12/18] remove avo options --- apps/nextjs/src/app/layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/nextjs/src/app/layout.tsx b/apps/nextjs/src/app/layout.tsx index 2041bb4f9..d1a5d397c 100644 --- a/apps/nextjs/src/app/layout.tsx +++ b/apps/nextjs/src/app/layout.tsx @@ -93,8 +93,6 @@ export default function RootLayout({ children }: Readonly) { Date: Fri, 30 Aug 2024 15:50:48 +0200 Subject: [PATCH 13/18] tighter type on feedback components --- .../Chat/drop-down-section/chat-section.tsx | 7 +- .../drop-down-form-wrapper.tsx | 14 +++- .../Chat/drop-down-section/flag-button.tsx | 64 +++++++++--------- .../Chat/drop-down-section/modify-button.tsx | 67 +++++++++++++------ 4 files changed, 92 insertions(+), 60 deletions(-) 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 index e83e17de0..923f20efc 100644 --- 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 @@ -1,8 +1,8 @@ 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 { sectionTitle } from "."; import { MemoizedReactMarkdownWithStyles } from "../markdown"; import FlagButton from "./flag-button"; import ModifyButton from "./modify-button"; @@ -12,8 +12,7 @@ const ChatSection = ({ value, }: { objectKey: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any; + value: unknown; }) => { return ( @@ -25,7 +24,7 @@ const ChatSection = ({ /> = { + label: string; + enumValue: T; +}; + +export const DropDownFormWrapper = < + T extends AilaUserModificationAction | AilaUserFlagType, +>({ children, onClickActions, setIsOpen, @@ -18,9 +26,9 @@ export const DropDownFormWrapper = ({ dropdownRef, }: { children: React.ReactNode; - onClickActions: (option: string) => void; + onClickActions: (option: FeedbackOption) => void; setIsOpen: (value: boolean) => void; - selectedRadio: string | null; + selectedRadio: FeedbackOption | null; title: string; buttonText: string; isOpen: boolean; 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 index 85943a3b4..3c3ff757b 100644 --- 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 @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { AilaUserFlagType } from "@oakai/db"; +import type { AilaUserFlagType } from "@oakai/db"; import { OakBox, OakP, OakRadioGroup } from "@oaknational/oak-components"; import styled from "styled-components"; @@ -8,46 +8,48 @@ import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import { trpc } from "@/utils/trpc"; import ActionButton from "./action-button"; -import { DropDownFormWrapper } from "./drop-down-form-wrapper"; +import { DropDownFormWrapper, FeedbackOption } from "./drop-down-form-wrapper"; import { SmallRadioButton } from "./small-radio-button"; -const flagOptions = [ - "Inappropriate", - "Inaccurate", - "Too hard", - "Too easy", - "Other", -]; +const flagOptions: { + label: string; + enumValue: AilaUserFlagType; +}[] = [ + { label: "Inappropriate", enumValue: "INAPPROPRIATE" }, + { label: "Inaccurate", enumValue: "INACCURATE" }, + { label: "Too hard", enumValue: "TOO_HARD" }, + { label: "Too easy", enumValue: "TOO_EASY" }, + { label: "Other", enumValue: "OTHER" }, +] as const; -type FlagButtonOptions = (typeof flagOptions)[number]; +type FlagButtonOptions = typeof flagOptions; const FlagButton = () => { const dropdownRef = useRef(null); const [isOpen, setIsOpen] = useState(false); - const [selectedRadio, setSelectedRadio] = useState(null); - const [displayTextBox, setDisplayTextBox] = - useState(null); + const [selectedRadio, setSelectedRadio] = + useState | null>(null); + const [displayTextBox, setDisplayTextBox] = useState(null); const [userFeedbackText, setUserFeedbackText] = useState(""); const chat = useLessonChat(); - const { id } = chat; + const { id, messages } = chat; + const lastAssistantMessage = messages[messages.length - 1]; const { mutateAsync } = trpc.chat.appSessions.flagSection.useMutation(); const flagSectionContent = useCallback(async () => { - if (selectedRadio) { + if (selectedRadio && lastAssistantMessage) { const payload = { chatId: id, - messageId: "asdf", - flagType: selectedRadio - .toUpperCase() - .replace(" ", "_") as AilaUserFlagType, + messageId: lastAssistantMessage.id, + flagType: selectedRadio.enumValue, userComment: userFeedbackText, }; await mutateAsync(payload); } - }, [selectedRadio, userFeedbackText, mutateAsync, id]); + }, [selectedRadio, userFeedbackText, mutateAsync, id, lastAssistantMessage]); useEffect(() => { !isOpen && setDisplayTextBox(null); @@ -80,7 +82,7 @@ const FlagButton = () => { > {flagOptions.map((option) => ( void; - setDisplayTextBox: (value: FlagButtonOptions | null) => void; - displayTextBox: FlagButtonOptions | null; + option: FlagButtonOptions[number]; + setSelectedRadio: (value: FeedbackOption) => void; + setDisplayTextBox: (value: string | null) => void; + displayTextBox: string | null; setUserFeedbackText: (value: string) => void; }) => { return ( <> { - setDisplayTextBox(option); + setDisplayTextBox(option.label); setSelectedRadio(option); }} /> - {displayTextBox === option && ( + {displayTextBox === option.label && ( <> Provide details below: setUserFeedbackText(e.target.value)} /> diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx index 1b89d12cb..83a07d136 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/modify-button.tsx @@ -1,15 +1,38 @@ import { useCallback, useRef, useState } from "react"; +import type { AilaUserModificationAction } from "@oakai/db"; import { OakBox, OakRadioGroup } from "@oaknational/oak-components"; -import { AilaUserModificationAction } from "@prisma/client"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import { trpc } from "@/utils/trpc"; import ActionButton from "./action-button"; -import { DropDownFormWrapper } from "./drop-down-form-wrapper"; +import { DropDownFormWrapper, FeedbackOption } from "./drop-down-form-wrapper"; import { SmallRadioButton } from "./small-radio-button"; +const modifyOptions: { + label: string; + enumValue: AilaUserModificationAction; +}[] = [ + { + label: "Make it easier", + enumValue: "MAKE_IT_EASIER", + }, + { + label: "Make it harder", + enumValue: "MAKE_IT_HARDER", + }, + { + label: "Shorten content", + enumValue: "SHORTEN_CONTENT", + }, + { + label: "Add more detail", + enumValue: "ADD_MORE_DETAIL", + }, + { label: "Other", enumValue: "OTHER" }, +] as const; + const ModifyButton = ({ section, sectionContent, @@ -19,14 +42,9 @@ const ModifyButton = ({ }) => { const dropdownRef = useRef(null); const [isOpen, setIsOpen] = useState(false); - const [selectedRadio, setSelectedRadio] = useState(null); - const modifyOptions = [ - "Make it easier", - "Make it harder", - "Shorten content", - "Add more detail", - "Other", - ]; + const [selectedRadio, setSelectedRadio] = + useState | null>(null); + const chat = useLessonChat(); const { append } = chat; @@ -34,23 +52,28 @@ const ModifyButton = ({ const { mutateAsync } = trpc.chat.appSessions.modifySection.useMutation(); + const lastAssistantMessage = chat.messages[chat.messages.length - 1]; + const recordUserModifySectionContent = useCallback(async () => { - if (selectedRadio) { + console.log("sectionContent", sectionContent); + console.log("selectedRadio", selectedRadio); + console.log("lastAssistentMessage", lastAssistantMessage); + if (selectedRadio && lastAssistantMessage) { const payload = { chatId: id, - messageId: "asdf", + messageId: lastAssistantMessage.id, textForMod: sectionContent, - action: selectedRadio - .toUpperCase() - .replace(" ", "_") as AilaUserModificationAction, + action: selectedRadio.enumValue, }; await mutateAsync(payload); } - }, [selectedRadio, sectionContent, mutateAsync, id]); + }, [selectedRadio, sectionContent, mutateAsync, id, lastAssistantMessage]); - async function modifySection(value: string) { + async function modifySection( + option: FeedbackOption, + ) { await append({ - content: `For the ${section}, ${value}`, + content: `For the ${section}, ${option.label}`, role: "user", }); await recordUserModifySectionContent(); @@ -83,10 +106,10 @@ const ModifyButton = ({ > {modifyOptions.map((option) => ( { setSelectedRadio(option); }} From 29404a54c49dabe6e53960a115e4c9abe7559215 Mon Sep 17 00:00:00 2001 From: mantagen Date: Mon, 2 Sep 2024 14:16:00 +0100 Subject: [PATCH 14/18] fix server side message ids --- .../AppComponents/Chat/Chat/utils/index.ts | 14 +++++ .../AppComponents/Chat/chat-message/index.tsx | 5 ++ .../Chat/chat-message/protocol.ts | 2 + .../drop-down-form-wrapper.tsx | 7 ++- .../Chat/drop-down-section/flag-button.tsx | 16 +++--- .../Chat/drop-down-section/modify-button.tsx | 55 +++++++------------ .../AppComponents/Chat/prompt-form.tsx | 2 +- .../ContextProviders/ChatProvider.tsx | 34 +++++++++++- packages/aila/src/core/Aila.ts | 7 ++- packages/aila/src/core/chat/AilaChat.ts | 14 ++++- .../aila/src/core/prompt/AilaPromptBuilder.ts | 2 +- .../features/persistence/AilaPersistence.ts | 27 ++++++++- .../persistence/adaptors/prisma/index.ts | 23 +++++--- .../aila/src/protocol/jsonPatchProtocol.ts | 26 ++++++++- packages/db/prisma/schema.prisma | 35 ++++++------ 15 files changed, 188 insertions(+), 81 deletions(-) diff --git a/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts b/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts index 8cbf375e4..4821fa645 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts @@ -1,6 +1,20 @@ import { type LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import { type Message } from "ai/react"; +export function findMessageIdFromContent({ content }: { content: string }) { + return content + .split(`␞`) + .map((s) => { + try { + return JSON.parse(s.trim()); + } catch (e) { + // ignore invalid JSON + return null; + } + }) + .find((i) => i?.type === "id")?.value; +} + export function findLatestServerSideState(workingMessages: Message[]) { console.log("Finding latest server-side state", { workingMessages }); const lastMessage = workingMessages[workingMessages.length - 1]; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx index 811f4637e..272f10344 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx @@ -216,6 +216,7 @@ function ChatMessagePart({ text: TextMessagePart, action: ActionMessagePart, moderation: ModerationMessagePart, + id: IdMessagePart, }[part.type]; if (!PartComponent) { @@ -277,6 +278,10 @@ function StateMessagePart({ part }: Readonly<{ part: StateDocument }>) { return null; } +function IdMessagePart() { + return null; +} + function ActionMessagePart({ // eslint-disable-next-line @typescript-eslint/no-unused-vars part, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/protocol.ts b/apps/nextjs/src/components/AppComponents/Chat/chat-message/protocol.ts index b8d366be5..9a58968cf 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/protocol.ts +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/protocol.ts @@ -7,6 +7,7 @@ import { CommentDocumentSchema, ErrorDocument, ErrorDocumentSchema, + MessageIdDocumentSchema, ModerationDocument, ModerationDocumentSchema, PatchDocument, @@ -54,6 +55,7 @@ export function parseMessageParts(content: string): MessagePart[] { action: ActionDocumentSchema, text: TextDocumentSchema, bad: BadDocumentSchema, + id: MessageIdDocumentSchema, }; const parts = extractMessageParts(content) 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 index 28a32cc71..d4d54d889 100644 --- 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 @@ -8,6 +8,8 @@ import { OakSmallPrimaryButton, } from "@oaknational/oak-components"; +import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; + export type FeedbackOption = { label: string; enumValue: T; @@ -34,6 +36,7 @@ export const DropDownFormWrapper = < isOpen: boolean; dropdownRef: React.RefObject; }) => { + const { isStreaming } = useLessonChat(); const firstButtonRef = useRef(null); useEffect(() => { if (isOpen && firstButtonRef.current) { @@ -91,7 +94,9 @@ export const DropDownFormWrapper = < {title} {children} - {buttonText} + + {buttonText} + 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 index 3c3ff757b..1341245e5 100644 --- 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 @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { AilaUserFlagType } from "@oakai/db"; import { OakBox, OakP, OakRadioGroup } from "@oaknational/oak-components"; @@ -11,10 +11,7 @@ import ActionButton from "./action-button"; import { DropDownFormWrapper, FeedbackOption } from "./drop-down-form-wrapper"; import { SmallRadioButton } from "./small-radio-button"; -const flagOptions: { - label: string; - enumValue: AilaUserFlagType; -}[] = [ +const flagOptions = [ { label: "Inappropriate", enumValue: "INAPPROPRIATE" }, { label: "Inaccurate", enumValue: "INACCURATE" }, { label: "Too hard", enumValue: "TOO_HARD" }, @@ -35,11 +32,12 @@ const FlagButton = () => { const chat = useLessonChat(); const { id, messages } = chat; - const lastAssistantMessage = messages[messages.length - 1]; + const assistantMessages = messages.filter((m) => m.role === "assistant"); + const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]; const { mutateAsync } = trpc.chat.appSessions.flagSection.useMutation(); - const flagSectionContent = useCallback(async () => { + const flagSectionContent = async () => { if (selectedRadio && lastAssistantMessage) { const payload = { chatId: id, @@ -49,7 +47,7 @@ const FlagButton = () => { }; await mutateAsync(payload); } - }, [selectedRadio, userFeedbackText, mutateAsync, id, lastAssistantMessage]); + }; useEffect(() => { !isOpen && setDisplayTextBox(null); @@ -75,7 +73,7 @@ const FlagButton = () => { dropdownRef={dropdownRef} > m.role === "assistant"); + const lastAssistantMessage = assistantMessages[assistantMessages.length - 1]; - const recordUserModifySectionContent = useCallback(async () => { - console.log("sectionContent", sectionContent); - console.log("selectedRadio", selectedRadio); - console.log("lastAssistentMessage", lastAssistantMessage); + const recordUserModifySectionContent = async () => { if (selectedRadio && lastAssistantMessage) { const payload = { chatId: id, @@ -67,16 +50,18 @@ const ModifyButton = ({ }; await mutateAsync(payload); } - }, [selectedRadio, sectionContent, mutateAsync, id, lastAssistantMessage]); + }; async function modifySection( option: FeedbackOption, ) { - await append({ - content: `For the ${section}, ${option.label}`, - role: "user", - }); - await recordUserModifySectionContent(); + await Promise.all([ + append({ + content: `For the ${section}, ${option.label}`, + role: "user", + }), + recordUserModifySectionContent(), + ]); } return ( @@ -93,13 +78,13 @@ const ModifyButton = ({ onClickActions={modifySection} setIsOpen={setIsOpen} selectedRadio={selectedRadio} - title={"Ask Aila to modify learning outcome:"} + title={`Ask Aila to modify ${section}:`} buttonText={"Modify section"} isOpen={isOpen} dropdownRef={dropdownRef} > diff --git a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx index 0d26aa860..f8971b022 100644 --- a/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx +++ b/apps/nextjs/src/components/ContextProviders/ChatProvider.tsx @@ -16,7 +16,7 @@ import { isToxic } from "@oakai/core/src/utils/ailaModeration/helpers"; import { PersistedModerationBase } from "@oakai/core/src/utils/ailaModeration/moderationSchema"; import { Moderation } from "@oakai/db"; import * as Sentry from "@sentry/nextjs"; -import { Message } from "ai"; +import { Message, nanoid } from "ai"; import { ChatRequestOptions, CreateMessage } from "ai"; import { useChat } from "ai/react"; import { deepClone } from "fast-json-patch"; @@ -29,7 +29,10 @@ import { AilaStreamingStatus, useAilaStreamingStatus, } from "../AppComponents/Chat/Chat/hooks/useAilaStreamingStatus"; -import { findLatestServerSideState } from "../AppComponents/Chat/Chat/utils"; +import { + findLatestServerSideState, + findMessageIdFromContent, +} from "../AppComponents/Chat/Chat/utils"; import { isAccountLocked, isModeration, @@ -220,6 +223,33 @@ export function ChatProvider({ }, }); + useEffect(() => { + /** + * This is a hack to ensure that the assistant messages have a stable id + * across server and client. + * We should move away from this either when the vercel/ai package supports it + * natively, or when we move away from streaming. + */ + return messages.forEach((message) => { + if (message.role !== "assistant") { + return; + } + + const idIsStable = message.id.startsWith("a-"); + if (idIsStable) { + return; + } + + const idFromContent = findMessageIdFromContent(message); + if (idFromContent) { + message.id = idFromContent; + return; + } + + message.id = "TEMP_PENDING_" + nanoid(); + }); + }, [messages]); + const { tempLessonPlan } = useTemporaryLessonPlanWithStreamingEdits({ lessonPlan, messages, diff --git a/packages/aila/src/core/Aila.ts b/packages/aila/src/core/Aila.ts index 92f8898db..ccc36178a 100644 --- a/packages/aila/src/core/Aila.ts +++ b/packages/aila/src/core/Aila.ts @@ -14,6 +14,7 @@ import { AilaPersistenceFeature, AilaThreatDetectionFeature, } from "../features/types"; +import { generateMessageId } from "../helpers/chat/generateMessageId"; import { AilaAuthenticationError, AilaGenerationError } from "./AilaError"; import { AilaFeatureFactory } from "./AilaFeatureFactory"; import { @@ -230,7 +231,11 @@ export class Aila implements AilaServices { ); } if (input) { - const message: Message = { id: nanoid(16), role: "user", content: input }; + const message: Message = { + id: generateMessageId({ role: "user" }), + role: "user", + content: input, + }; this._chat.addMessage(message); } if (title) { diff --git a/packages/aila/src/core/chat/AilaChat.ts b/packages/aila/src/core/chat/AilaChat.ts index 62756a73c..87f4d05cf 100644 --- a/packages/aila/src/core/chat/AilaChat.ts +++ b/packages/aila/src/core/chat/AilaChat.ts @@ -262,6 +262,8 @@ export class AilaChat implements AilaChatService { content, }; this.addMessage(assistantMessage); + + return assistantMessage; } private async enqueueFinalState() { @@ -272,6 +274,13 @@ export class AilaChat implements AilaChatService { }); } + private async enqueueMessageId(messageId: string) { + await this.enqueue({ + type: "id", + value: messageId, + }); + } + public async createChatCompletionStream(messages: Message[]) { return this._llmService.createChatCompletionStream({ model: this._aila.options.model ?? DEFAULT_MODEL, @@ -283,7 +292,10 @@ export class AilaChat implements AilaChatService { public async complete() { await this.reportUsageMetrics(); this.applyEdits(); - this.appendAssistantMessage(); + const assistantMessage = this.appendAssistantMessage(); + if (assistantMessage) { + await this.enqueueMessageId(assistantMessage.id); + } await this.moderate(); await this.enqueueFinalState(); await this.persistChat(); diff --git a/packages/aila/src/core/prompt/AilaPromptBuilder.ts b/packages/aila/src/core/prompt/AilaPromptBuilder.ts index 794ba99b8..0ff27baf8 100644 --- a/packages/aila/src/core/prompt/AilaPromptBuilder.ts +++ b/packages/aila/src/core/prompt/AilaPromptBuilder.ts @@ -33,7 +33,7 @@ export abstract class AilaPromptBuilder { const parsed = JSON.parse(toParse); if ( - ["state", "comment", "moderation", "action"].includes( + ["state", "comment", "moderation", "action", "id"].includes( parsed.type, ) ) { diff --git a/packages/aila/src/features/persistence/AilaPersistence.ts b/packages/aila/src/features/persistence/AilaPersistence.ts index df282c385..35123917a 100644 --- a/packages/aila/src/features/persistence/AilaPersistence.ts +++ b/packages/aila/src/features/persistence/AilaPersistence.ts @@ -1,7 +1,7 @@ import { GenerationStatus } from "@prisma/client"; import invariant from "tiny-invariant"; -import { AilaChatService, AilaServices, Message } from "../../core"; +import { AilaChatService, AilaError, AilaServices, Message } from "../../core"; import { AilaOptionsWithDefaultFallbackValues } from "../../core/types"; import { LooseLessonPlan } from "../../protocol/schema"; import { AilaGeneration } from "../generation"; @@ -63,7 +63,7 @@ export abstract class AilaPersistence { id, systemPrompt, completedAt, - chat: { userId, id: appSessionId }, + chat: { userId, id: appSessionId, messages }, responseText, queryDuration, tokenUsage, @@ -72,7 +72,7 @@ export abstract class AilaPersistence { invariant(userId, "userId is required for generation persistence"); invariant(promptId, "promptId is required for generation persistence"); - return { + const payload: GenerationPersistencePayload = { id, userId, appId: "lesson-planner", @@ -85,6 +85,26 @@ export abstract class AilaPersistence { promptId, status, }; + + const lastMessage = messages[messages.length - 1]; + + if (status === "SUCCESS") { + /** + * On success, we set the messageId to associate the generation with the last message + * + * @todo later it would make sense to generate the message_id earlier so that it is + * stored against the generation from the start + */ + + if (lastMessage?.role !== "assistant") { + throw new AilaError( + "Failed to create Generation payload: last message is not an assistant message", + ); + } + payload.messageId = lastMessage?.id; + } + + return payload; } abstract upsertChat(): Promise; @@ -112,6 +132,7 @@ export interface GenerationPersistencePayload { promptText: string; completedAt?: Date; appSessionId?: string; + messageId?: string; response: string; llmTimeTaken: number; completionTokensUsed: number; diff --git a/packages/aila/src/features/persistence/adaptors/prisma/index.ts b/packages/aila/src/features/persistence/adaptors/prisma/index.ts index be4ac9325..3550c86fb 100644 --- a/packages/aila/src/features/persistence/adaptors/prisma/index.ts +++ b/packages/aila/src/features/persistence/adaptors/prisma/index.ts @@ -1,4 +1,8 @@ -import { PrismaClientWithAccelerate, prisma as globalPrisma } from "@oakai/db"; +import { + Prisma, + PrismaClientWithAccelerate, + prisma as globalPrisma, +} from "@oakai/db"; import { AilaPersistence } from "../.."; import { AilaChatService, AilaServices } from "../../../../core"; @@ -52,15 +56,20 @@ export class AilaPrismaPersistence extends AilaPersistence { } if (payload.id) { + const updatePayload: Prisma.GenerationUpdateInput = { + status: payload.status, + response: payload.response, + completedAt: payload.completedAt, + llmTimeTaken: payload.llmTimeTaken, + }; + if (payload.messageId) { + // Only update messageId if it exists + updatePayload.messageId = payload.messageId; + } await this._prisma.generation.upsert({ where: { id: payload.id }, create: payload, - update: { - status: payload.status, - response: payload.response, - completedAt: payload.completedAt, - llmTimeTaken: payload.llmTimeTaken, - }, + update: updatePayload, }); } else { const result = await this._prisma.generation.create({ diff --git a/packages/aila/src/protocol/jsonPatchProtocol.ts b/packages/aila/src/protocol/jsonPatchProtocol.ts index 89f0c2a61..7ac229011 100644 --- a/packages/aila/src/protocol/jsonPatchProtocol.ts +++ b/packages/aila/src/protocol/jsonPatchProtocol.ts @@ -1,6 +1,11 @@ import { moderationCategoriesSchema } from "@oakai/core/src/utils/ailaModeration/moderationSchema"; import * as Sentry from "@sentry/nextjs"; -import { Operation, applyPatch, deepClone } from "fast-json-patch"; +import { + Operation, + applyPatch, + deepClone, + JsonPatchError, +} from "fast-json-patch"; import untruncateJson from "untruncate-json"; import { z } from "zod"; import zodToJsonSchema from "zod-to-json-schema"; @@ -237,6 +242,11 @@ export const BadDocumentSchema = z.object({ export type BadDocument = z.infer; +export const MessageIdDocumentSchema = z.object({ + type: z.literal("id"), + value: z.string(), +}); + export const JsonPatchDocumentSchema = z.discriminatedUnion("type", [ PatchDocumentSchema, PromptDocumentSchema, @@ -245,6 +255,7 @@ export const JsonPatchDocumentSchema = z.discriminatedUnion("type", [ ErrorDocumentSchema, ActionDocumentSchema, ModerationDocumentSchema, + MessageIdDocumentSchema, ]); // #TODO these optional schemas are perhaps not correct - they should be marked as optional @@ -257,9 +268,12 @@ export const JsonPatchDocumentOptionalSchema = z.discriminatedUnion("type", [ ErrorDocumentSchema, ActionDocumentSchema, ModerationDocumentSchema, + MessageIdDocumentSchema, ]); -export type JsonPatchDocumentOptional = z.infer; +export type JsonPatchDocumentOptional = z.infer< + typeof JsonPatchDocumentOptionalSchema +>; export type JsonPatchDocument = z.infer; @@ -398,10 +412,16 @@ export function applyLessonPlanPatch( ); updatedLessonPlan = { ...newUpdatedLessonPlan }; } catch (e) { + const extra: Record = {}; + if (e instanceof JsonPatchError) { + extra["index"] = e.index; + extra["operation"] = e.operation; + extra["tree"] = e.tree; + } console.error("Failed to apply patch", e); Sentry.withScope(function (scope) { scope.setLevel("info"); - Sentry.captureException(e); + Sentry.captureException(e, { extra }); }); } diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index d4ce7445f..b40ee9685 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -40,22 +40,22 @@ model App { } model Prompt { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id String @id @default(cuid()) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") name String slug String identifier String? - version Int @default(0) - app App @relation(fields: [appId], references: [id], onDelete: Cascade) - appId String @map("app_id") - hash String @unique + version Int @default(0) + app App @relation(fields: [appId], references: [id], onDelete: Cascade) + appId String @map("app_id") + hash String @unique variant String? - current Boolean @default(true) - gitSha String? @map("git_sha") + current Boolean @default(true) + gitSha String? @map("git_sha") template String // The prompt body - inputSchema Json @map("input_schema") // A JSON-schema to apply to prompt inputs - outputSchema Json @map("output_schema") // A JSON-schema to apply to completions + inputSchema Json @map("input_schema") // A JSON-schema to apply to prompt inputs + outputSchema Json @map("output_schema") // A JSON-schema to apply to completions statistics Statistics[] AnswersAndDistractorsForJudgement AnswersAndDistractorsForJudgement[] @@ -144,11 +144,11 @@ model AilaUserModification { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - userId String @map("user_id") - chatId String @map("chat_id") - messageId String @map("message_id") + userId String @map("user_id") + chatId String @map("chat_id") + messageId String @map("message_id") textForMod String @map("text_for_mod") - action AilaUserModificationAction @map("action") + action AilaUserModificationAction @map("action") @@map("aila_user_modifications") } @@ -349,11 +349,12 @@ model Generation { createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - appId String @map("app_id") - + appId String @map("app_id") + promptId String @map("prompt_id") appSessionId String? @map("app_session_id") + messageId String? @map("message_id") userId String @map("user_id") // The user that requested this generation status GenerationStatus @default(REQUESTED) From e90784ee3615a1322c5b5620cc7f16333c010497 Mon Sep 17 00:00:00 2001 From: mantagen Date: Mon, 2 Sep 2024 14:18:48 +0100 Subject: [PATCH 15/18] lint --- packages/aila/src/core/Aila.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/aila/src/core/Aila.ts b/packages/aila/src/core/Aila.ts index ccc36178a..17b2fe4dd 100644 --- a/packages/aila/src/core/Aila.ts +++ b/packages/aila/src/core/Aila.ts @@ -1,5 +1,4 @@ import { PrismaClientWithAccelerate, prisma as globalPrisma } from "@oakai/db"; -import { nanoid } from "nanoid"; import { DEFAULT_MODEL, From 7415c25a76df9f421dbf77f00713d74f5b4f3cb8 Mon Sep 17 00:00:00 2001 From: mantagen Date: Mon, 2 Sep 2024 17:04:16 +0100 Subject: [PATCH 16/18] adjust migration date --- .../migration.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/db/prisma/migrations/{20240822143910_flag_adjustments => 20240828143910_flag_adjustments}/migration.sql (100%) diff --git a/packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql b/packages/db/prisma/migrations/20240828143910_flag_adjustments/migration.sql similarity index 100% rename from packages/db/prisma/migrations/20240822143910_flag_adjustments/migration.sql rename to packages/db/prisma/migrations/20240828143910_flag_adjustments/migration.sql From b7f96e6e62613408a86f1c3f7c01c678f655aedd Mon Sep 17 00:00:00 2001 From: mantagen Date: Mon, 2 Sep 2024 17:06:41 +0100 Subject: [PATCH 17/18] migration --- .../migration.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql diff --git a/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql b/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql new file mode 100644 index 000000000..5242c6dda --- /dev/null +++ b/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql @@ -0,0 +1,11 @@ +/* + Warnings: + + - Made the column `feedback_message` on table `generation_user_flags` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "generation_user_flags" ALTER COLUMN "feedback_message" SET NOT NULL; + +-- AlterTable +ALTER TABLE "generations" ADD COLUMN "message_id" TEXT; From a32169cabfe947ce95b919cd904536f758ca7234 Mon Sep 17 00:00:00 2001 From: mantagen Date: Mon, 2 Sep 2024 18:32:29 +0100 Subject: [PATCH 18/18] fix migration --- .../20240902160513_generations_message_id/migration.sql | 9 --------- packages/db/prisma/schema.prisma | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql b/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql index 5242c6dda..95c588e8d 100644 --- a/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql +++ b/packages/db/prisma/migrations/20240902160513_generations_message_id/migration.sql @@ -1,11 +1,2 @@ -/* - Warnings: - - - Made the column `feedback_message` on table `generation_user_flags` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "generation_user_flags" ALTER COLUMN "feedback_message" SET NOT NULL; - -- AlterTable ALTER TABLE "generations" ADD COLUMN "message_id" TEXT; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index b40ee9685..d18ce39d6 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -426,8 +426,8 @@ model GenerationUserFlag { generation Generation @relation(fields: [generationId], references: [id], onDelete: Cascade) generationId String @map("generation_id") - feedbackMessage String @map("feedback_message") - feedbackValue String @map("feedback_value") // The fragment of the generation that feedback was given on + feedbackMessage String? @map("feedback_message") + feedbackValue String @map("feedback_value") // The fragment of the generation that feedback was given on feedbackReasons Json @@map("generation_user_flags")
- {humanizeCamelCaseString(objectKey)} -
Loading