diff --git a/apps/nextjs/.storybook/preview.tsx b/apps/nextjs/.storybook/preview.tsx index 59f9ee53d..0f552f1be 100644 --- a/apps/nextjs/.storybook/preview.tsx +++ b/apps/nextjs/.storybook/preview.tsx @@ -8,7 +8,11 @@ import "@fontsource/lexend/800.css"; import "@fontsource/lexend/900.css"; import { OakThemeProvider, oakDefaultTheme } from "@oaknational/oak-components"; import type { Preview, Decorator } from "@storybook/react"; -import { initialize as initializeMsw, mswLoader } from "msw-storybook-addon"; +import { + initialize as initializeMsw, + mswLoader, + MswParameters, +} from "msw-storybook-addon"; import { TooltipProvider } from "../src/components/AppComponents/Chat/ui/tooltip"; import { DialogProvider } from "../src/components/AppComponents/DialogContext"; @@ -19,6 +23,12 @@ import { chromaticParams } from "./chromatic"; import { RadixThemeDecorator } from "./decorators/RadixThemeDecorator"; import "./preview.css"; +declare module "@storybook/csf" { + interface Parameters { + msw?: MswParameters["msw"]; + } +} + initializeMsw(); const preview: Preview = { diff --git a/apps/nextjs/src/app/actions.ts b/apps/nextjs/src/app/actions.ts index b0d64870b..b365ff4bc 100644 --- a/apps/nextjs/src/app/actions.ts +++ b/apps/nextjs/src/app/actions.ts @@ -43,7 +43,7 @@ export async function getChatById( id: string, ): Promise { const session = await prisma?.appSession.findUnique({ - where: { id }, + where: { id, deletedAt: null }, }); if (!session) { diff --git a/apps/nextjs/src/app/aila/[id]/download/DownloadView.stories.tsx b/apps/nextjs/src/app/aila/[id]/download/DownloadView.stories.tsx index 164aaa6e6..f61acc667 100644 --- a/apps/nextjs/src/app/aila/[id]/download/DownloadView.stories.tsx +++ b/apps/nextjs/src/app/aila/[id]/download/DownloadView.stories.tsx @@ -6,7 +6,7 @@ import { chromaticParams } from "@/storybook/chromatic"; import { DemoProvider } from "../../../../../src/components/ContextProviders/Demo"; import { DownloadContent } from "./DownloadView"; -const meta: Meta = { +const meta = { title: "Pages/Chat/Download", component: DownloadContent, parameters: { @@ -20,10 +20,10 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const chat: AilaPersistedChat = { id: "nSLmbQ1LO75zLTcA", diff --git a/apps/nextjs/src/app/aila/[id]/share/index.stories.tsx b/apps/nextjs/src/app/aila/[id]/share/index.stories.tsx index 2136dbd62..4c4ba5945 100644 --- a/apps/nextjs/src/app/aila/[id]/share/index.stories.tsx +++ b/apps/nextjs/src/app/aila/[id]/share/index.stories.tsx @@ -5,17 +5,17 @@ import { chromaticParams } from "@/storybook/chromatic"; import ShareChat from "./"; -const meta: Meta = { +const meta = { title: "Pages/Chat/Share", component: ShareChat, parameters: { layout: "fullscreen", ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const lessonPlan: LooseLessonPlan = { title: "The End of Roman Britain", diff --git a/apps/nextjs/src/app/aila/help/index.stories.tsx b/apps/nextjs/src/app/aila/help/index.stories.tsx index 2fabd3d90..20695aa7b 100644 --- a/apps/nextjs/src/app/aila/help/index.stories.tsx +++ b/apps/nextjs/src/app/aila/help/index.stories.tsx @@ -5,7 +5,7 @@ import { chromaticParams } from "@/storybook/chromatic"; import { HelpContent } from "."; -const meta: Meta = { +const meta = { title: "Pages/Chat/Help", component: HelpContent, parameters: { @@ -20,10 +20,10 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/app/api/aila-download-all/route.ts b/apps/nextjs/src/app/api/aila-download-all/route.ts index 37d46e7f6..cb4b8031d 100644 --- a/apps/nextjs/src/app/api/aila-download-all/route.ts +++ b/apps/nextjs/src/app/api/aila-download-all/route.ts @@ -1,6 +1,5 @@ import { auth } from "@clerk/nextjs/server"; -import type { LessonExportType } from "@oakai/db"; -import { prisma } from "@oakai/db"; +import { prisma, type LessonExportType } from "@oakai/db"; import { downloadDriveFile } from "@oakai/exports"; import * as Sentry from "@sentry/node"; import { kv } from "@vercel/kv"; @@ -9,6 +8,7 @@ import { PassThrough } from "stream"; import { withSentry } from "@/lib/sentry/withSentry"; +import { saveDownloadEvent } from "../aila-download/downloadHelpers"; import { sanitizeFilename } from "../sanitizeFilename"; type FileIdsAndFormats = { @@ -16,48 +16,6 @@ type FileIdsAndFormats = { formats: ReadonlyArray<"pptx" | "docx" | "pdf">; }[]; -function getReadableExportType(exportType: LessonExportType) { - switch (exportType) { - case "EXIT_QUIZ_DOC": - return "Exit quiz"; - case "LESSON_PLAN_DOC": - return "Lesson plan"; - case "STARTER_QUIZ_DOC": - return "Starter quiz"; - case "WORKSHEET_SLIDES": - return "Worksheet"; - case "LESSON_SLIDES_SLIDES": - return "Lesson slides"; - case "ADDITIONAL_MATERIALS_DOCS": - return "Additional materials"; - } -} - -async function saveDownloadEvent({ - lessonExportId, - downloadedBy, - ext, -}: { - lessonExportId: string; - downloadedBy: string; - ext: string; -}) { - try { - await prisma.lessonExportDownload.create({ - data: { - lessonExportId, - downloadedBy, - ext, - }, - }); - } catch (error) { - Sentry.captureException(error, { - level: "warning", - extra: { lessonExportId, downloadedBy, ext }, - }); - } -} - function nodePassThroughToReadableStream(passThrough: PassThrough) { return new ReadableStream({ start(controller) { @@ -77,6 +35,23 @@ function nodePassThroughToReadableStream(passThrough: PassThrough) { }); } +function getReadableExportType(exportType: LessonExportType) { + switch (exportType) { + case "EXIT_QUIZ_DOC": + return "Exit quiz"; + case "LESSON_PLAN_DOC": + return "Lesson plan"; + case "STARTER_QUIZ_DOC": + return "Starter quiz"; + case "WORKSHEET_SLIDES": + return "Worksheet"; + case "LESSON_SLIDES_SLIDES": + return "Lesson slides"; + case "ADDITIONAL_MATERIALS_DOCS": + return "Additional materials"; + } +} + async function getHandler(req: Request): Promise { const { searchParams } = new URL(req.url); const fileIdsParam = searchParams.get("fileIds"); diff --git a/apps/nextjs/src/app/api/aila-download/downloadHelpers.ts b/apps/nextjs/src/app/api/aila-download/downloadHelpers.ts new file mode 100644 index 000000000..7ce92b445 --- /dev/null +++ b/apps/nextjs/src/app/api/aila-download/downloadHelpers.ts @@ -0,0 +1,27 @@ +import { prisma } from "@oakai/db"; +import * as Sentry from "@sentry/node"; + +export async function saveDownloadEvent({ + lessonExportId, + downloadedBy, + ext, +}: { + lessonExportId: string; + downloadedBy: string; + ext: string; +}) { + try { + await prisma.lessonExportDownload.create({ + data: { + lessonExportId, + downloadedBy, + ext, + }, + }); + } catch (error) { + Sentry.captureException(error, { + level: "warning", + extra: { lessonExportId, downloadedBy, ext }, + }); + } +} diff --git a/apps/nextjs/src/app/api/aila-download/route.ts b/apps/nextjs/src/app/api/aila-download/route.ts index 64c9010bc..6177ba41e 100644 --- a/apps/nextjs/src/app/api/aila-download/route.ts +++ b/apps/nextjs/src/app/api/aila-download/route.ts @@ -1,12 +1,12 @@ import { auth } from "@clerk/nextjs/server"; -import type { LessonExportType } from "@oakai/db"; -import { prisma } from "@oakai/db"; +import { prisma, type LessonExportType } from "@oakai/db"; import { downloadDriveFile } from "@oakai/exports"; import * as Sentry from "@sentry/node"; import { withSentry } from "@/lib/sentry/withSentry"; import { sanitizeFilename } from "../sanitizeFilename"; +import { saveDownloadEvent } from "./downloadHelpers"; // From: https://www.ericburel.tech/blog/nextjs-stream-files async function* nodeStreamToIterator(stream: NodeJS.ReadableStream) { @@ -46,31 +46,6 @@ function getReadableExportType(exportType: LessonExportType) { } } -async function saveDownloadEvent({ - lessonExportId, - downloadedBy, - ext, -}: { - lessonExportId: string; - downloadedBy: string; - ext: string; -}) { - try { - await prisma.lessonExportDownload.create({ - data: { - lessonExportId, - downloadedBy, - ext, - }, - }); - } catch (error) { - Sentry.captureException(error, { - level: "warning", - extra: { lessonExportId, downloadedBy, ext }, - }); - } -} - async function getHandler(req: Request): Promise { const { searchParams } = new URL(req.url); @@ -135,7 +110,7 @@ async function getHandler(req: Request): Promise { }); } - saveDownloadEvent({ + await saveDownloadEvent({ lessonExportId: lessonExport.id, downloadedBy: userId, ext, diff --git a/apps/nextjs/src/app/faqs/index.stories.tsx b/apps/nextjs/src/app/faqs/index.stories.tsx index 35b3504b9..937727546 100644 --- a/apps/nextjs/src/app/faqs/index.stories.tsx +++ b/apps/nextjs/src/app/faqs/index.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { FAQPageContent } from "."; -const meta: Meta = { +const meta = { title: "Pages/FAQs", component: FAQPageContent, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/app/home-page.stories.tsx b/apps/nextjs/src/app/home-page.stories.tsx index 3e370c474..f4db6a63e 100644 --- a/apps/nextjs/src/app/home-page.stories.tsx +++ b/apps/nextjs/src/app/home-page.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { HomePageContent } from "./home-page"; -const meta: Meta = { +const meta = { title: "Pages/Homepage", component: HomePageContent, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { diff --git a/apps/nextjs/src/app/legal/[slug]/legal.stories.tsx b/apps/nextjs/src/app/legal/[slug]/legal.stories.tsx index 928cfee17..087a95fcf 100644 --- a/apps/nextjs/src/app/legal/[slug]/legal.stories.tsx +++ b/apps/nextjs/src/app/legal/[slug]/legal.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { LegalContent } from "./legal"; -const meta: Meta = { +const meta = { title: "Pages/Legal/Sanity dynamic", component: LegalContent, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const fixture = { pageData: { diff --git a/apps/nextjs/src/app/legal/account-locked/account-locked.stories.tsx b/apps/nextjs/src/app/legal/account-locked/account-locked.stories.tsx index 3376e8996..d3802b627 100644 --- a/apps/nextjs/src/app/legal/account-locked/account-locked.stories.tsx +++ b/apps/nextjs/src/app/legal/account-locked/account-locked.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { AccountLocked } from "./account-locked"; -const meta: Meta = { +const meta = { title: "Pages/Legal/Account Locked", component: AccountLocked, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/app/prompts/prompts.stories.tsx b/apps/nextjs/src/app/prompts/prompts.stories.tsx index 97b18aea5..9fdb74c13 100644 --- a/apps/nextjs/src/app/prompts/prompts.stories.tsx +++ b/apps/nextjs/src/app/prompts/prompts.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { PromptsContent } from "./prompts"; -const meta: Meta = { +const meta = { title: "Pages/Prompts", component: PromptsContent, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const fixture = { apps: [ diff --git a/apps/nextjs/src/components/AppComponents/Chat/Chat/ChatModerationDisplay.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/Chat/ChatModerationDisplay.stories.tsx index ec9bcd8d9..93fb1b5b8 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/Chat/ChatModerationDisplay.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/Chat/ChatModerationDisplay.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ChatModerationDisplay } from "./ChatModerationDisplay"; -const meta: Meta = { +const meta = { title: "Components/Dialogs/ChatModerationDisplay", component: ChatModerationDisplay, tags: ["autodocs"], @@ -14,10 +14,10 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const toxicModeration: PersistedModerationBase = { id: "mock-moderation-id", diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-history.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-history.stories.tsx index d5ccab452..58ae81767 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-history.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-history.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ChatHistory } from "./chat-history"; -const meta: Meta = { +const meta = { title: "Components/Sidebar/ChatHistory", component: ChatHistory, parameters: { @@ -17,10 +17,10 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.stories.tsx index bb02e9628..7cff21bc8 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lessonPlanDisplay.stories.tsx @@ -21,19 +21,22 @@ const chatContext: Partial = { ailaStreamingStatus: "Idle", }; -const meta: Meta = { +const meta = { title: "Components/LessonPlan/LessonPlanDisplay", component: LessonPlanDisplay, tags: ["autodocs"], decorators: [ChatDecorator], args: { documentContainerRef: { current: null }, + chatEndRef: undefined, + sectionRefs: {}, + showLessonMobile: false, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx index e4e63c3f9..d18674a0b 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx @@ -1,30 +1,32 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; import { ChatDecorator } from "@/storybook/decorators/ChatDecorator"; import ChatLhsHeader from "./chat-lhs-header"; -const meta: Meta = { +const meta = { title: "Components/Chat/ChatLhsHeader", component: ChatLhsHeader, tags: ["autodocs"], decorators: [ChatDecorator], args: { showStreamingStatus: false, + setShowLessonMobile: fn(), + showLessonMobile: false, + isDemoUser: false, }, parameters: { chatContext: { ailaStreamingStatus: "Idle", }, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const Default: Story = { - args: {}, -}; +export const Default: Story = {}; export const NonProdStreamingStatus: Story = { args: { diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-list/demo-limit-message.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-list/demo-limit-message.stories.tsx index 135fef708..626d41a72 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-list/demo-limit-message.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-list/demo-limit-message.stories.tsx @@ -4,7 +4,7 @@ import { ChatModerationProvider } from "@/components/ContextProviders/ChatModera import { DemoLimitMessage } from "./demo-limit-message"; -const meta: Meta = { +const meta = { title: "Components/Chat/DemoLimitMessage", component: DemoLimitMessage, tags: ["autodocs"], @@ -15,10 +15,13 @@ const meta: Meta = { ), ], -}; + args: { + id: "test-chat-id", + }, +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-list/in-chat-download-buttons.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-list/in-chat-download-buttons.stories.tsx index 179e99445..0a3a91d47 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-list/in-chat-download-buttons.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-list/in-chat-download-buttons.stories.tsx @@ -7,7 +7,7 @@ import { import { InChatDownloadButtons } from "./in-chat-download-buttons"; -const meta: Meta = { +const meta = { title: "Components/Chat/InChatDownloadButtons", component: InChatDownloadButtons, tags: ["autodocs"], @@ -18,10 +18,10 @@ const meta: Meta = { parameters: { ...demoParams({ isDemoUser: true }), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.stories.tsx index 1895e0b16..a0bfcc600 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.stories.tsx @@ -3,17 +3,17 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ChatMessagePart } from "./ChatMessagePart"; -const meta: Meta = { +const meta = { title: "Components/Chat/ChatMessagePart", component: ChatMessagePart, tags: ["autodocs"], args: { inspect: false, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const basePart: Omit = { type: "message-part", diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.tsx index 0d944e59e..1876d9e4b 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/ChatMessagePart.tsx @@ -8,8 +8,6 @@ import { aiLogger } from "@oakai/logger"; import { MemoizedReactMarkdownWithStyles } from "@/components/AppComponents/Chat/markdown"; -import type { ModerationModalHelpers } from "../../FeedbackForms/ModerationFeedbackModal"; - const log = aiLogger("chat"); const components = { @@ -34,7 +32,6 @@ const components = { export interface ChatMessagePartProps { part: MessagePart; inspect: boolean; - moderationModalHelpers: ModerationModalHelpers; } export function ChatMessagePart({ diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.stories.tsx index 1b318e488..e2901608f 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.stories.tsx @@ -4,7 +4,7 @@ import { ChatModerationProvider } from "@/components/ContextProviders/ChatModera import { ChatMessage } from "./"; -const meta: Meta = { +const meta = { title: "Components/Chat/ChatMessage", component: ChatMessage, tags: ["autodocs"], @@ -16,12 +16,14 @@ const meta: Meta = { ), ], args: { + chatId: "test-chat-id", persistedModerations: [], + ailaStreamingStatus: "Idle", }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const UserMessage: Story = { args: { 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 4685bc77e..9ad0c1644 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx @@ -131,11 +131,7 @@ export function ChatMessage({ messageParts.map((part) => { return (
- +
); })} diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx index 989c13892..384b1f794 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx @@ -13,7 +13,7 @@ const DummyMessage: Message = { role: "user", }; -const meta: Meta = { +const meta = { title: "Components/Chat/ChatPanel", component: ChatPanel, tags: ["autodocs"], @@ -26,10 +26,10 @@ const meta: Meta = { messages: [DummyMessage], }, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const NoMessages: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx index 9ec91fd30..05adefe11 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx @@ -12,7 +12,7 @@ const DummyMessage: Message = { role: "user", }; -const meta: Meta = { +const meta = { title: "Components/Chat/ChatQuickButtons", component: ChatQuickButtons, tags: ["autodocs"], @@ -22,10 +22,10 @@ const meta: Meta = { messages: [DummyMessage], }, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Idle: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx deleted file mode 100644 index 4ce5992b9..000000000 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-start-accordion.tsx +++ /dev/null @@ -1,197 +0,0 @@ -"use client"; - -import React from "react"; - -import * as Accordion from "@radix-ui/react-accordion"; -import { lessonSections } from "ai-apps/lesson-planner/lessonSection"; - -import { Icon } from "@/components/Icon"; -import AiIcon from "@/components/SVGParts/AiIcon"; -import LessonIcon from "@/components/SVGParts/LessonIcon"; -import QuizIcon from "@/components/SVGParts/QuizIcon"; -import SlidesIcon from "@/components/SVGParts/SlidesIcon"; -import { convertTitleCaseToSentenceCase } from "@/utils/convertTitleCaseToSentenceCase"; - -import { handleRewordingSections } from "./export-buttons"; - -const ChatStartAccordion = () => { - const slidesLessonSections = lessonSections.filter( - (section) => - section !== "Title" && - section !== "Key stage" && - section !== "Subject" && - section !== "Prior knowledge" && - section !== "Key learning points" && - section !== "Misconceptions" && - section !== "Starter quiz" && - section !== "Exit Quiz", - ); - - const quizLessonSections = lessonSections.filter( - (section) => - section !== "Title" && - section !== "Key stage" && - section !== "Subject" && - section !== "Prior knowledge" && - section !== "Key learning points" && - section !== "Misconceptions" && - section !== "Learning cycle 1" && - section !== "Keywords" && - section !== "Learning cycles" && - section !== "Learning outcome", - ); - - return ( - - - - 1 lesson plan - - -
- {lessonSections.map((section) => { - if ( - section == "Title" || - section == "Key stage" || - section == "Subject" - ) { - return null; - } - return ( -
- - - {convertTitleCaseToSentenceCase( - handleRewordingSections(section), - )} - -
- ); - })} -
-
-
- - - 1 slide deck - - -
- {slidesLessonSections.map((section) => { - if ( - section == "Title" || - section == "Key stage" || - section == "Subject" - ) { - return null; - } - return ( -
- - - {convertTitleCaseToSentenceCase( - handleRewordingSections(section), - )} - -
- ); - })} -
-
-
- - - 2 quizzes - - -
- {quizLessonSections.map((section) => { - if ( - section == "Title" || - section == "Key stage" || - section == "Subject" - ) { - return null; - } - return ( -
- - - {convertTitleCaseToSentenceCase( - handleRewordingSections(section), - )} - -
- ); - })} -
-
-
- -
- - 1 worksheet - -
-
-
- ); -}; - -// Define prop types for each component -interface AccordionItemProps - extends React.ComponentPropsWithoutRef { - readonly children: React.ReactNode; -} - -interface AccordionTriggerProps - extends React.ComponentPropsWithoutRef { - readonly children: React.ReactNode; -} - -interface AccordionContentProps - extends React.ComponentPropsWithoutRef { - readonly children: React.ReactNode; -} - -const AccordionItem = React.forwardRef( - ({ children, ...props }, forwardedRef) => ( - - {children} - - ), -); -AccordionItem.displayName = "AccordionItem"; - -const AccordionTrigger = React.forwardRef< - HTMLDivElement, - AccordionTriggerProps ->(({ children, ...props }) => ( - - - {children} - - - -)); -AccordionTrigger.displayName = "AccordionTrigger"; - -const AccordionContent = React.forwardRef< - HTMLDivElement, - AccordionContentProps ->(({ children, ...props }, forwardedRef) => ( - -
{children}
-
-)); - -AccordionContent.displayName = "AccordionContent"; - -export default ChatStartAccordion; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-start-form.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-start-form.stories.tsx index 8ca82763a..22f08d579 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-start-form.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-start-form.stories.tsx @@ -2,14 +2,14 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ChatStartForm } from "./chat-start-form"; -const meta: Meta = { +const meta = { title: "Components/Chat Start/ChatStartForm", component: ChatStartForm, tags: ["autodocs"], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-start.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-start.stories.tsx index cb44eea25..45f1c3ecc 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-start.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-start.stories.tsx @@ -6,7 +6,7 @@ import { chromaticParams } from "@/storybook/chromatic"; import { DialogProvider } from "../DialogContext"; import { ChatStart } from "./chat-start"; -const meta: Meta = { +const meta = { title: "Pages/Chat/Chat Start", component: ChatStart, parameters: { @@ -23,10 +23,10 @@ const meta: Meta = { ), ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/clear-history.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/clear-history.stories.tsx index 084254fb1..de0d65c04 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/clear-history.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/clear-history.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import { ClearHistory } from "./clear-history"; -const meta: Meta = { +const meta = { title: "Components/Sidebar/ClearHistory", component: ClearHistory, tags: ["autodocs"], @@ -14,10 +14,10 @@ const meta: Meta = { return ; }, ], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { diff --git a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.stories.tsx index d425661e9..322496291 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/drop-down-section/index.stories.tsx @@ -9,7 +9,7 @@ const MAX_INT32 = 2 ** 31 - 1; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); -const meta: Meta = { +const meta = { title: "Components/LessonPlan/DropDownSection", component: DropDownSection, tags: ["autodocs"], @@ -19,6 +19,9 @@ const meta: Meta = { "I can explain the reasons why frogs are so important to British society and culture", documentContainerRef: { current: null }, streamingTimeout: 0, + userHasCancelledAutoScroll: false, + sectionRefs: {}, + showLessonMobile: false, }, decorators: [ChatDecorator], parameters: { @@ -38,10 +41,10 @@ const meta: Meta = { ailaStreamingStatus: "Idle", }, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, 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 db7a114f5..a9cf19bff 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 @@ -8,17 +8,18 @@ import type { Meta, StoryObj } from "@storybook/react"; import { LessonPlanProgressDropdown } from "./LessonPlanProgressDropdown"; -const meta: Meta = { +const meta = { title: "Components/LessonPlan/LessonPlanProgressDropdown", component: LessonPlanProgressDropdown, tags: ["autodocs"], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { + isStreaming: false, lessonPlan: { // 1 (lesson details) title: "Introduction to Glaciation", @@ -49,6 +50,7 @@ export const Default: Story = { export const PartiallyCompleted: Story = { args: { ...Default.args, + isStreaming: true, lessonPlan: { // 1 (lesson details) title: "Introduction to Glaciation", diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/MobileExportButtons.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/MobileExportButtons.stories.tsx index 542377fe5..42d1045d5 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/MobileExportButtons.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/MobileExportButtons.stories.tsx @@ -9,14 +9,14 @@ import { import { MobileExportButtons } from "./MobileExportButtons"; -const meta: Meta = { +const meta = { title: "Components/LessonPlan/MobileExportButtons", component: MobileExportButtons, tags: ["autodocs"], decorators: [ChatDecorator, DemoDecorator], parameters: { viewport: { - defaultViewport: "mobile1", + defaultViewport: "mobile", }, ...chromaticParams(["mobile"]), ...demoParams({ isDemoUser: false }), @@ -27,10 +27,10 @@ const meta: Meta = { args: { closeMobileLessonPullOut: () => {}, }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.stories.tsx index 60000c652..fc59a9a17 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/export-buttons/index.stories.tsx @@ -28,7 +28,7 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = {}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/guidance-required.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/guidance-required.stories.tsx index bae1cc2e9..e6df42f91 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/guidance-required.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/guidance-required.stories.tsx @@ -3,14 +3,14 @@ import type { Meta, StoryObj } from "@storybook/react"; import { GuidanceRequired } from "./guidance-required"; -const meta: Meta = { +const meta = { title: "Components/Chat/GuidanceRequired", component: GuidanceRequired, tags: ["autodocs"], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const mockModeration: PersistedModerationBase = { id: "moderated", diff --git a/apps/nextjs/src/components/AppComponents/Chat/header.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/header.stories.tsx index c92263948..7e39d249b 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/header.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/header.stories.tsx @@ -8,7 +8,7 @@ import { import { Header } from "./header"; -const meta: Meta = { +const meta = { title: "Components/Layout/ChatHeader", component: Header, tags: ["autodocs"], @@ -22,10 +22,10 @@ const meta: Meta = { }, ...demoParams({ isDemoUser: false }), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx b/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx index b970589d9..772bee5bd 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/sidebar-actions.stories.tsx @@ -3,17 +3,17 @@ import { within } from "@storybook/test"; import { SidebarActions } from "./sidebar-actions"; -const meta: Meta = { +const meta = { title: "Components/Sidebar/Actions", component: SidebarActions, parameters: { layout: "centered", }, tags: ["autodocs"], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; const mockChat = { id: "1", diff --git a/apps/nextjs/src/components/DialogControl/ContentOptions/ClearChatHistory.tsx b/apps/nextjs/src/components/DialogControl/ContentOptions/ClearChatHistory.tsx index 07e9ed1ca..bcdb28aa8 100644 --- a/apps/nextjs/src/components/DialogControl/ContentOptions/ClearChatHistory.tsx +++ b/apps/nextjs/src/components/DialogControl/ContentOptions/ClearChatHistory.tsx @@ -61,7 +61,9 @@ const ClearChatHistory = ({ $justifyContent="center" $gap="space-between-m" > - This will permanently delete all of your lesson history. + + This will permanently delete all of your lesson history. + - This will permanently delete this lesson. + This will permanently delete this lesson. {isLoading ? ( diff --git a/apps/nextjs/src/components/DialogControl/DialogContents.stories.tsx b/apps/nextjs/src/components/DialogControl/DialogContents.stories.tsx index a9b212545..b519c01e1 100644 --- a/apps/nextjs/src/components/DialogControl/DialogContents.stories.tsx +++ b/apps/nextjs/src/components/DialogControl/DialogContents.stories.tsx @@ -10,19 +10,21 @@ import { DialogContentDecorator } from "@/storybook/decorators/DialogContentDeco import { DemoProvider } from "../ContextProviders/Demo"; import DialogContents from "./DialogContents"; -const meta: Meta = { +const meta = { title: "Components/Dialogs/DialogContents", component: DialogContents, decorators: [DialogContentDecorator], -}; + args: { + lesson: {}, + chatId: "example-chat-id", + }, +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const ShareChat: Story = { args: { - lesson: {}, - chatId: "example-chat-id", isShared: false, }, parameters: { diff --git a/apps/nextjs/src/components/DialogControl/DialogContents.tsx b/apps/nextjs/src/components/DialogControl/DialogContents.tsx index 7187dc4e6..ed297f230 100644 --- a/apps/nextjs/src/components/DialogControl/DialogContents.tsx +++ b/apps/nextjs/src/components/DialogControl/DialogContents.tsx @@ -1,5 +1,8 @@ +import { useState } from "react"; + import type { LooseLessonPlan } from "@oakai/aila/src/protocol/schema"; import { + OakModal, OakModalCenter, OakModalCenterBody, type OakIconName, @@ -8,6 +11,7 @@ import type { Message } from "ai"; import styled from "styled-components"; import type { DialogTypes } from "../AppComponents/Chat/Chat/types"; +import { ChatHistory } from "../AppComponents/Chat/chat-history"; import { useDialog } from "../AppComponents/DialogContext"; import ClearChatHistory from "./ContentOptions/ClearChatHistory"; import ClearSingleChatFromChatHistory from "./ContentOptions/ClearSingleChatFromChatHistory"; diff --git a/apps/nextjs/src/components/ExportsDialogs/useExportAllLessonAssets.ts b/apps/nextjs/src/components/ExportsDialogs/useExportAllLessonAssets.ts index 7005f37b1..9a0560955 100644 --- a/apps/nextjs/src/components/ExportsDialogs/useExportAllLessonAssets.ts +++ b/apps/nextjs/src/components/ExportsDialogs/useExportAllLessonAssets.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { LessonDeepPartial } from "@oakai/exports/browser"; import { exportSlidesFullLessonSchema } from "@oakai/exports/browser"; -import type { LessonSlidesInputData } from "@oakai/exports/src/schema/input.schema"; +import type { LessonInputData } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; import { useDebounce } from "@uidotdev/usehooks"; @@ -29,8 +29,7 @@ export function useExportAllLessonAssets({ const query = trpc.exports.generateAllAssetExports.useMutation(); const [parseResult, setParseResult] = useState< - | { data?: LessonSlidesInputData; success: boolean; error?: ZodError } - | undefined + { data?: LessonInputData; success: boolean; error?: ZodError } | undefined >({ success: false }); const debouncedParseResult = useDebounce(parseResult, 500); diff --git a/apps/nextjs/src/components/Footer.stories.tsx b/apps/nextjs/src/components/Footer.stories.tsx index 1725794e8..688fc02b0 100644 --- a/apps/nextjs/src/components/Footer.stories.tsx +++ b/apps/nextjs/src/components/Footer.stories.tsx @@ -2,15 +2,15 @@ import type { Meta, StoryObj } from "@storybook/react"; import Footer from "./Footer"; -const meta: Meta = { +const meta = { title: "Components/Layout/Footer", component: Footer, tags: ["autodocs"], -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/Header.stories.tsx b/apps/nextjs/src/components/Header.stories.tsx index 6b914c5c9..300f608c9 100644 --- a/apps/nextjs/src/components/Header.stories.tsx +++ b/apps/nextjs/src/components/Header.stories.tsx @@ -1,8 +1,9 @@ import type { Meta, StoryObj } from "@storybook/react"; +import { fn } from "@storybook/test"; import Header from "./Header"; -const meta: Meta = { +const meta = { title: "Components/Layout/Header", component: Header, tags: ["autodocs"], @@ -14,11 +15,15 @@ const meta: Meta = { }, }, }, -}; + args: { + menuOpen: false, + setMenuOpen: fn(), + }, +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const SignedIn: Story = { args: {}, diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx index efc0e2527..bd2ba269b 100644 --- a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { AcceptTermsForm } from "./AcceptTermsForm"; -const meta: Meta = { +const meta = { title: "Pages/Onboarding/AcceptTermsForm", component: AcceptTermsForm, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx index b2e2fccf9..11d53aad7 100644 --- a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx +++ b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx @@ -4,16 +4,16 @@ import { chromaticParams } from "@/storybook/chromatic"; import { LegacyUpgradeNotice } from "./LegacyUpgradeNotice"; -const meta: Meta = { +const meta = { title: "Pages/Onboarding/LegacyUpgradeNotice", component: LegacyUpgradeNotice, parameters: { ...chromaticParams(["mobile", "desktop"]), }, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx b/apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx index 4065320f4..d3a282067 100644 --- a/apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx +++ b/apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx @@ -2,13 +2,13 @@ import type { Meta, StoryObj } from "@storybook/react"; import TermsContent from "./TermsContent"; -const meta: Meta = { +const meta = { title: "Components/Onboarding/TermsContent", component: TermsContent, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: {}, diff --git a/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx b/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx index 63cda9985..567147602 100644 --- a/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx +++ b/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx @@ -2,14 +2,17 @@ import type { Meta, StoryObj } from "@storybook/react"; import SignUpSignInLayout from "./SignUpSignInLayout"; -const meta: Meta = { +const meta = { title: "Components/Layout/SignUpSignInLayout", component: SignUpSignInLayout, -}; +} satisfies Meta; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { - args: {}, + args: { + children: null, + loaded: true, + }, }; diff --git a/packages/aila/src/features/persistence/adaptors/prisma/index.ts b/packages/aila/src/features/persistence/adaptors/prisma/index.ts index 28b2923e3..198930700 100644 --- a/packages/aila/src/features/persistence/adaptors/prisma/index.ts +++ b/packages/aila/src/features/persistence/adaptors/prisma/index.ts @@ -39,6 +39,7 @@ export class AilaPrismaPersistence extends AilaPersistence { const appSession = await this._prisma.appSession.findFirst({ where: { id, + deletedAt: null, }, }); diff --git a/packages/api/src/export/exportAdditionalMaterialsDoc.ts b/packages/api/src/export/exportAdditionalMaterialsDoc.ts index 826339ea4..4b532aebd 100644 --- a/packages/api/src/export/exportAdditionalMaterialsDoc.ts +++ b/packages/api/src/export/exportAdditionalMaterialsDoc.ts @@ -1,19 +1,13 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; -import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { exportAdditionalMaterials } from "@oakai/exports"; -import type { LessonSlidesInputData } from "@oakai/exports/src/schema/input.schema"; +import type { LessonInputData } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; -import type { - OutputSchema} from "../router/exports"; -import { - ailaGetExportBySnapshotId, - ailaSaveExport, - reportErrorResult, -} from "../router/exports"; +import type { OutputSchema } from "../router/exports"; +import { ailaSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; const log = aiLogger("exports"); @@ -22,7 +16,7 @@ export async function exportAdditionalMaterialsDoc({ ctx, }: { input: { - data: LessonSlidesInputData; + data: LessonInputData; chatId: string; messageId: string; }; @@ -31,50 +25,23 @@ export async function exportAdditionalMaterialsDoc({ prisma: PrismaClientWithAccelerate; }; }) { - const user = await clerkClient.users.getUser(ctx.auth.userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = "ADDITIONAL_MATERIALS_DOCS"; - + const userEmail = await getUserEmail(ctx); if (!userEmail) { return { error: new Error("User email not found"), message: "User email not found", }; } + const exportType = "ADDITIONAL_MATERIALS_DOCS"; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ - userId: ctx.auth.userId, - chatId: input.chatId, - messageId: input.messageId, - snapshot: input.data, - trigger: "EXPORT_BY_USER", - }); - - Sentry.addBreadcrumb({ - category: "exportAdditionalMaterialsDoc", - message: "Got or saved snapshot", - data: { lessonSnapshot }, - }); - - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId: lessonSnapshot.id, + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, exportType, }); - Sentry.addBreadcrumb({ - category: "exportAdditionalMaterialsDoc", - message: "Got export data", - data: { exportData }, - }); - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } const result = await exportAdditionalMaterials({ diff --git a/packages/api/src/export/exportHelpers.ts b/packages/api/src/export/exportHelpers.ts new file mode 100644 index 000000000..e110de9ab --- /dev/null +++ b/packages/api/src/export/exportHelpers.ts @@ -0,0 +1,132 @@ +import type { SignedInAuthObject } from "@clerk/backend/internal"; +import { clerkClient } from "@clerk/nextjs/server"; +import { LessonSnapshots } from "@oakai/core"; +import type { Snapshot } from "@oakai/core/src/models/lessonSnapshots"; +import type { + LessonExportType, + LessonSnapshot, + PrismaClientWithAccelerate, +} from "@oakai/db"; +import * as Sentry from "@sentry/nextjs"; + +import { + ailaGetExportBySnapshotId, + type OutputSchema, +} from "../router/exports"; + +export const getUserEmail = async (ctx: { + auth: SignedInAuthObject; + prisma: PrismaClientWithAccelerate; +}) => { + const user = await clerkClient.users.getUser(ctx.auth.userId); + const userEmail = user?.emailAddresses[0]?.emailAddress; + + return userEmail; +}; + +const categoryMap: Record = { + LESSON_SLIDES_SLIDES: "exportLessonSlidesDoc", + LESSON_PLAN_DOC: "exportLessonPlanDoc", + WORKSHEET_SLIDES: "exportWorksheetDocs", + ADDITIONAL_MATERIALS_DOCS: "exportAdditionalMaterialsDoc", + EXIT_QUIZ_DOC: "exportQuizDoc", + STARTER_QUIZ_DOC: "exportQuizDoc", +}; + +export const getLessonSnapshot = async ({ + input, + ctx, + exportType, +}: { + input: { + data: T; + chatId: string; + messageId: string; + }; + ctx: { + auth: SignedInAuthObject; + prisma: PrismaClientWithAccelerate; + }; + exportType: LessonExportType; +}): Promise => { + const lessonSnapshots = new LessonSnapshots(ctx.prisma); + + const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ + userId: ctx.auth.userId, + chatId: input.chatId, + messageId: input.messageId, + snapshot: input.data as Snapshot, + trigger: "EXPORT_BY_USER", + }); + + const category = categoryMap[exportType] ?? "exportLessonDoc"; + + Sentry.addBreadcrumb({ + category, + message: "Got or saved snapshot", + data: { lessonSnapshot }, + }); + + return lessonSnapshot; +}; + +export const getExportData = async ({ + prisma, + snapshotId, + exportType, +}: { + prisma: PrismaClientWithAccelerate; + snapshotId: string; + exportType: LessonExportType; +}) => { + const exportData = await ailaGetExportBySnapshotId({ + prisma, + snapshotId, + exportType, + }); + const category = categoryMap[exportType] ?? "exportLessonDoc"; + Sentry.addBreadcrumb({ + category, + message: "Got export data", + data: { exportData }, + }); + + if (exportData) { + const output: OutputSchema = { + link: exportData.gdriveFileUrl, + canViewSourceDoc: exportData.userCanViewGdriveFile, + }; + return output; + } +}; + +export const getExistingExportData = async ({ + input, + ctx, + exportType, +}: { + input: { + data: T; + chatId: string; + messageId: string; + }; + ctx: { + auth: SignedInAuthObject; + prisma: PrismaClientWithAccelerate; + }; + exportType: LessonExportType; +}) => { + const lessonSnapshot = await getLessonSnapshot({ + ctx, + input, + exportType, + }); + + const exportData = await getExportData({ + prisma: ctx.prisma, + snapshotId: lessonSnapshot.id, + exportType, + }); + + return { exportData, lessonSnapshot }; +}; diff --git a/packages/api/src/export/exportLessonPlan.ts b/packages/api/src/export/exportLessonPlan.ts index 705b197ae..941fb9470 100644 --- a/packages/api/src/export/exportLessonPlan.ts +++ b/packages/api/src/export/exportLessonPlan.ts @@ -1,19 +1,13 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; -import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { exportDocLessonPlan } from "@oakai/exports"; -import type { LessonSlidesInputData } from "@oakai/exports/src/schema/input.schema"; +import type { LessonInputData } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; -import type { - OutputSchema} from "../router/exports"; -import { - ailaGetExportBySnapshotId, - ailaSaveExport, - reportErrorResult, -} from "../router/exports"; +import type { OutputSchema } from "../router/exports"; +import { ailaSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; const log = aiLogger("exports"); @@ -22,7 +16,7 @@ export async function exportLessonPlan({ ctx, }: { input: { - data: LessonSlidesInputData; + data: LessonInputData; chatId: string; messageId: string; }; @@ -31,10 +25,7 @@ export async function exportLessonPlan({ prisma: PrismaClientWithAccelerate; }; }) { - const user = await clerkClient.users.getUser(ctx.auth.userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = "LESSON_PLAN_DOC"; - + const userEmail = await getUserEmail(ctx); if (!userEmail) { return { error: new Error("User email not found"), @@ -42,39 +33,16 @@ export async function exportLessonPlan({ }; } - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ - userId: ctx.auth.userId, - chatId: input.chatId, - messageId: input.messageId, - snapshot: input.data, - trigger: "EXPORT_BY_USER", - }); - - Sentry.addBreadcrumb({ - category: "exportLessonPlanDoc", - message: "Got or saved snapshot", - data: { lessonSnapshot }, - }); + const exportType = "LESSON_PLAN_DOC"; - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId: lessonSnapshot.id, + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, exportType, }); - Sentry.addBreadcrumb({ - category: "exportLessonPlanDoc", - message: "Got export data", - data: { exportData }, - }); - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } /** diff --git a/packages/api/src/export/exportLessonSlides.ts b/packages/api/src/export/exportLessonSlides.ts index c941915e3..0da0d6f99 100644 --- a/packages/api/src/export/exportLessonSlides.ts +++ b/packages/api/src/export/exportLessonSlides.ts @@ -1,19 +1,13 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; -import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { exportSlidesFullLesson } from "@oakai/exports"; import type { LessonSlidesInputData } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; -import type { - OutputSchema} from "../router/exports"; -import { - ailaGetExportBySnapshotId, - ailaSaveExport, - reportErrorResult, -} from "../router/exports"; +import type { OutputSchema } from "../router/exports"; +import { ailaSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; const log = aiLogger("exports"); @@ -31,50 +25,23 @@ export async function exportLessonSlides({ prisma: PrismaClientWithAccelerate; }; }) { - const user = await clerkClient.users.getUser(ctx.auth.userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = "LESSON_SLIDES_SLIDES"; - + const userEmail = await getUserEmail(ctx); if (!userEmail) { return { error: new Error("User email not found"), message: "User email not found", }; } + const exportType = "LESSON_SLIDES_SLIDES"; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ - userId: ctx.auth.userId, - chatId: input.chatId, - messageId: input.messageId, - snapshot: input.data, - trigger: "EXPORT_BY_USER", - }); - - Sentry.addBreadcrumb({ - category: "exportLessonSlides", - message: "Got or saved snapshot", - data: { lessonSnapshot }, - }); - - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId: lessonSnapshot.id, + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, exportType, }); - Sentry.addBreadcrumb({ - category: "exportLessonSlides", - message: "Got export data", - data: { exportData }, - }); - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } /** diff --git a/packages/api/src/export/exportQuizDesignerSlides.ts b/packages/api/src/export/exportQuizDesignerSlides.ts new file mode 100644 index 000000000..48a04a968 --- /dev/null +++ b/packages/api/src/export/exportQuizDesignerSlides.ts @@ -0,0 +1,96 @@ +import type { SignedInAuthObject } from "@clerk/backend/internal"; +import type { PrismaClientWithAccelerate } from "@oakai/db"; +import { exportQuizDesignerSlides } from "@oakai/exports"; +import type { ExportableQuizAppState } from "@oakai/exports/src/schema/input.schema"; +import { aiLogger } from "@oakai/logger"; +import * as Sentry from "@sentry/nextjs"; + +import type { OutputSchema } from "../router/exports"; +import { qdSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; + +const log = aiLogger("exports"); + +export async function exportQuizDesignerSlidesWrapper({ + input, + ctx, +}: { + input: { + data: ExportableQuizAppState; + chatId: string; + messageId: string; + }; + ctx: { + auth: SignedInAuthObject; + prisma: PrismaClientWithAccelerate; + }; +}) { + const userEmail = await getUserEmail(ctx); + if (!userEmail) { + return { + error: new Error("User email not found"), + message: "User email not found", + }; + } + const exportType = "LESSON_SLIDES_SLIDES"; + + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, + exportType, + }); + + if (exportData) { + return exportData; + } + + /** + * User hasn't yet exported the lesson in this state, so we'll do it now + * and store the result in the database + */ + + const result = await exportQuizDesignerSlides({ + snapshotId: "lessonSnapshot.id", + userEmail, + onStateChange: (state) => { + log.info(state); + + Sentry.addBreadcrumb({ + category: "exportWorksheetSlides", + message: "Export state change", + data: state, + }); + }, + quiz: input.data, + }); + + Sentry.addBreadcrumb({ + category: "exportLessonSlides", + message: "Got export result", + data: { result }, + }); + + if ("error" in result) { + reportErrorResult(result, input); + return { + error: result.error, + message: "Failed to export lesson", + }; + } + + const { data } = result; + + await qdSaveExport({ + auth: ctx.auth, + prisma: ctx.prisma, + snapshotId: lessonSnapshot.id, + exportType, + data: result.data, + }); + + const output: OutputSchema = { + link: data.fileUrl, + canViewSourceDoc: data.userCanViewGdriveFile, + }; + return output; +} diff --git a/packages/api/src/export/exportQuizDoc.ts b/packages/api/src/export/exportQuizDoc.ts index e7472ac7b..178194fa0 100644 --- a/packages/api/src/export/exportQuizDoc.ts +++ b/packages/api/src/export/exportQuizDoc.ts @@ -1,6 +1,4 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; -import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { exportDocQuiz } from "@oakai/exports"; import type { QuizDocInputData } from "@oakai/exports/src/schema/input.schema"; @@ -9,11 +7,8 @@ import * as Sentry from "@sentry/nextjs"; import { z } from "zod"; import type { OutputSchema } from "../router/exports"; -import { - ailaGetExportBySnapshotId, - ailaSaveExport, - reportErrorResult, -} from "../router/exports"; +import { ailaSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; const log = aiLogger("exports"); @@ -36,11 +31,7 @@ export async function exportQuizDoc({ data: QuizDocInputData; }; }) { - const user = await clerkClient.users.getUser(ctx.auth.userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = - input.data.quizType === "exit" ? "EXIT_QUIZ_DOC" : "STARTER_QUIZ_DOC"; - + const userEmail = await getUserEmail(ctx); if (!userEmail) { return { error: new Error("User email not found"), @@ -48,39 +39,17 @@ export async function exportQuizDoc({ }; } - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ - userId: ctx.auth.userId, - chatId: input.chatId, - messageId: input.messageId, - snapshot: input.lessonSnapshot, - trigger: "EXPORT_BY_USER", - }); - - Sentry.addBreadcrumb({ - category: "exportQuizDoc", - message: "Got or saved snapshot", - data: { lessonSnapshot }, - }); + const exportType = + input.data.quizType === "exit" ? "EXIT_QUIZ_DOC" : "STARTER_QUIZ_DOC"; - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId: lessonSnapshot.id, + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, exportType, }); - Sentry.addBreadcrumb({ - category: "exportQuizDoc", - message: "Got export data", - data: { exportData }, - }); - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } /** diff --git a/packages/api/src/export/exportWorksheets.ts b/packages/api/src/export/exportWorksheets.ts index 45e9785fa..4ead33d65 100644 --- a/packages/api/src/export/exportWorksheets.ts +++ b/packages/api/src/export/exportWorksheets.ts @@ -1,19 +1,13 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; -import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { exportDocsWorksheet } from "@oakai/exports"; import type { WorksheetSlidesInputData } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; -import type { - OutputSchema} from "../router/exports"; -import { - ailaGetExportBySnapshotId, - ailaSaveExport, - reportErrorResult, -} from "../router/exports"; +import type { OutputSchema } from "../router/exports"; +import { ailaSaveExport, reportErrorResult } from "../router/exports"; +import { getExistingExportData, getUserEmail } from "./exportHelpers"; const log = aiLogger("exports"); @@ -31,9 +25,7 @@ export async function exportWorksheets({ prisma: PrismaClientWithAccelerate; }; }) { - const user = await clerkClient.users.getUser(ctx.auth.userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = "WORKSHEET_SLIDES"; + const userEmail = await getUserEmail(ctx); if (!userEmail) { return { @@ -41,40 +33,16 @@ export async function exportWorksheets({ message: "User email not found", }; } + const exportType = "WORKSHEET_SLIDES"; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const lessonSnapshot = await lessonSnapshots.getOrSaveSnapshot({ - userId: ctx.auth.userId, - chatId: input.chatId, - messageId: input.messageId, - snapshot: input.data, - trigger: "EXPORT_BY_USER", - }); - - Sentry.addBreadcrumb({ - category: "exportWorksheetDocs", - message: "Got or saved snapshot", - data: { lessonSnapshot }, - }); - - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId: lessonSnapshot.id, + const { exportData, lessonSnapshot } = await getExistingExportData({ + ctx, + input, exportType, }); - Sentry.addBreadcrumb({ - category: "exportWorksheetDocs", - message: "Got export data", - data: { exportData }, - }); - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } /** diff --git a/packages/api/src/router/appSessions.ts b/packages/api/src/router/appSessions.ts index 7375e1420..29f9042a6 100644 --- a/packages/api/src/router/appSessions.ts +++ b/packages/api/src/router/appSessions.ts @@ -79,6 +79,7 @@ export async function getChat(id: string, prisma: PrismaClientWithAccelerate) { const chatRecord = await prisma.appSession.findUnique({ where: { id: id, + deletedAt: null, }, }); if (!chatRecord) { @@ -205,6 +206,7 @@ export const appSessionsRouter = router({ "app_sessions" WHERE "user_id" = ${userId} AND "app_id" = 'lesson-planner' + AND "deleted_at" IS NULL ORDER BY "updated_at" DESC `; @@ -245,22 +247,27 @@ export const appSessionsRouter = router({ const { userId } = ctx.auth; const { id } = input; - await ctx.prisma.appSession.deleteMany({ + await ctx.prisma.appSession.update({ where: { id, appId: "lesson-planner", userId, }, + data: { + deletedAt: new Date(), + }, }); }), deleteAllChats: protectedProcedure.mutation(async ({ ctx }) => { const { userId } = ctx.auth; - - await ctx.prisma.appSession.deleteMany({ + await ctx.prisma.appSession.updateMany({ where: { userId, appId: "lesson-planner", }, + data: { + deletedAt: new Date(), + }, }); }), shareChat: protectedProcedure @@ -278,6 +285,7 @@ export const appSessionsRouter = router({ id, userId, appId: "lesson-planner", + deletedAt: null, }, }); diff --git a/packages/api/src/router/chats.ts b/packages/api/src/router/chats.ts index 403dc29fb..c63158839 100644 --- a/packages/api/src/router/chats.ts +++ b/packages/api/src/router/chats.ts @@ -45,7 +45,7 @@ export const chatsRouter = router({ const { id } = input; const session = await prisma?.appSession.findUnique({ - where: { id }, + where: { id, deletedAt: null }, }); if (!session) { @@ -72,6 +72,7 @@ export const chatsRouter = router({ where: { userId, appId: "lesson-planner", + deletedAt: null, }, }); diff --git a/packages/api/src/router/exports.ts b/packages/api/src/router/exports.ts index 24ef6a433..e7eda2240 100644 --- a/packages/api/src/router/exports.ts +++ b/packages/api/src/router/exports.ts @@ -1,6 +1,5 @@ import type { SignedInAuthObject } from "@clerk/backend/internal"; import { clerkClient } from "@clerk/nextjs/server"; -import { LessonSnapshots } from "@oakai/core"; import { sendEmail } from "@oakai/core/src/utils/sendEmail"; import type { PrismaClientWithAccelerate } from "@oakai/db"; import { @@ -8,7 +7,6 @@ import { exportDocQuizSchema, exportSlidesFullLessonSchema, exportDocsWorksheetSchema, - exportQuizDesignerSlides, } from "@oakai/exports"; import { exportableQuizAppStateSchema } from "@oakai/exports/src/schema/input.schema"; import { aiLogger } from "@oakai/logger"; @@ -18,8 +16,10 @@ import { kv } from "@vercel/kv"; import { z } from "zod"; import { exportAdditionalMaterialsDoc } from "../export/exportAdditionalMaterialsDoc"; +import { getExistingExportData } from "../export/exportHelpers"; import { exportLessonPlan } from "../export/exportLessonPlan"; import { exportLessonSlides } from "../export/exportLessonSlides"; +import { exportQuizDesignerSlidesWrapper } from "../export/exportQuizDesignerSlides"; import { exportQuizDoc } from "../export/exportQuizDoc"; import { exportWorksheets } from "../export/exportWorksheets"; import { protectedProcedure } from "../middleware/auth"; @@ -178,10 +178,6 @@ const outputSchema = z.union([ ]); export type OutputSchema = z.infer; -/** - * @todo refactor this router to for less duplication - */ - export const exportsRouter = router({ exportLessonSlides: protectedProcedure .input( @@ -214,28 +210,15 @@ export const exportsRouter = router({ ) .mutation(async ({ input, ctx }) => { try { - const userId = ctx.auth.userId; - const { chatId, data, messageId } = input; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot: data, - trigger: "EXPORT_BY_USER", - messageId, + const exportType = "LESSON_PLAN_DOC"; + const { exportData } = await getExistingExportData({ + ctx, + input, + exportType, }); - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, - exportType: "LESSON_PLAN_DOC", - }); if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } } catch (error) { log.error("Error checking if download exists:", error); @@ -256,28 +239,16 @@ export const exportsRouter = router({ ) .mutation(async ({ input, ctx }) => { try { - const userId = ctx.auth.userId; - const { chatId, data, messageId } = input; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot: data, - trigger: "EXPORT_BY_USER", - messageId, - }); // find the latest export for this snapshot - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, - exportType: "LESSON_SLIDES_SLIDES", + const exportType = "LESSON_SLIDES_SLIDES"; + const { exportData } = await getExistingExportData({ + ctx, + input, + exportType, }); + if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } } catch (error) { log.error("Error checking if download exists:", error); @@ -299,106 +270,9 @@ export const exportsRouter = router({ .output(outputSchema) .mutation(async ({ input, ctx }) => { try { - const { userId } = ctx.auth; - const user = await clerkClient.users.getUser(userId); - const userEmail = user?.emailAddresses[0]?.emailAddress; - const exportType = "LESSON_SLIDES_SLIDES"; - - if (!userEmail) { - return { - error: new Error("User email not found"), - message: "User email not found", - }; - } - - const { chatId, messageId, data: snapshot } = input; - - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot, - trigger: "EXPORT_BY_USER", - messageId, - }); - - Sentry.addBreadcrumb({ - category: "exportQuizDesignerSlides", - message: "Got or saved snapshot", - data: { snapshot }, - }); - - const exportData = await qdGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, - exportType, - }); - - Sentry.addBreadcrumb({ - category: "exportQuizDesignerSlides", - message: "Got export data", - data: { exportData }, - }); - - if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; - } - - /** - * User hasn't yet exported the lesson in this state, so we'll do it now - * and store the result in the database - */ - - const result = await exportQuizDesignerSlides({ - snapshotId: "lessonSnapshot.id", - userEmail, - onStateChange: (state) => { - log.info(state); - - Sentry.addBreadcrumb({ - category: "exportWorksheetSlides", - message: "Export state change", - data: state, - }); - }, - quiz: input.data, - }); - - Sentry.addBreadcrumb({ - category: "exportLessonSlides", - message: "Got export result", - data: { result }, - }); - - if ("error" in result) { - reportErrorResult(result, input); - return { - error: result.error, - message: "Failed to export lesson", - }; - } - - const { data } = result; - - await qdSaveExport({ - auth: ctx.auth, - prisma: ctx.prisma, - snapshotId, - exportType, - data: result.data, - }); - - const output: OutputSchema = { - link: data.fileUrl, - canViewSourceDoc: data.userCanViewGdriveFile, - }; - return output; + return await exportQuizDesignerSlidesWrapper({ input, ctx }); } catch (error) { - const message = "Failed to export lesson"; + const message = "Failed to export quiz designer slides"; reportErrorResult({ error, message }, input); return { error, @@ -420,7 +294,7 @@ export const exportsRouter = router({ try { return await exportAdditionalMaterialsDoc({ input, ctx }); } catch (error) { - const message = "Failed to export lesson"; + const message = "Failed to export additional materials"; reportErrorResult({ error, message }, input); return { error, @@ -438,31 +312,20 @@ export const exportsRouter = router({ ) .mutation(async ({ input, ctx }) => { try { - const userId = ctx.auth.userId; - const { chatId, data: snapshot, messageId } = input; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot, - trigger: "EXPORT_BY_USER", - messageId, - }); - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, - exportType: "ADDITIONAL_MATERIALS_DOCS", + const exportType = "ADDITIONAL_MATERIALS_DOCS"; + const { exportData } = await getExistingExportData({ + ctx, + input, + exportType, }); + if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } } catch (error) { log.error("Error checking if download exists:", error); - const message = "Failed to check if download exists"; + const message = + "Failed to check if additional materials download exists"; return { error, message, @@ -500,27 +363,15 @@ export const exportsRouter = router({ ) .mutation(async ({ input, ctx }) => { try { - const userId = ctx.auth.userId; - const { chatId, data, messageId } = input; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot: data, - trigger: "EXPORT_BY_USER", - messageId, - }); - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, - exportType: "WORKSHEET_SLIDES", + const exportType = "WORKSHEET_SLIDES"; + const { exportData } = await getExistingExportData({ + ctx, + input, + exportType, }); + if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } } catch (error) { log.error("Error checking if download exists:", error); @@ -564,30 +415,17 @@ export const exportsRouter = router({ ) .mutation(async ({ input, ctx }) => { try { - const userId = ctx.auth.userId; - const { chatId, data, lessonSnapshot, messageId } = input; - const lessonSnapshots = new LessonSnapshots(ctx.prisma); - const { id: snapshotId } = await lessonSnapshots.getOrSaveSnapshot({ - userId, - chatId, - snapshot: lessonSnapshot, - trigger: "EXPORT_BY_USER", - messageId, - }); const exportType = - data.quizType === "exit" ? "EXIT_QUIZ_DOC" : "STARTER_QUIZ_DOC"; + input.data.quizType === "exit" ? "EXIT_QUIZ_DOC" : "STARTER_QUIZ_DOC"; - const exportData = await ailaGetExportBySnapshotId({ - prisma: ctx.prisma, - snapshotId, + const { exportData } = await getExistingExportData({ + ctx, + input, exportType, }); + if (exportData) { - const output: OutputSchema = { - link: exportData.gdriveFileUrl, - canViewSourceDoc: exportData.userCanViewGdriveFile, - }; - return output; + return exportData; } } catch (error) { log.error("Error checking if download exists:", error); diff --git a/packages/core/src/models/apps.ts b/packages/core/src/models/apps.ts index 4c9b6563a..59ba0007d 100644 --- a/packages/core/src/models/apps.ts +++ b/packages/core/src/models/apps.ts @@ -46,6 +46,7 @@ export class Apps { where: { id: sessionId, userId: userId, + deletedAt: null, output: { not: Prisma.AnyNull, }, @@ -59,6 +60,7 @@ export class Apps { return this.prisma.appSession.findMany({ where: { userId: userId, + deletedAt: null, output: { not: Prisma.AnyNull, }, diff --git a/packages/core/src/models/lessonSnapshots.ts b/packages/core/src/models/lessonSnapshots.ts index 330344254..fc5f79074 100644 --- a/packages/core/src/models/lessonSnapshots.ts +++ b/packages/core/src/models/lessonSnapshots.ts @@ -105,7 +105,7 @@ export class LessonSnapshots { messageId: string; snapshot: Snapshot; trigger: LessonSnapshotTrigger; - }) { + }): Promise { /** * Prisma types complained when passing the JSON schema directly to the Prisma */ diff --git a/packages/core/src/types/export-template.ts b/packages/core/src/types/export-template.ts deleted file mode 100644 index 1f20062db..000000000 --- a/packages/core/src/types/export-template.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { z } from "zod"; - -export const proformaTemplateDataSchema = z.object({ - lesson_title: z.string(), - subject: z.string(), - key_stage: z.string(), - topic: z.string().optional(), - learning_outcome: z.string(), - learning_cycle_outcome_1: z.string(), - learning_cycle_outcome_2: z.string().optional(), - learning_cycle_outcome_3: z.string().optional(), - prior_knowledge: z.array(z.string()), - misconception_1: z.string(), - misconception_2: z.string(), - misconception_3: z.string(), - keyword_1: z.string(), - keyword_2: z.string(), - keyword_3: z.string().optional(), - keyword_4: z.string().optional(), - keyword_5: z.string().optional(), - keyword_definition_1: z.string(), - keyword_definition_2: z.string(), - keyword_definition_3: z.string().optional(), - keyword_definition_4: z.string().optional(), - keyword_definition_5: z.string().optional(), - key_learning_point_1: z.string(), - key_learning_point_2: z.string().optional(), - key_learning_point_3: z.string().optional(), - key_learning_point_4: z.string().optional(), - starter_quiz_question_1: z.string(), - starter_quiz_question_1_answer_1: z.string(), - starter_quiz_question_1_answer_2: z.string(), - starter_quiz_question_1_answer_3: z.string(), - starter_quiz_question_2: z.string(), - starter_quiz_question_2_answer_1: z.string(), - starter_quiz_question_2_answer_2: z.string(), - starter_quiz_question_2_answer_3: z.string(), - starter_quiz_question_3: z.string(), - starter_quiz_question_3_answer_1: z.string(), - starter_quiz_question_3_answer_2: z.string(), - starter_quiz_question_3_answer_3: z.string(), - starter_quiz_question_4: z.string(), - starter_quiz_question_4_answer_1: z.string(), - starter_quiz_question_4_answer_2: z.string(), - starter_quiz_question_4_answer_3: z.string(), - starter_quiz_question_5: z.string(), - starter_quiz_question_5_answer_1: z.string(), - starter_quiz_question_5_answer_2: z.string(), - starter_quiz_question_5_answer_3: z.string(), - starter_quiz_question_6: z.string(), - starter_quiz_question_6_answer_1: z.string(), - starter_quiz_question_6_answer_2: z.string(), - starter_quiz_question_6_answer_3: z.string(), - exit_quiz_question_1: z.string(), - exit_quiz_question_1_answer_1: z.string(), - exit_quiz_question_1_answer_2: z.string(), - exit_quiz_question_1_answer_3: z.string(), - exit_quiz_question_2: z.string(), - exit_quiz_question_2_answer_1: z.string(), - exit_quiz_question_2_answer_2: z.string(), - exit_quiz_question_2_answer_3: z.string(), - exit_quiz_question_3: z.string(), - exit_quiz_question_3_answer_1: z.string(), - exit_quiz_question_3_answer_2: z.string(), - exit_quiz_question_3_answer_3: z.string(), - exit_quiz_question_4: z.string(), - exit_quiz_question_4_answer_1: z.string(), - exit_quiz_question_4_answer_2: z.string(), - exit_quiz_question_4_answer_3: z.string(), - exit_quiz_question_5: z.string(), - exit_quiz_question_5_answer_1: z.string(), - exit_quiz_question_5_answer_2: z.string(), - exit_quiz_question_5_answer_3: z.string(), - exit_quiz_question_6: z.string(), - exit_quiz_question_6_answer_1: z.string(), - exit_quiz_question_6_answer_2: z.string(), - exit_quiz_question_6_answer_3: z.string(), - - learning_cycle_1_title: z.string(), - learning_cycle_2_title: z.string(), - learning_cycle_3_title: z.string(), - learning_cycle_1_text: z.string(), - cycle_1_spoken_explanation: z.string(), - cycle_1_check_for_understanding_question_1: z.string(), - cycle_1_check_for_understanding_question_1_answer_1: z.string(), - cycle_1_check_for_understanding_question_1_answer_2: z.string(), - cycle_1_check_for_understanding_question_1_answer_3: z.string(), - - cycle_1_check_for_understanding_question_2: z.string(), - cycle_1_check_for_understanding_question_2_answer_1: z.string(), - cycle_1_check_for_understanding_question_2_answer_2: z.string(), - cycle_1_check_for_understanding_question_2_answer_3: z.string(), - - cycle_1_practice: z.string(), - cycle_1_feedback: z.string(), - - cycle_2_spoken_explanation: z.string(), - cycle_2_check_for_understanding_question_1: z.string(), - cycle_2_check_for_understanding_question_1_answer_1: z.string(), - cycle_2_check_for_understanding_question_1_answer_2: z.string(), - cycle_2_check_for_understanding_question_1_answer_3: z.string(), - - cycle_2_check_for_understanding_question_2: z.string(), - cycle_2_check_for_understanding_question_2_answer_1: z.string(), - cycle_2_check_for_understanding_question_2_answer_2: z.string(), - cycle_2_check_for_understanding_question_2_answer_3: z.string(), - - cycle_2_practice: z.string(), - cycle_2_feedback: z.string(), - - cycle_3_spoken_explanation: z.string(), - cycle_3_check_for_understanding_question_1: z.string(), - cycle_3_check_for_understanding_question_1_answer_1: z.string(), - cycle_3_check_for_understanding_question_1_answer_2: z.string(), - cycle_3_check_for_understanding_question_1_answer_3: z.string(), - - cycle_3_check_for_understanding_question_2: z.string(), - cycle_3_check_for_understanding_question_2_answer_1: z.string(), - cycle_3_check_for_understanding_question_2_answer_2: z.string(), - cycle_3_check_for_understanding_question_2_answer_3: z.string(), - - cycle_3_practice: z.string(), - cycle_3_feedback: z.string(), -}); - -export type ProformaTemplateData = z.infer; - -export const slidesTemplateDataSchema = z.object({ - lesson_title: z.string(), - subject_title: z.string(), - key_stage_title: z.string().nullish(), - topic_title: z.string().nullish(), - learning_outcomes: z.string(), - keywords: z.array(z.string()).nullish(), - keyword_sentences: z.array(z.string()).nullish(), - learning_cycle_1_title: z.string(), - learning_cycle_2_title: z.string().nullish(), - learning_cycle_3_title: z.string().nullish(), - learning_cycle_1_text: z.string(), - learning_cycle_1_image_prompt: z.string(), - - learning_cycle_1_title_long: z.string(), - learning_cycle_2_title_long: z.string().nullish(), - learning_cycle_3_title_long: z.string().nullish(), - - learning_cycle_1_check_question_1_question: z.string(), - learning_cycle_1_question_1_check_answer_1: z.string(), - learning_cycle_1_question_1_check_answer_2: z.string(), - learning_cycle_1_question_1_check_answer_3: z.string(), - learning_cycle_1_check_question_2: z.string(), - learning_cycle_1_question_2_check_answer_1: z.string(), - learning_cycle_1_question_2_check_answer_2: z.string(), - learning_cycle_1_question_2_check_answer_3: z.string(), - - learning_cycle_1_practise: z.string().nullish(), - - learning_cycle_2_text: z.string().nullish(), - learning_cycle_2_image_prompt: z.string().nullish(), - learning_cycle_2_question_1_check_question: z.string().nullish(), - learning_cycle_2_question_1_check_answer_1: z.string().nullish(), - learning_cycle_2_question_1_check_answer_2: z.string().nullish(), - learning_cycle_2_question_1_check_answer_3: z.string().nullish(), - - learning_cycle_2_question_2_check_question: z.string().nullish(), - learning_cycle_2_question_2_check_answer_1: z.string().nullish(), - learning_cycle_2_question_2_check_answer_2: z.string().nullish(), - learning_cycle_2_question_2_check_answer_3: z.string().nullish(), - - learning_cycle_2_practise: z.string().nullish(), - - learning_cycle_3_text: z.string().nullish(), - learning_cycle_3_image_prompt: z.string().nullish(), - - learning_cycle_3_question_1_check_question: z.string().nullish(), - learning_cycle_3_question_1_check_answer_1: z.string().nullish(), - learning_cycle_3_question_1_check_answer_2: z.string().nullish(), - learning_cycle_3_question_1_check_answer_3: z.string().nullish(), - - learning_cycle_3_question_2_check_question: z.string().nullish(), - learning_cycle_3_question_2_check_answer_1: z.string().nullish(), - learning_cycle_3_question_2_check_answer_2: z.string().nullish(), - learning_cycle_3_question_2_check_answer_3: z.string().nullish(), - - learning_cycle_3_practise: z.string().nullish(), -}); - -export const WARN_IF_MISSING = [ - "learning_cycle_2_title", - "learning_cycle_2_text", - "learning_cycle_2_image_prompt", - "learning_cycle_2_question_1_check_question", - "learning_cycle_2_question_1_check_answer_1", - "learning_cycle_2_question_1_check_answer_2", - "learning_cycle_2_question_1_check_answer_3", - "learning_cycle_2_question_2_check_question", - "learning_cycle_2_question_2_check_answer_1", - "learning_cycle_2_question_2_check_answer_2", - "learning_cycle_2_question_2_check_answer_3", - "learning_cycle_2_practise", -]; - -export type SlidesTemplateData = z.infer; - -export const lessonQuizSchema = z.object({ - lesson_title: z.string(), - quiz_type: z.string(), - question_1: z.string().nullish(), - question_1_answer_a: z.string().nullish(), - question_1_answer_b: z.string().nullish(), - question_1_answer_c: z.string().nullish(), - question_2: z.string().nullish(), - question_2_answer_a: z.string().nullish(), - question_2_answer_b: z.string().nullish(), - question_2_answer_c: z.string().nullish(), - question_3: z.string().nullish(), - question_3_answer_a: z.string().nullish(), - question_3_answer_b: z.string().nullish(), - question_3_answer_c: z.string().nullish(), - question_4: z.string().nullish(), - question_4_answer_a: z.string().nullish(), - question_4_answer_b: z.string().nullish(), - question_4_answer_c: z.string().nullish(), - question_5: z.string().nullish(), - question_5_answer_a: z.string().nullish(), - question_5_answer_b: z.string().nullish(), - question_5_answer_c: z.string().nullish(), - question_6: z.string().nullish(), - question_6_answer_a: z.string().nullish(), - question_6_answer_b: z.string().nullish(), - question_6_answer_c: z.string().nullish(), -}); - -export type LessonQuiz = z.infer; diff --git a/packages/db/prisma/migrations/20241126134932_add_deleted_session_col/migration.sql b/packages/db/prisma/migrations/20241126134932_add_deleted_session_col/migration.sql new file mode 100644 index 000000000..b9e9d9f95 --- /dev/null +++ b/packages/db/prisma/migrations/20241126134932_add_deleted_session_col/migration.sql @@ -0,0 +1,3 @@ + +-- AlterTable +ALTER TABLE "public"."app_sessions" ADD COLUMN "deleted_at" TIMESTAMP(3); diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 4cabd577c..a6f727de3 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -344,14 +344,15 @@ model AppSession { id String @id @default(cuid()) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") - + deletedAt DateTime? @map("deleted_at") + app App @relation(fields: [appId], references: [id], onDelete: Cascade) appId String @map("app_id") userId String @map("user_id") // The user that requested this generation output Json? // The final output of the session - + userTweaks UserTweak[] reGenerations ReGeneration[] generationUserFlags GenerationUserFlag[] diff --git a/packages/db/schemas/lesson.ts b/packages/db/schemas/lesson.ts index 32d5e3843..e8d42fe98 100644 --- a/packages/db/schemas/lesson.ts +++ b/packages/db/schemas/lesson.ts @@ -38,26 +38,7 @@ export const ZLesson = z.object({ programmeOfStudyUnits: z.array( z.object({ id: z.number(), - programme: z.object({ - id: z.number(), - slug: z.string(), - title: z.string(), - subject: z.object({ - id: z.number(), - slug: z.string(), - title: z.string(), - }), - year: z.object({ - id: z.number(), - title: z.string(), - slug: z.string(), - keyStage: z.object({ - id: z.number(), - title: z.string(), - slug: z.string(), - }), - }), - }), + programme: Programme, }), ), therapyUnits: z.array(z.unknown()).nullish(), diff --git a/packages/exports/src/exportAdditionalMaterials.ts b/packages/exports/src/exportAdditionalMaterials.ts index 4f8d3fe32..5ce40ea36 100644 --- a/packages/exports/src/exportAdditionalMaterials.ts +++ b/packages/exports/src/exportAdditionalMaterials.ts @@ -4,7 +4,7 @@ import { prepLessonForAdditionalMaterialsDoc } from "./dataHelpers/prepLessonFor import { exportGeneric } from "./exportGeneric"; import { getDocsClient } from "./gSuite/docs/client"; import { populateDoc } from "./gSuite/docs/populateDoc"; -import type { LessonSlidesInputData } from "./schema/input.schema"; +import type { LessonInputData } from "./schema/input.schema"; import { getSlidesTemplateIdAdditionalMaterials as getDocsTemplateIdAdditionalMaterials } from "./templates"; import type { OutputData, Result, State } from "./types"; @@ -17,7 +17,7 @@ export const exportAdditionalMaterials = async ({ onStateChange, }: { snapshotId: string; - lesson: LessonSlidesInputData; + lesson: LessonInputData; userEmail: string; onStateChange: (state: State) => void; }): Promise> => { diff --git a/packages/exports/src/schema/input.schema.ts b/packages/exports/src/schema/input.schema.ts index 59870b427..19645e7f4 100644 --- a/packages/exports/src/schema/input.schema.ts +++ b/packages/exports/src/schema/input.schema.ts @@ -37,7 +37,7 @@ export const keywordSchema = z.object({ definition: z.string(), }); -export const lessonSlidesInputSchema = z.object({ +export const lessonInputSchema = z.object({ title: z.string(), subject: z.string(), keyStage: z.string().nullish(), @@ -58,6 +58,10 @@ export const lessonSlidesInputSchema = z.object({ _experimental_exitQuizMathsV0: quizSchema.nullish(), }); +export type LessonInputData = z.infer; + +export const lessonSlidesInputSchema = lessonInputSchema; + export type LessonSlidesInputData = z.infer; export const lessonPlanDocInputSchema = lessonSlidesInputSchema; diff --git a/sonar-project.properties b/sonar-project.properties index c082e59ce..5d3c422c8 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -18,9 +18,10 @@ sonar.exclusions=\ sonar.cpd.exclusions=\ **/*.test.ts, \ **/*.fixture.ts, \ + apps/nextjs/jest.config.js \ **/*.json \ **/*.stories.ts \ - **/*.stories.tsx \ + **/*.stories.tsx sonar.tests=. sonar.test.inclusions=**/*.test.ts