diff --git a/apps/nextjs/src/ai-apps/lesson-planner/lessonSection.ts b/apps/nextjs/src/ai-apps/lesson-planner/lessonSection.ts index 42f59dafc..ca1a25a18 100644 --- a/apps/nextjs/src/ai-apps/lesson-planner/lessonSection.ts +++ b/apps/nextjs/src/ai-apps/lesson-planner/lessonSection.ts @@ -1,14 +1,14 @@ export const lessonSections = [ "Title", "Subject", - "Key Stage", - "Learning Outcome", - "Learning Cycles", - "Prior Knowledge", - "Key Learning Points", + "Key stage", + "Learning outcome", + "Learning cycles", + "Prior knowledge", + "Key learning points", "Misconceptions", "Keywords", - "Starter Quiz", - "Cycle 1", - "Exit Quiz", + "Starter quiz", + "Learning cycle 1", + "Exit quiz", ]; diff --git a/apps/nextjs/src/app/aila/[id]/download/DownloadView.tsx b/apps/nextjs/src/app/aila/[id]/download/DownloadView.tsx new file mode 100644 index 000000000..818d42298 --- /dev/null +++ b/apps/nextjs/src/app/aila/[id]/download/DownloadView.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { AilaPersistedChat } from "@oakai/aila/src/protocol/schema"; +import { Box, Flex, Grid } from "@radix-ui/themes"; + +import Layout from "@/components/AppComponents/Layout"; +import Button from "@/components/Button"; +import DialogContents from "@/components/DialogControl/DialogContents"; +import { DialogRoot } from "@/components/DialogControl/DialogRoot"; +import { DownloadButton } from "@/components/DownloadButton"; +import { Icon } from "@/components/Icon"; + +import { SurveyDialogLauncher } from "./SurveyDialogLauncher"; +import { useDownloadView } from "./useDownloadView"; + +type DownloadViewProps = Readonly<{ + chat: AilaPersistedChat; + featureFlag: boolean; +}>; +export function DownloadView({ + chat, + featureFlag, +}: Readonly) { + const { lessonPlan } = chat; + const { + lessonSlidesExport, + worksheetExport, + lessonPlanExport, + starterQuizExport, + exitQuizExport, + additionalMaterialsExport, + sections, + totalSections, + totalSectionsComplete, + } = useDownloadView(chat); + + return ( + + + + +
+ + + + +
+

Download resources

+

+ Choose the resources you would like to generate and download. +

+
+ + + lessonPlanExport.start()} + title="Lesson" + subTitle="Overview of the complete lesson" + downloadAvailable={!!lessonPlanExport.readyToExport} + downloadLoading={lessonPlanExport.status === "loading"} + data={lessonPlanExport.data} + exportsType="lessonPlanDoc" + data-testid="chat-download-lesson-plan" + lesson={lessonPlan} + /> + starterQuizExport.start()} + title="Starter quiz" + subTitle="Questions and answers to assess prior knowledge" + downloadAvailable={!!starterQuizExport.readyToExport} + downloadLoading={starterQuizExport.status === "loading"} + data={starterQuizExport.data} + exportsType="starterQuiz" + lesson={lessonPlan} + /> + lessonSlidesExport.start()} + data-testid="chat-download-slides-btn" + title="Slide deck" + subTitle="Learning outcome, keywords and learning cycles" + downloadAvailable={lessonSlidesExport.readyToExport} + downloadLoading={lessonSlidesExport.status === "loading"} + data={lessonSlidesExport.data} + exportsType="lessonSlides" + lesson={lessonPlan} + /> + worksheetExport.start()} + title="Worksheet" + subTitle="Practice tasks" + downloadAvailable={!!worksheetExport.readyToExport} + downloadLoading={worksheetExport.status === "loading"} + data={worksheetExport.data} + lesson={lessonPlan} + exportsType="worksheet" + /> + exitQuizExport.start()} + title="Exit quiz" + subTitle="Questions and answers to assess understanding" + downloadAvailable={!!exitQuizExport.readyToExport} + downloadLoading={exitQuizExport.status === "loading"} + data={exitQuizExport.data} + exportsType="exitQuiz" + lesson={lessonPlan} + /> + {lessonPlan.additionalMaterials && + lessonPlan.additionalMaterials !== "None" && ( + additionalMaterialsExport.start()} + title="Additional materials" + subTitle="Document containing any additional materials" + downloadAvailable={ + !!additionalMaterialsExport.readyToExport + } + downloadLoading={ + additionalMaterialsExport.status === "loading" + } + data={additionalMaterialsExport.data} + exportsType="additionalMaterials" + lesson={lessonPlan} + /> + )} + + +
+ + {`${totalSectionsComplete} of ${totalSections} sections complete`} + +
+ {sections.map((section) => { + return ( + + + + +

{section.label}

+
+ ); + })} +
+
+
+
+
+
+
+ ); +} diff --git a/apps/nextjs/src/app/aila/[id]/download/SurveyDialogLauncher.ts b/apps/nextjs/src/app/aila/[id]/download/SurveyDialogLauncher.ts new file mode 100644 index 000000000..d672efb9b --- /dev/null +++ b/apps/nextjs/src/app/aila/[id]/download/SurveyDialogLauncher.ts @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +import { usePosthogFeedbackSurvey } from "hooks/surveys/usePosthogFeedbackSurvey"; + +import { useDialog } from "@/components/AppComponents/DialogContext"; + +export function SurveyDialogLauncher() { + const { survey } = usePosthogFeedbackSurvey({ + closeDialog: () => null, + surveyName: "End of Aila generation survey launch aug24", + }); + const { setDialogWindow } = useDialog(); + + useEffect(() => { + if (survey) { + const timer = setTimeout(() => { + setDialogWindow("feedback"); + }, 3000); + return () => clearTimeout(timer); + } + }, [survey, setDialogWindow]); + + return null; +} diff --git a/apps/nextjs/src/app/aila/[id]/download/index.tsx b/apps/nextjs/src/app/aila/[id]/download/index.tsx deleted file mode 100644 index 49347ebfe..000000000 --- a/apps/nextjs/src/app/aila/[id]/download/index.tsx +++ /dev/null @@ -1,232 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -import { AilaPersistedChat } from "@oakai/aila/src/protocol/schema"; -import { Box, Flex, Grid } from "@radix-ui/themes"; -import { lessonSections } from "ai-apps/lesson-planner/lessonSection"; -import { usePosthogFeedbackSurvey } from "hooks/surveys/usePosthogFeedbackSurvey"; - -import { setLessonPlanProgress } from "@/components/AppComponents/Chat/Chat/utils"; -import { useDialog } from "@/components/AppComponents/DialogContext"; -import Layout from "@/components/AppComponents/Layout"; -import Button from "@/components/Button"; -import DialogContents from "@/components/DialogControl/DialogContents"; -import { DialogRoot } from "@/components/DialogControl/DialogRoot"; -import { DownloadButton } from "@/components/DownloadButton"; -import { useExportAdditionalMaterials } from "@/components/ExportsDialogs/useExportAdditionalMaterials"; -import { useExportLessonPlanDoc } from "@/components/ExportsDialogs/useExportLessonPlanDoc"; -import { useExportLessonSlides } from "@/components/ExportsDialogs/useExportLessonSlides"; -import { useExportQuizDoc } from "@/components/ExportsDialogs/useExportQuizDoc"; -import { useExportWorksheetSlides } from "@/components/ExportsDialogs/useExportWorksheetSlides"; -import { Icon } from "@/components/Icon"; - -interface DownloadPageProps { - chat: AilaPersistedChat; - featureFlag: boolean; -} -export default function DownloadPage({ - chat, - featureFlag, -}: Readonly) { - return ( - - - - ); -} - -export function DownloadPageContents({ chat }: Readonly) { - const { setDialogWindow } = useDialog(); - - const [undefinedLessonPlanSections, setUndefinedLessonPlanSections] = - useState([]); - const lessonPlan = chat.lessonPlan; - useEffect(() => { - setLessonPlanProgress({ - lessonPlan: lessonPlan, - setUndefinedItems: setUndefinedLessonPlanSections, - }); - }, [lessonPlan, setUndefinedLessonPlanSections]); - - const exportProps = { - onStart: () => null, - lesson: lessonPlan, - chatId: chat.id, - messageId: chat.messages.length, - active: true, - }; - - const lessonSlidesExport = useExportLessonSlides(exportProps); - - const worksheetExport = useExportWorksheetSlides(exportProps); - - const lessonPlanExport = useExportLessonPlanDoc(exportProps); - - const starterQuizExport = useExportQuizDoc({ - ...exportProps, - quizType: "starter", - }); - - const exitQuizExport = useExportQuizDoc({ - ...exportProps, - quizType: "exit", - }); - - const additionalMaterialsExport = useExportAdditionalMaterials(exportProps); - - const { survey } = usePosthogFeedbackSurvey({ - closeDialog: () => null, - surveyName: "End of Aila generation survey launch aug24", - }); - - useEffect(() => { - if (survey) { - const timer = setTimeout(() => { - setDialogWindow("feedback"); - }, 3000); - return () => clearTimeout(timer); - } - }, [survey, setDialogWindow]); - - return ( - - -
- - - - -
-

Download resources

-

- Complete all 12 sections of your lesson to unlock resources - below. If a section is missing, just ask Aila to help you - complete your lesson. -

-
- - - lessonPlanExport.start()} - title="Lesson plan" - subTitle="All sections" - downloadAvailable={!!lessonPlanExport.readyToExport} - downloadLoading={lessonPlanExport.status === "loading"} - data={lessonPlanExport.data} - exportsType="lessonPlanDoc" - data-testid="chat-download-lesson-plan" - lesson={lessonPlan} - /> - starterQuizExport.start()} - title="Starter quiz" - subTitle="Questions and answers" - downloadAvailable={!!starterQuizExport.readyToExport} - downloadLoading={starterQuizExport.status === "loading"} - data={starterQuizExport.data} - exportsType="starterQuiz" - lesson={lessonPlan} - /> - lessonSlidesExport.start()} - data-testid="chat-download-slides-btn" - title="Slide deck" - subTitle="Outcomes, key words, 1-3 learning cycles, summary" - downloadAvailable={lessonSlidesExport.readyToExport} - downloadLoading={lessonSlidesExport.status === "loading"} - data={lessonSlidesExport.data} - exportsType="lessonSlides" - lesson={lessonPlan} - /> - worksheetExport.start()} - title="Worksheet" - subTitle="Interactive activities" - downloadAvailable={!!worksheetExport.readyToExport} - downloadLoading={worksheetExport.status === "loading"} - data={worksheetExport.data} - lesson={lessonPlan} - exportsType="worksheet" - /> - exitQuizExport.start()} - title="Exit quiz" - subTitle="Questions and answers" - downloadAvailable={!!exitQuizExport.readyToExport} - downloadLoading={exitQuizExport.status === "loading"} - data={exitQuizExport.data} - exportsType="exitQuiz" - lesson={lessonPlan} - /> - {lessonPlan.additionalMaterials && - lessonPlan.additionalMaterials !== "None" && ( - additionalMaterialsExport.start()} - title="Additional materials" - subTitle="Document containing any additional materials" - downloadAvailable={ - !!additionalMaterialsExport.readyToExport - } - downloadLoading={ - additionalMaterialsExport.status === "loading" - } - data={additionalMaterialsExport.data} - exportsType="additionalMaterials" - lesson={lessonPlan} - /> - )} - - -
- - {`${lessonSections.length - undefinedLessonPlanSections.length} - of ${lessonSections.length} sections complete`} - -
- {lessonSections.map((section) => { - return ( - - - - -

- {section.includes("Cycle 1") ? "Cycles 1-3" : section} -

-
- ); - })} -
-
-
-
-
-
- ); -} diff --git a/apps/nextjs/src/app/aila/[id]/download/page.tsx b/apps/nextjs/src/app/aila/[id]/download/page.tsx index a077481d9..64c13c5ac 100644 --- a/apps/nextjs/src/app/aila/[id]/download/page.tsx +++ b/apps/nextjs/src/app/aila/[id]/download/page.tsx @@ -5,7 +5,7 @@ import { type Metadata } from "next"; import { getChatById } from "@/app/actions"; import { serverSideFeatureFlag } from "@/utils/serverSideFeatureFlag"; -import DownloadView from "."; +import { DownloadView } from "./DownloadView"; export interface DownloadPageProps { params: { diff --git a/apps/nextjs/src/app/aila/[id]/download/useDownloadView.ts b/apps/nextjs/src/app/aila/[id]/download/useDownloadView.ts new file mode 100644 index 000000000..67692096b --- /dev/null +++ b/apps/nextjs/src/app/aila/[id]/download/useDownloadView.ts @@ -0,0 +1,55 @@ +import { AilaPersistedChat } from "@oakai/aila/src/protocol/schema"; + +import { useProgressForDownloads } from "@/components/AppComponents/Chat/Chat/hooks/useProgressForDownloads"; +import { useExportAdditionalMaterials } from "@/components/ExportsDialogs/useExportAdditionalMaterials"; +import { useExportLessonPlanDoc } from "@/components/ExportsDialogs/useExportLessonPlanDoc"; +import { useExportLessonSlides } from "@/components/ExportsDialogs/useExportLessonSlides"; +import { useExportQuizDoc } from "@/components/ExportsDialogs/useExportQuizDoc"; +import { useExportWorksheetSlides } from "@/components/ExportsDialogs/useExportWorksheetSlides"; + +export function useDownloadView({ + id, + lessonPlan, + messages, +}: AilaPersistedChat) { + const exportProps = { + onStart: () => null, + lesson: lessonPlan, + chatId: id, + messageId: messages.length, + active: true, + }; + + const lessonSlidesExport = useExportLessonSlides(exportProps); + + const worksheetExport = useExportWorksheetSlides(exportProps); + + const lessonPlanExport = useExportLessonPlanDoc(exportProps); + + const starterQuizExport = useExportQuizDoc({ + ...exportProps, + quizType: "starter", + }); + + const exitQuizExport = useExportQuizDoc({ + ...exportProps, + quizType: "exit", + }); + + const additionalMaterialsExport = useExportAdditionalMaterials(exportProps); + + const { sections, totalSections, totalSectionsComplete } = + useProgressForDownloads({ lessonPlan, isStreaming: false }); + + return { + lessonSlidesExport, + worksheetExport, + lessonPlanExport, + starterQuizExport, + exitQuizExport, + additionalMaterialsExport, + sections, + totalSections, + totalSectionsComplete, + }; +} diff --git a/apps/nextjs/src/app/aila/help/index.tsx b/apps/nextjs/src/app/aila/help/index.tsx index 6b64a81be..169a73e15 100644 --- a/apps/nextjs/src/app/aila/help/index.tsx +++ b/apps/nextjs/src/app/aila/help/index.tsx @@ -115,7 +115,7 @@ const Help = () => { include the following sections:

    -
  • Lesson learning outcome
  • +
  • Learning outcome
  • Learning cycle outcomes
  • Prior knowledge
  • Key learning points
  • diff --git a/apps/nextjs/src/app/quiz-designer/preview/[slug]/preview.tsx b/apps/nextjs/src/app/quiz-designer/preview/[slug]/preview.tsx index 3d46801c6..b77d2c825 100644 --- a/apps/nextjs/src/app/quiz-designer/preview/[slug]/preview.tsx +++ b/apps/nextjs/src/app/quiz-designer/preview/[slug]/preview.tsx @@ -33,7 +33,7 @@ export default function QuizPreview(questions, featureFlag) { return (
    -

    Question {index + 1}:

    +

    Question {index + 1}.

    {question.question.value}

    diff --git a/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useProgressForDownloads.ts b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useProgressForDownloads.ts new file mode 100644 index 000000000..8a03c54c5 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/hooks/useProgressForDownloads.ts @@ -0,0 +1,127 @@ +import { useMemo } from "react"; + +import { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; +import { lessonPlanSectionsSchema } from "@oakai/exports/src/schema/input.schema"; +import { ZodIssue } from "zod"; + +/** + * For a given list of Zod issues and lessonPlan fields, checks that none of + * the errors pertain to the fields. + */ +function getCompleteness(errors: ZodIssue[], fields: string[]) { + const hasErrorInSomeField = errors.reduce( + (acc, curr) => acc || fields.some((field) => curr.path[0] === field), + false, + ); + + return !hasErrorInSomeField; +} +type ProgressSections = { + label: string; + key: string; + complete: boolean; +}[]; +type ProgressForDownloads = { + sections: ProgressSections; + totalSections: number; + totalSectionsComplete: number; +}; + +export function useProgressForDownloads({ + lessonPlan, + isStreaming, +}: { + lessonPlan: LooseLessonPlan; + isStreaming: boolean; +}): ProgressForDownloads { + return useMemo(() => { + const parsedLessonPlan = lessonPlanSectionsSchema.safeParse(lessonPlan); + const errors = + parsedLessonPlan.error?.errors.filter((error) => { + if (isStreaming) { + /** + * When we have partial data, we get errors about nested + * properties. We want to filter these out during streaming to avoid a + * flickering progress bar. + */ + return error.path.length === 1; + } + if (!isStreaming) { + /** + * When not streaming, we want to check that each section is successfully + * parsed, so we keep all errors. + * + * This means we know it's in the correct structure for exports. + * + * Most of the time, this will be the same as the above condition because + * we don't expect to have errors in the data structure when Aila is not + * streaming. + */ + return true; + } + }) || []; + const sections = [ + { + label: "Lesson details", + key: "title", + complete: getCompleteness(errors, ["title", "subject", "keyStage"]), + }, + { + label: "Learning outcome", + key: "learningOutcome", + complete: getCompleteness(errors, ["learningOutcome"]), + }, + { + label: "Learning cycle outcomes", + key: "learningCycles", + complete: getCompleteness(errors, ["learningCycles"]), + }, + { + label: "Prior knowledge", + key: "priorKnowledge", + complete: getCompleteness(errors, ["priorKnowledge"]), + }, + { + label: "Key learning points", + key: "keyLearningPoints", + complete: getCompleteness(errors, ["keyLearningPoints"]), + }, + { + label: "Misconceptions", + key: "misconceptions", + complete: getCompleteness(errors, ["misconceptions"]), + }, + { + label: "Keywords", + key: "keywords", + complete: getCompleteness(errors, ["keywords"]), + }, + { + label: "Starter quiz", + key: "starterQuiz", + complete: getCompleteness(errors, ["starterQuiz"]), + }, + { + label: "Learning cycles", + key: "cycle1", + complete: getCompleteness(errors, ["cycle1", "cycle2", "cycle3"]), + }, + { + label: "Exit quiz", + key: "exitQuiz", + complete: getCompleteness(errors, ["exitQuiz"]), + }, + ]; + + const totalSections = sections.length; + const totalSectionsComplete = sections.filter( + (section) => section.complete, + ).length; + + return { + sections, + totalSections, + totalSectionsComplete, + }; + }, [lessonPlan, isStreaming]); +} 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 ae04d9409..8cbf375e4 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/utils/index.ts @@ -1,34 +1,6 @@ import { type LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; -import { findListOfUndefinedKeysFromZod } from "@oakai/exports/src/dataHelpers/findListOfUndefinedKeysFromZod"; -import { lessonPlanSectionsSchema } from "@oakai/exports/src/schema/input.schema"; import { type Message } from "ai/react"; -export function setLessonPlanProgress({ - lessonPlan, - setUndefinedItems, -}: { - lessonPlan: LooseLessonPlan; - setUndefinedItems: React.Dispatch>; -}) { - const parseForProgress = lessonPlanSectionsSchema.safeParse(lessonPlan); - - if (!parseForProgress.success) { - const undefinedItems = findListOfUndefinedKeysFromZod( - parseForProgress.error.errors as { - expected: string; - received: string; - code: string; - path: (string | number)[]; - message: string; - }[], - ); - - setUndefinedItems(undefinedItems); - } else { - setUndefinedItems([]); - } -} - 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-dropdownsection.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx index f8c599961..bf3560881 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-dropdownsection.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { sectionToMarkdown } from "@oakai/aila/src/protocol/sectionToMarkdown"; +import { camelCaseToSentenceCase } from "@oakai/core/src/utils/camelCaseToSentenceCase"; import { lessonSectionTitlesAndMiniDescriptions } from "data/lessonSectionTitlesAndMiniDescriptions"; import { Icon } from "@/components/Icon"; @@ -94,9 +95,7 @@ const DropDownSection = ({ onClick={() => setIsOpen(!isOpen)} className="flex w-full justify-between" > -

    - {humanizeCamelCaseString(objectKey)} -

    +

    {sectionTitle(objectKey)}

    @@ -153,13 +152,12 @@ const valuesAreEqual = ( return val1 === val2; }; -export function humanizeCamelCaseString(str: string) { +export function sectionTitle(str: string) { if (str.startsWith("cycle")) { - return "Cycle " + str.split("cycle")[1]; + return "Learning cycle " + str.split("cycle")[1]; } - return str.replace(/([A-Z])/g, " $1").replace(/^./, function (str) { - return str.toUpperCase(); - }); + + return camelCaseToSentenceCase(str); } export default DropDownSection; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx index 55256f80e..584496591 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanMapToMarkDown.tsx @@ -4,7 +4,7 @@ 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 { sectionTitle } from "./chat-dropdownsection"; import { notEmpty } from "./chat-lessonPlanDisplay"; import { MemoizedReactMarkdownWithStyles } from "./markdown"; @@ -72,7 +72,7 @@ const ChatSection = ({ sectionRefs, objectKey, value }) => { lessonPlanSectionDescription={ lessonSectionTitlesAndMiniDescriptions[objectKey]?.description } - markdown={`# ${objectKey.includes("cycle") ? "Cycle " + objectKey.split("cycle")[1] : humanizeCamelCaseString(objectKey)} + markdown={`# ${sectionTitle(objectKey)} ${sectionToMarkdown(objectKey, value)}`} /> 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..55bfe94b2 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx @@ -17,27 +17,27 @@ const ChatStartAccordion = () => { const slidesLessonSections = lessonSections.filter( (section) => section !== "Title" && - section !== "Key Stage" && + section !== "Key stage" && section !== "Subject" && - section !== "Prior Knowledge" && - section !== "Key Learning Points" && + section !== "Prior knowledge" && + section !== "Key learning points" && section !== "Misconceptions" && - section !== "Starter Quiz" && + section !== "Starter quiz" && section !== "Exit Quiz", ); const quizLessonSections = lessonSections.filter( (section) => section !== "Title" && - section !== "Key Stage" && + section !== "Key stage" && section !== "Subject" && - section !== "Prior Knowledge" && - section !== "Key Learning Points" && + section !== "Prior lnowledge" && + section !== "Key learning points" && section !== "Misconceptions" && - section !== "Cycle 1" && + section !== "Learning cycle 1" && section !== "Keywords" && - section !== "Learning Cycles" && - section !== "Learning Outcome", + section !== "Learning cycles" && + section !== "Learning outcome", ); return ( @@ -51,7 +51,7 @@ const ChatStartAccordion = () => { {lessonSections.map((section) => { if ( section == "Title" || - section == "Key Stage" || + section == "Key stage" || section == "Subject" ) { return null; @@ -79,7 +79,7 @@ const ChatStartAccordion = () => { {slidesLessonSections.map((section) => { if ( section == "Title" || - section == "Key Stage" || + section == "Key stage" || section == "Subject" ) { return null; @@ -107,7 +107,7 @@ const ChatStartAccordion = () => { {quizLessonSections.map((section) => { if ( section == "Title" || - section == "Key Stage" || + section == "Key stage" || section == "Subject" ) { return null; 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..073de838a 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/empty-screen-accordian.tsx @@ -16,18 +16,18 @@ const EmptyScreenAccordion = () => { (section) => ![ "Title", - "Key Stage", + "Key stage", "Subject", - "Prior Knowledge", - "Key Learning Points", + "Prior knowledge", + "Key learning points", "Misconceptions", - "Starter Quiz", - "Exit Quiz", + "Starter quiz", + "Exit quiz", ].includes(section), ); const quizLessonSections = lessonSections.filter( - (section) => !["Title", "Key Stage", "Subject"].includes(section), + (section) => !["Title", "Key stage", "Subject"].includes(section), ); return ( @@ -41,7 +41,7 @@ const EmptyScreenAccordion = () => { {lessonSections.map((section) => { if ( section === "Title" || - section === "Key Stage" || + section === "Key stage" || section === "Subject" ) { return null; @@ -69,7 +69,7 @@ const EmptyScreenAccordion = () => { {slidesLessonSections.map((section) => { if ( section == "Title" || - section == "Key Stage" || + section == "Key stage" || section == "Subject" ) { return null; @@ -97,7 +97,7 @@ const EmptyScreenAccordion = () => { {quizLessonSections.map((section) => { if ( section == "Title" || - section == "Key Stage" || + section == "Key stage" || section == "Subject" ) { return null; diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.stories.tsx index 413e72904..cbac20242 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.stories.tsx @@ -1,3 +1,9 @@ +import { + Cycle, + Keyword, + Misconception, + Quiz, +} from "@oakai/aila/src/protocol/schema"; import type { Meta, StoryObj } from "@storybook/react"; import { LessonPlanProgressDropdown } from "./LessonPlanProgressDropdown"; @@ -14,11 +20,15 @@ type Story = StoryObj; export const Default: Story = { args: { lessonPlan: { + // 1 (lesson details) title: "Introduction to Glaciation", keyStage: "key-stage-3", subject: "geography", + // 2 learningOutcome: "Sample learning outcome", + // 3 learningCycles: ["Sample learning cycles"], + // 4 priorKnowledge: ["Sample prior knowledge"], }, sectionRefs: { @@ -40,11 +50,23 @@ export const PartiallyCompleted: Story = { args: { ...Default.args, lessonPlan: { - ...(Default?.args?.lessonPlan ?? {}), + // 1 (lesson details) + title: "Introduction to Glaciation", + keyStage: "key-stage-3", + subject: "geography", + // 2 + learningOutcome: "Sample learning outcome", + // 3 + learningCycles: ["Sample learning cycles"], + // 4 + priorKnowledge: ["Sample prior knowledge"], + // 5 keyLearningPoints: ["Sample key learning point"], - misconceptions: [{ misconception: "Sample misconception" }], - cycle1: { title: "Sample cycle 1" }, - // Only cycle1 is completed + // 6 + misconceptions: [sampleMisconception()], + // 7 + cycle1: sampleCycle(1), + // Only cycle1 is completed, but that is sufficient for downloads }, }, }; @@ -53,22 +75,30 @@ export const FullyCompleted: Story = { args: { ...Default.args, lessonPlan: { + // 1 (lesson details) title: "Introduction to Glaciation", keyStage: "key-stage-3", subject: "geography", + // 2 learningOutcome: "Sample learning outcome", + // 3 learningCycles: ["Sample learning cycles"], + // 4 priorKnowledge: ["Sample prior knowledge"], + // 5 keyLearningPoints: ["Sample key learning points"], - misconceptions: [{ misconception: "Sample misconceptions" }], - keywords: [{ keyword: "Sample keyword" }], - starterQuiz: [ - { question: "Sample starter quiz", answers: ["Sample answer"] }, - ], - cycle1: { title: "Sample cycle 1" }, - cycle2: { title: "Sample cycle 2" }, - cycle3: { title: "Sample cycle 3" }, - exitQuiz: [{ question: "Sample exit quiz", answers: ["Answer"] }], + // 6 + misconceptions: [sampleMisconception()], + // 7 + keywords: [sampleKeyword()], + // 8 + starterQuiz: sampleQuiz(), + // 9 + cycle1: sampleCycle(1), + cycle2: sampleCycle(2), + cycle3: sampleCycle(3), + // 10 + exitQuiz: sampleQuiz(), additionalMaterials: "Sample additional materials", }, }, @@ -78,10 +108,58 @@ export const PartialCycles: Story = { args: { ...Default.args, lessonPlan: { + // 1 - 4 ...(Default?.args?.lessonPlan ?? {}), - cycle1: { title: "Sample cycle 1" }, - cycle2: { title: "Sample cycle 2" }, - // cycle3 is missing + // 5 + cycle1: sampleCycle(1), + cycle2: sampleCycle(2), + // cycle3 is missing, but that is sufficient for downloads }, }, }; + +function sampleCycle(i: number): Cycle { + return { + title: `Sample cycle ${i}`, + durationInMinutes: 10, + explanation: { + spokenExplanation: "Sample spoken explanation", + accompanyingSlideDetails: "Sample slide details", + imagePrompt: "Sample image prompt", + slideText: "Sample slide text", + }, + checkForUnderstanding: [ + { + question: "Sample question", + answers: ["Sample answer"], + distractors: ["Sample distractor"], + }, + ], + practice: "Sample practice", + feedback: "Sample feedback", + }; +} + +function sampleMisconception(): Misconception { + return { + misconception: "Sample misconception", + description: "Sample description", + }; +} + +function sampleKeyword(): Keyword { + return { + keyword: "Sample keyword", + definition: "Sample description", + }; +} + +function sampleQuiz(): Quiz { + return [ + { + question: "Sample question", + answers: ["Sample answer"], + distractors: ["Sample distractor"], + }, + ]; +} diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.test.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.test.tsx index 1ba24fc35..6665475c5 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.test.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.test.tsx @@ -21,23 +21,23 @@ const { Default, PartiallyCompleted, FullyCompleted, PartialCycles } = describe("LessonPlanProgressDropdown", () => { it("renders the Default story with correct closed state", () => { render(); - expect(screen.getByText("6 of 12 sections complete")).toBeInTheDocument(); + expect(screen.getByText("4 of 10 sections complete")).toBeInTheDocument(); expect(screen.queryByText("Cycles")).not.toBeInTheDocument(); }); it("renders the PartiallyCompleted story with correct closed state", () => { render(); - expect(screen.getByText("8 of 12 sections complete")).toBeInTheDocument(); + expect(screen.getByText("7 of 10 sections complete")).toBeInTheDocument(); }); it("renders the FullyCompleted story with correct closed state", () => { render(); - expect(screen.getByText("12 of 12 sections complete")).toBeInTheDocument(); + expect(screen.getByText("10 of 10 sections complete")).toBeInTheDocument(); }); it("renders the PartialCycles story with correct closed state", () => { render(); - expect(screen.getByText("6 of 12 sections complete")).toBeInTheDocument(); + expect(screen.getByText("5 of 10 sections complete")).toBeInTheDocument(); }); it("displays the dropdown menu when clicked and shows correct completed sections", async () => { @@ -59,23 +59,21 @@ describe("LessonPlanProgressDropdown", () => { "lesson-plan-progress-dropdown-content", ); expect(cyclesSection).toBeInTheDocument(); - expect(cyclesSection).toHaveTextContent("Cycles 1-3"); + expect(cyclesSection).toHaveTextContent("Learning cycles"); const dropdownContent = screen.getByTestId( "lesson-plan-progress-dropdown-content", ); const sectionStates = [ - { name: "Title", completed: true }, - { name: "Subject", completed: true }, - { name: "Key Stage", completed: true }, - { name: "Learning Outcome", completed: true }, - { name: "Learning Cycles", completed: true }, - { name: "Prior Knowledge", completed: true }, - { name: "Key Learning Points", completed: true }, + { name: "Lesson details", completed: true }, + { name: "Learning outcome", completed: true }, + { name: "Learning cycle outcomes", completed: true }, + { name: "Prior knowledge", completed: true }, + { name: "Key learning points", completed: true }, { name: "Misconceptions", completed: true }, { name: "Keywords", completed: false }, - { name: "Starter Quiz", completed: false }, - { name: "Cycles 1-3", completed: false }, - { name: "Exit Quiz", completed: false }, + { name: "Starter quiz", completed: false }, + { name: "Learning cycles", completed: true }, + { name: "Exit quiz", completed: false }, ]; sectionStates.forEach(({ name, completed }) => { @@ -108,18 +106,16 @@ describe("LessonPlanProgressDropdown", () => { "lesson-plan-progress-dropdown-content", ); const allSections = [ - "Title", - "Subject", - "Key Stage", - "Learning Outcome", - "Learning Cycles", - "Prior Knowledge", - "Key Learning Points", + "Lesson details", + "Learning outcome", + "Learning cycle outcomes", + "Prior knowledge", + "Key learning points", "Misconceptions", "Keywords", - "Starter Quiz", - "Cycles 1-3", - "Exit Quiz", + "Starter quiz", + "Learning cycles", + "Exit quiz", ]; allSections.forEach((name) => { @@ -147,18 +143,16 @@ describe("LessonPlanProgressDropdown", () => { "lesson-plan-progress-dropdown-content", ); const sectionStates = [ - { name: "Title", completed: true }, - { name: "Subject", completed: true }, - { name: "Key Stage", completed: true }, - { name: "Learning Outcome", completed: true }, - { name: "Learning Cycles", completed: true }, - { name: "Prior Knowledge", completed: true }, - { name: "Key Learning Points", completed: false }, + { name: "Lesson details", completed: true }, + { name: "Learning outcome", completed: true }, + { name: "Learning cycle outcomes", completed: true }, + { name: "Prior knowledge", completed: true }, + { name: "Key learning points", completed: false }, { name: "Misconceptions", completed: false }, { name: "Keywords", completed: false }, - { name: "Starter Quiz", completed: false }, - { name: "Cycles 1-3", completed: false }, - { name: "Exit Quiz", completed: false }, + { name: "Starter quiz", completed: false }, + { name: "Learning cycles", completed: true }, + { name: "Exit quiz", completed: false }, ]; sectionStates.forEach(({ name, completed }) => { diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.tsx index cb206f8d7..72b1aa62a 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/LessonPlanProgressDropdown.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from "react"; +import React, { useState } from "react"; import { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; @@ -7,53 +7,22 @@ import { Flex } from "@radix-ui/themes"; import { Icon } from "@/components/Icon"; import { scrollToRef } from "@/utils/scrollToRef"; -export const LESSON_PLAN_SECTIONS = [ - { key: "title", title: "Title" }, - { key: "subject", title: "Subject" }, - { key: "keyStage", title: "Key Stage" }, - { key: "learningOutcome", title: "Learning Outcome" }, - { key: "learningCycles", title: "Learning Cycles" }, - { key: "priorKnowledge", title: "Prior Knowledge" }, - { key: "keyLearningPoints", title: "Key Learning Points" }, - { key: "misconceptions", title: "Misconceptions" }, - { key: "keywords", title: "Keywords" }, - { key: "starterQuiz", title: "Starter Quiz" }, - { key: "cycles", title: "Cycles 1-3" }, - { key: "exitQuiz", title: "Exit Quiz" }, - // { key: "additionalMaterials", title: "Additional Materials" }, Do not show the additional materials section -] as const; +import { useProgressForDownloads } from "../Chat/hooks/useProgressForDownloads"; -export type LessonPlanSectionKey = (typeof LESSON_PLAN_SECTIONS)[number]["key"]; - -export type LessonPlanProgressDropdownProps = { +type LessonPlanProgressDropdownProps = Readonly<{ lessonPlan: LooseLessonPlan; + isStreaming: boolean; sectionRefs: Record>; documentContainerRef: React.MutableRefObject; -}; - -export const lessonPlanSectionIsComplete = ( - lessonPlan: LooseLessonPlan, - key: LessonPlanSectionKey, -) => { - if (key === "cycles") { - return lessonPlan.cycle1 && lessonPlan.cycle2 && lessonPlan.cycle3; - } - return lessonPlan[key] !== undefined && lessonPlan[key] !== null; -}; +}>; export const LessonPlanProgressDropdown: React.FC< LessonPlanProgressDropdownProps -> = ({ lessonPlan, sectionRefs, documentContainerRef }) => { +> = ({ lessonPlan, sectionRefs, documentContainerRef, isStreaming }) => { + const { sections, totalSections, totalSectionsComplete } = + useProgressForDownloads({ lessonPlan, isStreaming }); const [openProgressDropDown, setOpenProgressDropDown] = useState(false); - const completedSections = useMemo(() => { - return LESSON_PLAN_SECTIONS.filter(({ key }) => - lessonPlanSectionIsComplete(lessonPlan, key), - ).length; - }, [lessonPlan]); - - const allCyclesComplete = lessonPlanSectionIsComplete(lessonPlan, "cycles"); - return ( - {`${completedSections} of ${LESSON_PLAN_SECTIONS.length} sections complete`} + {`${totalSectionsComplete} of ${totalSections} sections complete`} - {LESSON_PLAN_SECTIONS.map(({ key, title }) => ( - + {sections.map(({ label, complete, key }) => ( + ))} diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.tsx index c1cb0b125..5b2b1399b 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.tsx @@ -29,6 +29,7 @@ const ExportButtons = ({
    @@ -79,10 +80,10 @@ const ExportButtons = ({ export default ExportButtons; export function handleRewordingSections(section: string) { - if (section.includes("Cycle 1")) { - return "Learning Cycles"; + if (section.includes("Learning cycle 1")) { + return "Learning cycles"; } - if (section === "Learning Cycles") { + if (section === "Learning cycles") { return "Learning Cycle Outcomes"; } return section; diff --git a/apps/nextjs/src/components/DownloadButton.tsx b/apps/nextjs/src/components/DownloadButton.tsx index 91e9d299f..9c6104f20 100644 --- a/apps/nextjs/src/components/DownloadButton.tsx +++ b/apps/nextjs/src/components/DownloadButton.tsx @@ -71,7 +71,7 @@ export const DownloadButton = ({
    - Download {title} (.{ext}) + Download {title.toLocaleLowerCase()} (.{ext}) All sections
    diff --git a/packages/aila/src/protocol/sectionToMarkdown.ts b/packages/aila/src/protocol/sectionToMarkdown.ts index 8f48efa64..4bef3bd89 100644 --- a/packages/aila/src/protocol/sectionToMarkdown.ts +++ b/packages/aila/src/protocol/sectionToMarkdown.ts @@ -1,4 +1,4 @@ -import { humanizeCamelCaseString } from "@oakai/core/src/utils/humanizeCamelCaseString"; +import { camelCaseToSentenceCase } from "@oakai/core/src/utils/camelCaseToSentenceCase"; import { isArray, isNumber, isObject, isString } from "remeda"; import { @@ -20,18 +20,18 @@ export function sortIgnoringSpecialChars(strings: string[]): string[] { } const keyToHeadingMappings = { - spokenExplanation: "Teacher Explanation", - imagePrompt: "Image Search Suggestion", - cycle1: "Learning Cycle 1", - cycle2: "Learning Cycle 2", - cycle3: "Learning Cycle 3", + spokenExplanation: "Teacher explanation", + imagePrompt: "Image search suggestion", + cycle1: "Learning cycle 1", + cycle2: "Learning cycle 2", + cycle3: "Learning cycle 3", }; export function keyToHeading(key: string) { if (key in keyToHeadingMappings) { return keyToHeadingMappings[key as keyof typeof keyToHeadingMappings]; } - return humanizeCamelCaseString(key); + return camelCaseToSentenceCase(key); } export function sectionToMarkdown( key: string, @@ -120,7 +120,7 @@ export function organiseAnswersAndDistractors(quiz: QuizOptional) { ...answers, ...distractors, ]).join("\n"); - return `### ${i + 1}: ${v.question ?? "…"}\n\n${answersAndDistractors}`; + return `### ${i + 1}. ${v.question ?? "…"}\n\n${answersAndDistractors}`; }) .join("\n\n"); } diff --git a/packages/core/src/models/lessonPlans.ts b/packages/core/src/models/lessonPlans.ts index bfabf076b..37a7ce5eb 100644 --- a/packages/core/src/models/lessonPlans.ts +++ b/packages/core/src/models/lessonPlans.ts @@ -20,8 +20,8 @@ import { inngest } from "../client"; import { createOpenAIClient } from "../llm/openai"; import { template } from "../prompts/lesson-assistant"; import { RAG } from "../rag"; +import { camelCaseToSentenceCase } from "../utils/camelCaseToSentenceCase"; import { embedWithCache } from "../utils/embeddings"; -import { humanizeCamelCaseString } from "../utils/humanizeCamelCaseString"; import { Caption, CaptionsSchema } from "./types/caption"; // Simplifies the input to a string for embedding @@ -319,7 +319,7 @@ ${part.content}`; } const text = lessonPlan.parts .map( - (p) => `${humanizeCamelCaseString(p.key)} + (p) => `${camelCaseToSentenceCase(p.key)} ${p.content}`, ) .join("\n\n"); diff --git a/packages/core/src/prompts/lesson-assistant/old.ts b/packages/core/src/prompts/lesson-assistant/old.ts index d529624db..4ae3db5e3 100644 --- a/packages/core/src/prompts/lesson-assistant/old.ts +++ b/packages/core/src/prompts/lesson-assistant/old.ts @@ -483,7 +483,7 @@ Are you happy with the learning outcomes and learning cycles? If not, select **'Retry'** or type an edit in the text box below. -When you are happy with this section, tap **'Continue'** and I will suggest 'prior knowledge', 'key learning points', 'common misconceptions' and 'key words'. +When you are happy with this section, tap **'Continue'** and I will suggest 'prior knowledge', 'key learning points', 'common misconceptions' and 'keywords'. END OF EXAMPLE HAPPINESS CHECK START OF SECOND EXAMPLE HAPPINESS CHECK diff --git a/packages/core/src/prompts/lesson-assistant/parts/yourInstructions.ts b/packages/core/src/prompts/lesson-assistant/parts/yourInstructions.ts index 40e1929b6..854d7ea39 100644 --- a/packages/core/src/prompts/lesson-assistant/parts/yourInstructions.ts +++ b/packages/core/src/prompts/lesson-assistant/parts/yourInstructions.ts @@ -104,7 +104,7 @@ Are you happy with the learning outcomes and learning cycles? If not, select **'Retry'** or type an edit in the text box below. -When you are happy with this section, tap **'Continue'** and I will suggest 'prior knowledge', 'key learning points', 'common misconceptions' and 'key words'. +When you are happy with this section, tap **'Continue'** and I will suggest 'prior knowledge', 'key learning points', 'common misconceptions' and 'keywords'. END OF EXAMPLE HAPPINESS CHECK START OF SECOND EXAMPLE HAPPINESS CHECK diff --git a/packages/core/src/utils/camelCaseToSentenceCase.ts b/packages/core/src/utils/camelCaseToSentenceCase.ts new file mode 100644 index 000000000..79b81c414 --- /dev/null +++ b/packages/core/src/utils/camelCaseToSentenceCase.ts @@ -0,0 +1,6 @@ +export function camelCaseToSentenceCase(str: string) { + return str + .replace(/([A-Z0-9])/g, " $1") // Insert a space before each uppercase letter or digit + .replace(/^./, (str) => str.toUpperCase()) + .replace(/(?<=\s)[A-Z]/g, (str) => str.toLowerCase()); +} diff --git a/packages/core/src/utils/humanizeCamelCaseString.ts b/packages/core/src/utils/humanizeCamelCaseString.ts deleted file mode 100644 index ff987fa5d..000000000 --- a/packages/core/src/utils/humanizeCamelCaseString.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function humanizeCamelCaseString(str: string) { - return str.replace(/([A-Z0-9])/g, " $1").replace(/^./, function (str) { - return str.toUpperCase(); - }); -} diff --git a/packages/exports/src/dataHelpers/findListOfUndefinedKeysFromZod.ts b/packages/exports/src/dataHelpers/findListOfUndefinedKeysFromZod.ts deleted file mode 100644 index 648fe23d0..000000000 --- a/packages/exports/src/dataHelpers/findListOfUndefinedKeysFromZod.ts +++ /dev/null @@ -1,36 +0,0 @@ -export function humanizeCamelCaseString(str: string) { - return str.replace(/([A-Z])/g, " $1").replace(/^./, function (str) { - return str.toUpperCase(); - }); -} - -export function findListOfUndefinedKeysFromZod( - errors: { - expected: string; - received: string; - code: string; - path: (string | number)[]; - message: string; - }[], -) { - const undefinedItems: (string | number)[] = []; - - // Map through errors and find undefined items - errors.forEach((e) => { - if (e.received === "undefined") { - undefinedItems.push(...e.path); - } - }); - - // Make array human readable - const humanReadableArray = undefinedItems.map((item) => { - if (typeof item === "number") { - return `[${item}]`; - } - if (item.includes("cycle")) { - return "Cycle " + item.split("cycle")[1]; - } - return humanizeCamelCaseString(item); - }); - return humanReadableArray; -} diff --git a/packages/exports/src/schema/input.schema.ts b/packages/exports/src/schema/input.schema.ts index 838ed8338..0a5d9993e 100644 --- a/packages/exports/src/schema/input.schema.ts +++ b/packages/exports/src/schema/input.schema.ts @@ -78,6 +78,7 @@ export const lessonPlanSectionsSchema = z.object({ cycle3: cycleSchema.nullish(), additionalMaterials: z.string().nullish(), }); +export type LessonPlanSections = z.infer; export type LessonPlanDocInputData = z.infer;