Repository: oaknational/oak-ai-lesson-assistant
Pull request #667: feat: additional materials - connect frontend to exports - AI-1082
Author: login: JBR90
Labels: name: released
Created: 2025-05-06T12:59:11Z | Merged: 2025-05-13T07:51:11Z

## Description

- Connects backend downloads to front end
- Download front end logic moved to store
- Updated loading and disabled states for buttons

## Issue(s)

Fixes #

## How to test

1. Go to /aila/resources
2. Click through with Glossary to create resource
3. Download Glossary
4. Adapt disabled during download
5. Download button disabled during download
6. Zip downloaded

## Checklist

- [ ] Manually tested across browsers / devices
- [ ] Considered impact on accessibility
- [ ] Does this PR update a package with a breaking change



Diff:
diff --git a/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts b/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts
index 6e9721fe7..44050c04c 100644
--- a/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts
+++ b/apps/nextjs/src/app/api/cron-jobs/expired-exports/route.ts
@@ -50,7 +50,7 @@ async function updateExpiredAtAndDelete(fileIds: string[]) {
 
       log.info(`Successfully updated expiredAt for file: ${id}`);
 
-      await googleDrive.files.delete({ fileId: id });
+      await googleDrive.files.delete({ fileId: id, supportsAllDrives: true });
       log.info(`Successfully deleted file: ${id}`);
     } catch (error) {
       log.error(`Error processing file with gdriveFileId: ${id}`, error);
@@ -87,6 +87,7 @@ async function fetchExpiredExports({
       q: query,
       fields: "files(id, name, modifiedTime, ownedByMe )",
       pageSize: 1000,
+      supportsAllDrives: true,
     });
 
     const files =
diff --git a/apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx b/apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
index 11e579531..90729aebf 100644
--- a/apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
+++ b/apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
@@ -6,6 +6,7 @@ import {
   readingAgeRefinement,
 } from "@oakai/additional-materials/src/documents/additionalMaterials/glossary/schema";
 import { camelCaseToSentenceCase } from "@oakai/core/src/utils/camelCaseConversion";
+import { aiLogger } from "@oakai/logger";
 
 import {
   OakFlex,
@@ -15,6 +16,7 @@ import {
   OakSecondaryButton,
   OakSpan,
 } from "@oaknational/oak-components";
+import * as Sentry from "@sentry/nextjs";
 
 import {
   useResourcesActions,
@@ -23,6 +25,7 @@ import {
 import {
   docTypeSelector,
   generationSelector,
+  isResourcesDownloadingSelector,
   isResourcesLoadingSelector,
 } from "@/stores/resourcesStore/selectors";
 import { trpc } from "@/utils/trpc";
@@ -32,16 +35,35 @@ import { Glossary } from "../../AdditionalMaterials/Glossary";
 import InlineButton from "../InlineButton";
 import ResourcesFooter from "../ResourcesFooter";
 
+const log = aiLogger("additional-materials");
+
 const StepThree = () => {
   const generation = useResourcesStore(generationSelector);
+
   const docType = useResourcesStore(docTypeSelector);
   const isResourcesLoading = useResourcesStore(isResourcesLoadingSelector);
   const { setStepNumber, refineMaterial } = useResourcesActions();
   const [isFooterAdaptOpen, setIsFooterAdaptOpen] = useState(false);
+  const { downloadMaterial, setIsResourceDownloading } = useResourcesActions();
+  const isDownloading = useResourcesStore(isResourcesDownloadingSelector);
 
   const fetchMaterial =
     trpc.additionalMaterials.generateAdditionalMaterial.useMutation();
 
+  const handleDownloadMaterial = async () => {
+    if (!generation || !docType) {
+      return;
+    }
+    try {
+      await downloadMaterial();
+    } catch (err) {
+      log.error("Download failed", err);
+      Sentry.captureException(err);
+    } finally {
+      setIsResourceDownloading(false);
+    }
+  };
+
   const getRefinementOptions = () => {
     if (docType === "additional-glossary") {
       return readingAgeRefinement;
@@ -126,15 +148,18 @@ const StepThree = () => {
                 onClick={() => {
                   setIsFooterAdaptOpen(true);
                 }}
-                disabled={refinementOptions.length === 0 || isResourcesLoading}
+                disabled={
+                  refinementOptions.length === 0 || !generation || isDownloading
+                }
               >
                 Adapt
               </OakSecondaryButton>
               <OakPrimaryButton
-                onClick={() => null}
+                onClick={() => void handleDownloadMaterial()}
                 iconName="download"
                 isTrailingIcon={true}
-                disabled={isResourcesLoading}
+                isLoading={isDownloading}
+                disabled={!generation}
               >
                 Download (.zip)
               </OakPrimaryButton>
diff --git a/apps/nextjs/src/stores/resourcesStore/actionFunctions/handleDownload.ts b/apps/nextjs/src/stores/resourcesStore/actionFunctions/handleDownload.ts
new file mode 100644
index 000000000..c73f663ad
--- /dev/null
+++ b/apps/nextjs/src/stores/resourcesStore/actionFunctions/handleDownload.ts
@@ -0,0 +1,46 @@
+import {
+  type AdditionalMaterialSchemas,
+  additionalMaterialTypeEnum,
+} from "@oakai/additional-materials/src/documents/additionalMaterials/configSchema";
+import type { GenerateAdditionalMaterialInput } from "@oakai/additional-materials/src/documents/additionalMaterials/configSchema";
+import { aiLogger } from "@oakai/logger";
+
+import * as Sentry from "@sentry/nextjs";
+import type { UseMutateAsyncFunction } from "@tanstack/react-query";
+import { z } from "zod";
+
+import type { ResourcesGetter, ResourcesSetter } from "../types";
+
+const log = aiLogger("additional-materials");
+
+export const handleDownload =
+  (set: ResourcesSetter, get: ResourcesGetter) => async () => {
+    // Clear any existing generation
+    set({
+      isDownloading: true,
+    });
+    log.info("Download started");
+    const docType = get().docType;
+    const response = await fetch("/api/additional-resources-download", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+      },
+      body: JSON.stringify({
+        documentType: docType,
+        resource: get().generation,
+        lessonTitle: get().pageData.lessonPlan.title,
+      }),
+    });
+
+    if (!response.ok) {
+      throw new Error("Failed to generate ZIP");
+    }
+    const blob = await response.blob();
+    const url = window.URL.createObjectURL(blob);
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = `${docType}.zip`;
+    link.click();
+    window.URL.revokeObjectURL(url);
+  };
diff --git a/apps/nextjs/src/stores/resourcesStore/index.ts b/apps/nextjs/src/stores/resourcesStore/index.ts
index ef9176824..87af0dab7 100644
--- a/apps/nextjs/src/stores/resourcesStore/index.ts
+++ b/apps/nextjs/src/stores/resourcesStore/index.ts
@@ -1,6 +1,7 @@
 import { create } from "zustand";
 
 import { logStoreUpdates } from "../zustandHelpers";
+import { handleDownload } from "./actionFunctions/handleDownload";
 import {
   handleResetFormState,
   handleSetActiveDropdown,
@@ -31,6 +32,7 @@ export const createResourcesStore = ({ id }: CreateResourcesStoreParams) => {
     stepNumber: 0,
     isLoadingLessonPlan: false,
     isResourcesLoading: false,
+    isDownloading: false,
     pageData: {
       lessonPlan: {
         title: "",
@@ -68,6 +70,8 @@ export const createResourcesStore = ({ id }: CreateResourcesStoreParams) => {
       setDocType: handleSetDocType(set, get),
       setIsLoadingLessonPlan: handleSetIsLoadingLessonPlan(set, get),
       setIsResourcesLoading: handleSetIsResourcesLoading(set, get),
+      setIsResourceDownloading: (isDownloading: boolean) =>
+        set({ isDownloading }),
       // Form state setters
       setSubject: handleSetSubject(set, get),
       setTitle: handleSetTitle(set, get),
@@ -79,6 +83,7 @@ export const createResourcesStore = ({ id }: CreateResourcesStoreParams) => {
       submitLessonPlan: handleSubmitLessonPlan(set, get),
       generateMaterial: handleGenerateMaterial(set, get),
       refineMaterial: handleRefineMaterial(set, get),
+      downloadMaterial: handleDownload(set, get),
     },
   }));
 
diff --git a/apps/nextjs/src/stores/resourcesStore/selectors.ts b/apps/nextjs/src/stores/resourcesStore/selectors.ts
index f94e28b3f..1b27a5cc7 100644
--- a/apps/nextjs/src/stores/resourcesStore/selectors.ts
+++ b/apps/nextjs/src/stores/resourcesStore/selectors.ts
@@ -31,6 +31,13 @@ export const docTypeSelector = (state: ResourcesState) => state.docType;
 export const isResourcesLoadingSelector = (state: ResourcesState) =>
   state.isResourcesLoading;
 
+/**
+ * Selector for the is resources loading in the resources workflow
+ * @example const isResourcesDownloading = useResourcesStore(isResourcesLoadingSelector);
+ */
+export const isResourcesDownloadingSelector = (state: ResourcesState) =>
+  state.isDownloading;
+
 // Form state selectors
 export const formStateSelector = (state: ResourcesState) => state.formState;
 export const subjectSelector = (state: ResourcesState) =>
diff --git a/apps/nextjs/src/stores/resourcesStore/types.ts b/apps/nextjs/src/stores/resourcesStore/types.ts
index ba63aa35a..17813ef3b 100644
--- a/apps/nextjs/src/stores/resourcesStore/types.ts
+++ b/apps/nextjs/src/stores/resourcesStore/types.ts
@@ -25,6 +25,7 @@ export type ResourcesState = {
   stepNumber: number;
   isLoadingLessonPlan: boolean;
   isResourcesLoading: boolean;
+  isDownloading: boolean;
   pageData: PageData;
   generation: AdditionalMaterialSchemas | null;
   docType: string | null;
@@ -44,6 +45,7 @@ export type ResourcesState = {
     setTitle: (title: string | null) => void;
     setYear: (year: string | null) => void;
     setActiveDropdown: (dropdown: string | null) => void;
+    setIsResourceDownloading: (isDownloading: boolean) => void;
 
     // Clear form state
     resetFormState: () => void;
@@ -58,6 +60,7 @@ export type ResourcesState = {
     refineMaterial: (
       params: RefineMaterialParams,
     ) => Promise<AdditionalMaterialSchemas>;
+    downloadMaterial: () => Promise<void>;
   };
 };
 
diff --git a/packages/additional-materials/src/documents/additionalMaterials/configSchema.ts b/packages/additional-materials/src/documents/additionalMaterials/configSchema.ts
index 3729a9be5..e2c77d371 100644
--- a/packages/additional-materials/src/documents/additionalMaterials/configSchema.ts
+++ b/packages/additional-materials/src/documents/additionalMaterials/configSchema.ts
@@ -1,6 +1,5 @@
 import { type ZodSchema, type ZodType, z } from "zod";
 
-import { partialLessonContextSchema } from "../partialLessonPlan/schema";
 import { glossaryContextSchema, glossarySchema } from ".//glossary/schema";
 import {
   buildComprehensionPrompt,
diff --git a/packages/additional-materials/src/documents/additionalMaterials/glossary/buildGlossaryPrompt.ts b/packages/additional-materials/src/documents/additionalMaterials/glossary/buildGlossaryPrompt.ts
index d61da33ec..b79156354 100644
--- a/packages/additional-materials/src/documents/additionalMaterials/glossary/buildGlossaryPrompt.ts
+++ b/packages/additional-materials/src/documents/additionalMaterials/glossary/buildGlossaryPrompt.ts
@@ -7,7 +7,6 @@ import {
 
 export const buildGlossaryPrompt = (
   context: ContextByMaterialType["additional-glossary"],
-  action: Action,
 ) => {
   const { lessonPlan } = context;
 
diff --git a/packages/additional-materials/src/moderation/moderationPrompt.ts b/packages/additional-materials/src/moderation/moderationPrompt.ts
index a0879e093..1984c6a37 100644
--- a/packages/additional-materials/src/moderation/moderationPrompt.ts
+++ b/packages/additional-materials/src/moderation/moderationPrompt.ts
@@ -253,7 +253,7 @@ const moderationCategories = [
 export const moderationPrompt = `
 OBJECTIVE:
 
-You are a content moderation supervisor examining a additional material resource document which has been generated by a user through interactions with Oak National Academy's additional material resourcening assistant. Your job is to assess and moderate the additional material resource across a variety of categories.
+You are a content moderation supervisor examining a additional material resource document which has been generated by a user through interactions with Oak National Academy's additional material assistant. Your job is to assess and moderate the additional material resource across a variety of categories.
 
 For each category group, you must determine the degree to which the additional material resource is compliant with the following guidelines, providing a single rating on a 1-5 Likert scale that encompasses the following categories.
 
diff --git a/packages/exports/src/exportAdditionalResourceDoc.ts b/packages/exports/src/exportAdditionalResourceDoc.ts
index 2cfccdc4e..349e71a36 100644
--- a/packages/exports/src/exportAdditionalResourceDoc.ts
+++ b/packages/exports/src/exportAdditionalResourceDoc.ts
@@ -48,6 +48,7 @@ export const exportAdditionalResourceDoc = async <InputData, TemplateData>({
       },
       userEmail,
       onStateChange,
+      folderId: process.env.GOOGLE_DRIVE_OUTPUT_FOLDER_ID_ADDITIONAL_RESOURCES,
     });
     if ("error" in result) {
       onStateChange({ status: "error", error: result.error });
diff --git a/packages/exports/src/exportGeneric.ts b/packages/exports/src/exportGeneric.ts
index 91df72e1e..b947d4143 100644
--- a/packages/exports/src/exportGeneric.ts
+++ b/packages/exports/src/exportGeneric.ts
@@ -13,6 +13,7 @@ export const exportGeneric = async <InputData, TemplateData>({
   populateTemplate,
   userEmail,
   onStateChange,
+  folderId = process.env.GOOGLE_DRIVE_OUTPUT_FOLDER_ID,
 }: {
   newFileName: string;
   data: InputData;
@@ -25,7 +26,11 @@ export const exportGeneric = async <InputData, TemplateData>({
   }) => Promise<Result<{ missingData: string[] }>>;
   userEmail: string;
   onStateChange: (state: State<OutputData>) => void;
+  folderId?: string;
 }): Promise<Result<OutputData>> => {
+  if (!folderId) {
+    throw new Error("GOOGLE_DRIVE_OUTPUT_FOLDER_ID is not set");
+  }
   try {
     let userCanViewGdriveFile = false;
     onStateChange({ status: "loading", message: "Copying template..." });
@@ -33,6 +38,7 @@ export const exportGeneric = async <InputData, TemplateData>({
       drive: googleDrive,
       templateId,
       newFileName,
+      folderId,
     });
     if ("error" in copyResult) {
       onStateChange({ status: "error", error: copyResult.error });
diff --git a/packages/exports/src/gSuite/drive/addReader.ts b/packages/exports/src/gSuite/drive/addReader.ts
index 053ee2742..3966ef146 100644
--- a/packages/exports/src/gSuite/drive/addReader.ts
+++ b/packages/exports/src/gSuite/drive/addReader.ts
@@ -17,6 +17,7 @@ export async function addReader({
   try {
     await drive.permissions.create({
       fileId,
+      supportsAllDrives: true,
       sendNotificationEmail: false,
       requestBody: {
         role: "reader",
diff --git a/packages/exports/src/gSuite/drive/copyTemplate.ts b/packages/exports/src/gSuite/drive/copyTemplate.ts
index 3cd567040..4e7d3606e 100644
--- a/packages/exports/src/gSuite/drive/copyTemplate.ts
+++ b/packages/exports/src/gSuite/drive/copyTemplate.ts
@@ -2,13 +2,6 @@ import type { drive_v3 } from "@googleapis/drive";
 
 import type { Result } from "../../types";
 
-const folderId = process.env.GOOGLE_DRIVE_OUTPUT_FOLDER_ID;
-
-if (!folderId) {
-  // Checking at build time for extra safety
-  throw new Error("GOOGLE_DRIVE_OUTPUT_FOLDER_ID is not set");
-}
-
 /**
  * @description Copies the template presentation to a new file.
  * @returns The new presentation or document ID
@@ -17,10 +10,12 @@ export async function copyTemplate({
   drive,
   templateId,
   newFileName,
+  folderId = process.env.GOOGLE_DRIVE_OUTPUT_FOLDER_ID,
 }: {
   drive: drive_v3.Drive;
   templateId: string;
   newFileName: string;
+  folderId?: string;
 }): Promise<Result<{ fileCopyId: string }>> {
   try {
     if (!folderId) {
@@ -30,6 +25,7 @@ export async function copyTemplate({
 
     const response = await drive.files.copy({
       fileId: templateId,
+      supportsAllDrives: true,
       requestBody: {
         name: newFileName,
         parents: [folderId],
diff --git a/packages/exports/src/gSuite/drive/getLink.ts b/packages/exports/src/gSuite/drive/getLink.ts
index 3612a8594..ae5b31ff4 100644
--- a/packages/exports/src/gSuite/drive/getLink.ts
+++ b/packages/exports/src/gSuite/drive/getLink.ts
@@ -16,6 +16,7 @@ export async function getLink({
     const response = await drive.files.get({
       fileId,
       fields: "webViewLink",
+      supportsAllDrives: true,
     });
 
     const url = response.data.webViewLink;
diff --git a/packages/exports/src/schema/additionalResourceBlockSchema.schema.ts b/packages/exports/src/schema/additionalResourceBlockSchema.schema.ts
index c7be86e0a..02efe800e 100644
--- a/packages/exports/src/schema/additionalResourceBlockSchema.schema.ts
+++ b/packages/exports/src/schema/additionalResourceBlockSchema.schema.ts
@@ -12,11 +12,6 @@ const labelValue = z.object({
   value: z.string(),
 });
 
-const paragraph = z.object({
-  type: z.literal("paragraph"),
-  text: z.string(),
-});
-
 const labelValueArray = z.object({
   type: z.literal("labelValue"),
   items: z.array(labelValue),


Reviews:
user: login: mikeritson-oak
body: I've followed the testing instructions and had a few goes at this, across different browsers. The journey seems normal up until the last step, clicking the download button and the buttons are disabled and then return, but no file is downloaded. So everything seems normal, just no download passed to the browser or in the downloads location.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T12:29:32Z
user: login: mantagen
body: Code looks good, i've commented a naming request!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-08T12:31:25Z
user: login: mikeritson-oak
body: This has been reviewed as things currently stand, with the functionality very much in development and behind a feature flag.
All was as expected in testing. Users needed to be signed in and authorised to access the content. The user could produce a glossary and download the contents of the glossary.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-12T10:33:09Z

Review comments:
diff_hunk: @@ -32,16 +35,35 @@ import { Glossary } from "../../AdditionalMaterials/Glossary";
 import InlineButton from "../InlineButton";
 import ResourcesFooter from "../ResourcesFooter";
 
+const log = aiLogger("additional-materials");
+
 const StepThree = () => {
   const generation = useResourcesStore(generationSelector);
+
   const docType = useResourcesStore(docTypeSelector);
   const isResourcesLoading = useResourcesStore(isResourcesLoadingSelector);
   const { setStepNumber, refineMaterial } = useResourcesActions();
   const [isFooterAdaptOpen, setIsFooterAdaptOpen] = useState(false);
+  const { downloadMaterial, setIsResourceDownloading } = useResourcesActions();
+  const isDownloading = useResourcesStore(isResourcesDownloadingSelector);
 
   const fetchMaterial =
     trpc.additionalMaterials.generateAdditionalMaterial.useMutation();
 
+  const handleDownload = async () => {
path: apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
user: login: mantagen
body: Re. naming i wonder if naming is confusing here with the other `handleDownload` function introduced inthis PR

Could this be `safelyDownloadMaterial` (in the sense that it's wrapped in a try catch and checks generation/docType first?)

Or something like that?

Or
- attemptDownloadMaterial
- tryDownloadMaterial
- handleDownloadMaterial

What do you think?
created_at: 2025-05-08T12:29:49Z
author_association: COLLABORATOR
diff_hunk: @@ -79,6 +83,7 @@ export const createResourcesStore = ({ id }: CreateResourcesStoreParams) => {
       submitLessonPlan: handleSubmitLessonPlan(set, get),
       generateMaterial: handleGenerateMaterial(set, get),
       refineMaterial: handleRefineMaterial(set, get),
+      downloadMaterial: handleDownload(set, get),
path: apps/nextjs/src/stores/resourcesStore/index.ts
user: login: mantagen
body: realising that here for consistency it should be `handleDownloadMaterial`
created_at: 2025-05-08T12:30:38Z
author_association: COLLABORATOR
diff_hunk: @@ -32,16 +35,35 @@ import { Glossary } from "../../AdditionalMaterials/Glossary";
 import InlineButton from "../InlineButton";
 import ResourcesFooter from "../ResourcesFooter";
 
+const log = aiLogger("additional-materials");
+
 const StepThree = () => {
   const generation = useResourcesStore(generationSelector);
+
   const docType = useResourcesStore(docTypeSelector);
   const isResourcesLoading = useResourcesStore(isResourcesLoadingSelector);
   const { setStepNumber, refineMaterial } = useResourcesActions();
   const [isFooterAdaptOpen, setIsFooterAdaptOpen] = useState(false);
+  const { downloadMaterial, setIsResourceDownloading } = useResourcesActions();
+  const isDownloading = useResourcesStore(isResourcesDownloadingSelector);
 
   const fetchMaterial =
     trpc.additionalMaterials.generateAdditionalMaterial.useMutation();
 
+  const handleDownload = async () => {
path: apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
user: login: mantagen
body: Or `onDownloadZipClick` 
created_at: 2025-05-08T12:30:56Z
author_association: COLLABORATOR
diff_hunk: @@ -32,16 +35,35 @@ import { Glossary } from "../../AdditionalMaterials/Glossary";
 import InlineButton from "../InlineButton";
 import ResourcesFooter from "../ResourcesFooter";
 
+const log = aiLogger("additional-materials");
+
 const StepThree = () => {
   const generation = useResourcesStore(generationSelector);
+
   const docType = useResourcesStore(docTypeSelector);
   const isResourcesLoading = useResourcesStore(isResourcesLoadingSelector);
   const { setStepNumber, refineMaterial } = useResourcesActions();
   const [isFooterAdaptOpen, setIsFooterAdaptOpen] = useState(false);
+  const { downloadMaterial, setIsResourceDownloading } = useResourcesActions();
+  const isDownloading = useResourcesStore(isResourcesDownloadingSelector);
 
   const fetchMaterial =
     trpc.additionalMaterials.generateAdditionalMaterial.useMutation();
 
+  const handleDownload = async () => {
path: apps/nextjs/src/components/AppComponents/AdditionalMaterials/StepLayouts/StepThree.tsx
user: login: mantagen
body: `downloadClickHandler`
created_at: 2025-05-08T12:31:07Z
author_association: COLLABORATOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-06T12:59:16Z
body: [vc]: #TVRa1E3cR8neYkuobpxdQaQkcREadeaZP80zFOVZ81I=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJvYWstYWktbGVzc29uLWFzc2lzdGFudCIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9vYWstbmF0aW9uYWwtYWNhZGVteS9vYWstYWktbGVzc29uLWFzc2lzdGFudC81bWF0UjdERHZkNWU2bWc1eUt5eVFlQ2R6bVFUIiwicHJldmlld1VybCI6Im9hay1haS1sZXNzb24tYXNzaXN0YW50LWdpdC1mZWF0LWNvbm5lY3QtZnJvbnRlbmQtdG8tZXhwb3J0cy52ZXJjZWwtcHJldmlldy50aGVuYXRpb25hbC5hY2FkZW15IiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Im9hay1haS1sZXNzb24tYXNzaXN0YW50LWdpdC1mZWF0LWNvbm5lY3QtZnJvbnRlbmQtdG8tZXhwb3J0cy52ZXJjZWwtcHJldmlldy50aGVuYXRpb25hbC5hY2FkZW15In0sInJvb3REaXJlY3RvcnkiOiJhcHBzL25leHRqcyJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **oak-ai-lesson-assistant** | ✅ Ready ([Inspect](https://vercel.com/oak-national-academy/oak-ai-lesson-assistant/5matR7DDvd5e6mg5yKyyQeCdzmQT)) | [Visit Preview](https://oak-ai-lesson-assistant-git-feat-connect-frontend-to-exports.vercel-preview.thenational.academy) | 💬 [**Add feedback**](https://vercel.live/open-feedback/oak-ai-lesson-assistant-git-feat-connect-frontend-to-exports.vercel-preview.thenational.academy?via=pr-comment-feedback-link) | May 13, 2025 7:34am |


author_association: NONE
user: login: notion-workspace[bot]
created_at: 2025-05-06T13:01:18Z
body: <a href="https://www.notion.so/I-want-the-backend-exports-to-be-connected-to-front-end-1e426cc4e1b1809c97adc8afeaf1589d?pvs=8&n=github_linkback">💾 I want the backend exports to be connected to front end</a>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T13:13:29Z
body: ### Playwright test results

![passed](https://icongr.am/octicons/check-circle.svg?size=14&color=3fb950)  **17 passed**    
![skipped](https://icongr.am/octicons/skip.svg?size=14&color=0967d9)  **2 skipped**

#### Details

![report](https://icongr.am/octicons/package.svg?size=14&color=abb4bf)  [Open report ↗︎](https://aila-test-results-lpa4bt5kc.vercel-preview.thenational.academy)  
![stats](https://icongr.am/octicons/pulse.svg?size=14&color=abb4bf)  19 tests across 16 suites  
![duration](https://icongr.am/octicons/clock.svg?size=14&color=abb4bf)  2 minutes, 14 seconds  
![commit](https://icongr.am/octicons/git-pull-request.svg?size=14&color=abb4bf)  c28d943

<details ><summary><strong>Skipped tests</strong></summary>

No persona › tests/auth.test.ts › authenticate through Clerk UI
  No persona › tests/chat-performance.test.ts › Component renders during lesson chat › There are no unnecessary rerenders across left and right side of chat

</details>
<!-- thollander/actions-comment-pull-request "playwright-report" -->
author_association: NONE
user: login: JBR90
created_at: 2025-05-08T14:31:39Z
body: > I've followed the testing instructions and had a few goes at this, across different browsers. The journey seems normal up until the last step, clicking the download button and the buttons are disabled and then return, but no file is downloaded. So everything seems normal, just no download passed to the browser or in the downloads location.

Templates were updated to new drive folders, permissions needed to be updated and shared drived allowed in the code base. Working again now
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-13T07:28:25Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=oaknational_oak-ai-lesson-assistant&pullRequest=667) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [9 New issues](https://sonarcloud.io/project/issues?id=oaknational_oak-ai-lesson-assistant&pullRequest=667&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=oaknational_oak-ai-lesson-assistant&pullRequest=667&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=oaknational_oak-ai-lesson-assistant&pullRequest=667&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=oaknational_oak-ai-lesson-assistant&pullRequest=667&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=oaknational_oak-ai-lesson-assistant&pullRequest=667&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=oaknational_oak-ai-lesson-assistant&pullRequest=667)


author_association: NONE
user: login: oak-machine-user
created_at: 2025-05-20T12:07:34Z
body: :tada: This PR is included in version 1.37.0 :tada:

The release is available on [GitHub release](https://github.com/oaknational/oak-ai-lesson-assistant/releases/tag/v1.37.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:
author_association: COLLABORATOR