From abbff8545476f6c045331a63290fc8f57cf3f54e Mon Sep 17 00:00:00 2001 From: MG Date: Mon, 2 Dec 2024 15:24:33 +0000 Subject: [PATCH 01/11] fix: more flexible and efficient solution for images in docs (#411) --- apps/nextjs/next-env.d.ts | 2 +- .../experimentalPatches/mathsQuiz.fixture.ts | 11 ++ packages/api/src/export/exportQuizDoc.ts | 3 +- .../src/gSuite/docs/findMarkdownImages.ts | 70 ++++++++++ .../src/gSuite/docs/findPlaceholderIndex.ts | 42 ------ .../src/gSuite/docs/imageReplacements.ts | 62 +++++++++ .../exports/src/gSuite/docs/populateDoc.ts | 27 ++-- .../gSuite/docs/processImagesReplacements.ts | 120 ------------------ 8 files changed, 164 insertions(+), 173 deletions(-) create mode 100644 packages/exports/src/gSuite/docs/findMarkdownImages.ts delete mode 100644 packages/exports/src/gSuite/docs/findPlaceholderIndex.ts create mode 100644 packages/exports/src/gSuite/docs/imageReplacements.ts delete mode 100644 packages/exports/src/gSuite/docs/processImagesReplacements.ts diff --git a/apps/nextjs/next-env.d.ts b/apps/nextjs/next-env.d.ts index 4f11a03dc..40c3d6809 100644 --- a/apps/nextjs/next-env.d.ts +++ b/apps/nextjs/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/packages/aila/src/utils/experimentalPatches/mathsQuiz.fixture.ts b/packages/aila/src/utils/experimentalPatches/mathsQuiz.fixture.ts index 1ca49262c..281b27ed9 100644 --- a/packages/aila/src/utils/experimentalPatches/mathsQuiz.fixture.ts +++ b/packages/aila/src/utils/experimentalPatches/mathsQuiz.fixture.ts @@ -44,4 +44,15 @@ export const mathsQuizFixture: Quiz = [ "neither similar nor congruent.", ], }, + { + question: + "Here is an image ![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1706110974/fukcqeavzcevgjhmm1n4.png) and here is the same image ![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1706110974/fukcqeavzcevgjhmm1n4.png)", + answers: [ + "![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703169784/fg4uyx41rfnksbvav2nh.png)", + ], + distractors: [ + "![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703163380/pz6cn5k4wmowycnjq5am.png)", + "![image](http://oaknationalacademy-res.cloudinary.com/image/upload/v1703169784/mr09mrwkqdtk1dvjdoi0.png)", + ], + }, ]; diff --git a/packages/api/src/export/exportQuizDoc.ts b/packages/api/src/export/exportQuizDoc.ts index 640273a14..e7472ac7b 100644 --- a/packages/api/src/export/exportQuizDoc.ts +++ b/packages/api/src/export/exportQuizDoc.ts @@ -8,8 +8,7 @@ import { aiLogger } from "@oakai/logger"; import * as Sentry from "@sentry/nextjs"; import { z } from "zod"; -import type { - OutputSchema} from "../router/exports"; +import type { OutputSchema } from "../router/exports"; import { ailaGetExportBySnapshotId, ailaSaveExport, diff --git a/packages/exports/src/gSuite/docs/findMarkdownImages.ts b/packages/exports/src/gSuite/docs/findMarkdownImages.ts new file mode 100644 index 000000000..34b09977c --- /dev/null +++ b/packages/exports/src/gSuite/docs/findMarkdownImages.ts @@ -0,0 +1,70 @@ +import type { docs_v1 } from "@googleapis/docs"; + +export async function findMarkdownImages( + googleDocs: docs_v1.Docs, + documentId: string, +): Promise<{ url: string; altText: string; startIndex: number }[]> { + const doc = await googleDocs.documents.get({ documentId }); + const body = doc.data.body?.content; + + if (!body) return []; + + const markdownImageRegex = /!\[(.*?)\]\((.*?)\)/g; + const matches: { url: string; altText: string; startIndex: number }[] = []; + + function handleMatch( + match: RegExpExecArray, + textElement: docs_v1.Schema$ParagraphElement, + ): void { + const [, altText, url] = match; + const textElementStartIndex = textElement.startIndex; + const matchIndex = match.index; + if ( + typeof textElementStartIndex !== "number" || + typeof matchIndex !== "number" + ) { + return; + } + const startIndex = textElementStartIndex + matchIndex; + if (url && typeof altText === "string") { + matches.push({ url, altText, startIndex }); + } + } + + function processTextElements(elements: docs_v1.Schema$ParagraphElement[]) { + for (const textElement of elements) { + const textContent = textElement.textRun?.content ?? ""; + let match: RegExpExecArray | null; + while ((match = markdownImageRegex.exec(textContent)) !== null) { + handleMatch(match, textElement); + } + } + } + + function processTableCells(cells: docs_v1.Schema$TableCell[]) { + for (const cell of cells) { + for (const cellElement of cell.content ?? []) { + if (cellElement.paragraph?.elements) { + processTextElements(cellElement.paragraph.elements); + } + } + } + } + + function processBodyElements(elements: docs_v1.Schema$StructuralElement[]) { + for (const element of elements) { + if (element.table) { + for (const row of element.table.tableRows ?? []) { + processTableCells(row.tableCells ?? []); + } + } + if (element.paragraph?.elements) { + processTextElements(element.paragraph.elements); + } + } + } + + processBodyElements(body); + + return matches; +} diff --git a/packages/exports/src/gSuite/docs/findPlaceholderIndex.ts b/packages/exports/src/gSuite/docs/findPlaceholderIndex.ts deleted file mode 100644 index c31495607..000000000 --- a/packages/exports/src/gSuite/docs/findPlaceholderIndex.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { docs_v1 } from "@googleapis/docs"; - -/** - * Helper to find the index of a placeholder in the document. - */ -export async function findPlaceholderIndex( - googleDocs: docs_v1.Docs, - documentId: string, - placeholder: string, -): Promise { - const doc = await googleDocs.documents.get({ documentId }); - const body = doc.data.body?.content; - - if (!body) return null; - - for (const element of body) { - if (element.table) { - for (const row of element.table.tableRows || []) { - for (const cell of row.tableCells || []) { - for (const cellElement of cell.content || []) { - if (cellElement.paragraph?.elements) { - for (const textElement of cellElement.paragraph.elements) { - if (textElement.textRun?.content?.includes(placeholder)) { - return textElement.startIndex ?? null; - } - } - } - } - } - } - } - - if (element.paragraph?.elements) { - for (const textElement of element.paragraph.elements) { - if (textElement.textRun?.content?.includes(placeholder)) { - return textElement.startIndex ?? null; - } - } - } - } - return null; -} diff --git a/packages/exports/src/gSuite/docs/imageReplacements.ts b/packages/exports/src/gSuite/docs/imageReplacements.ts new file mode 100644 index 000000000..49cdae924 --- /dev/null +++ b/packages/exports/src/gSuite/docs/imageReplacements.ts @@ -0,0 +1,62 @@ +import type { docs_v1 } from "@googleapis/docs"; +import { aiLogger } from "@oakai/logger"; + +const log = aiLogger("exports"); + +export function imageReplacements( + markdownImages: { url: string; altText: string; startIndex: number }[], +): { requests: docs_v1.Schema$Request[] } { + if (markdownImages.length === 0) { + log.info("No Markdown images to process."); + return { requests: [] }; + } + + let cumulativeShift = 0; // Tracks the total index shift from previous operations + + const requests: docs_v1.Schema$Request[] = []; + + markdownImages.forEach((image) => { + // Construct the full Markdown reference + const markdownImageReference = `![${image.altText}](${image.url})`; + + const markdownLength = markdownImageReference.length; + + // Adjust the start and end index dynamically based on the cumulative shift + const adjustedStartIndex = image.startIndex + cumulativeShift; + const adjustedEndIndex = adjustedStartIndex + markdownLength; + + // Request to delete the exact range of the Markdown reference + requests.push({ + deleteContentRange: { + range: { + startIndex: adjustedStartIndex, + endIndex: adjustedEndIndex, + }, + }, + }); + + // Request to insert the inline image at the adjusted startIndex + requests.push({ + insertInlineImage: { + uri: image.url, + location: { + index: adjustedStartIndex, // Insert at the same startIndex where the Markdown was removed + }, + objectSize: { + height: { + magnitude: 150, + unit: "PT", + }, + width: { + magnitude: 150, + unit: "PT", + }, + }, + }, + }); + const netShift = 1 - markdownLength; // Inline image adds 1, Markdown removes its length + cumulativeShift += netShift; + }); + + return { requests }; +} diff --git a/packages/exports/src/gSuite/docs/populateDoc.ts b/packages/exports/src/gSuite/docs/populateDoc.ts index 7ff5d4d97..3f27edc5d 100644 --- a/packages/exports/src/gSuite/docs/populateDoc.ts +++ b/packages/exports/src/gSuite/docs/populateDoc.ts @@ -1,10 +1,15 @@ import type { docs_v1 } from "@googleapis/docs"; +import { aiLogger } from "@oakai/logger"; import type { Result } from "../../types"; import type { ValueToString } from "../../utils"; import { defaultValueToString } from "../../utils"; +import { findMarkdownImages } from "./findMarkdownImages"; +import { imageReplacements } from "./imageReplacements"; import { textReplacements } from "./textReplacements"; +const log = aiLogger("exports"); + /** * Populates the template document with the given data, handling image replacements for all placeholders. */ @@ -26,13 +31,6 @@ export async function populateDoc< try { const missingData: string[] = []; - // Commenting out this part until the issues are resolved (see @TODOs on function defintiion) - // await processImageReplacements({ - // googleDocs, - // documentId, - // data, - // }); - const { requests: textRequests } = textReplacements({ data, warnIfMissing, @@ -48,13 +46,26 @@ export async function populateDoc< }); } + const markdownImages = await findMarkdownImages(googleDocs, documentId); + + const { requests: imageRequests } = imageReplacements(markdownImages); + + if (imageRequests.length > 0) { + await googleDocs.documents.batchUpdate({ + documentId, + requestBody: { + requests: imageRequests, + }, + }); + } + return { data: { missingData, }, }; } catch (error) { - console.error("Failed to populate document:", error); + log.error("Failed to populate document:", error); return { error, message: "Failed to populate doc template", diff --git a/packages/exports/src/gSuite/docs/processImagesReplacements.ts b/packages/exports/src/gSuite/docs/processImagesReplacements.ts deleted file mode 100644 index 757411204..000000000 --- a/packages/exports/src/gSuite/docs/processImagesReplacements.ts +++ /dev/null @@ -1,120 +0,0 @@ -import type { docs_v1 } from "@googleapis/docs"; - -import { findPlaceholderIndex } from "./findPlaceholderIndex"; - -export async function processImageReplacements< - Data extends Record, ->({ - googleDocs, - documentId, - data, -}: { - googleDocs: docs_v1.Docs; - documentId: string; - data: Data; -}) { - /** - * @TODO this function will ideally return a list of requests which is then - * sent in one batch update. It currently makes a lot of requests to Google - * API, and I've seen it error with 429. - * @TODO currently this function doesn't support multiple images in the same - * string, or images with text before and after it. - */ - for (const [key, value] of Object.entries(data)) { - if (typeof value === "string" && value.includes("![image](")) { - // Extract image URL - const imageMatch = value.match(/!\[.*?\]\((.*?)\)/); - const imageUrl = imageMatch?.[1]; - if (!imageUrl) continue; - - // Extract surrounding text before and after the image placeholder - const [beforeImage, afterImage] = value.split(imageMatch[0]); - - const placeholder = `{{${key}}}`; - const index = await findPlaceholderIndex( - googleDocs, - documentId, - placeholder, - ); - - if (index !== null) { - // Delete the entire placeholder - await googleDocs.documents.batchUpdate({ - documentId, - requestBody: { - requests: [ - { - deleteContentRange: { - range: { - startIndex: index, - endIndex: index + placeholder.length, - }, - }, - }, - ], - }, - }); - - // Insert the before text - if (beforeImage?.trim()) { - await googleDocs.documents.batchUpdate({ - documentId, - requestBody: { - requests: [ - { - insertText: { - text: beforeImage, - location: { - segmentId: null, - index, - }, - }, - }, - ], - }, - }); - } - - // Insert the image - await googleDocs.documents.batchUpdate({ - documentId, - requestBody: { - requests: [ - { - insertInlineImage: { - uri: imageUrl, - location: { - segmentId: null, - index: beforeImage ? index + beforeImage.length : index, - }, - }, - }, - ], - }, - }); - - // Insert the after text - if (afterImage?.trim()) { - await googleDocs.documents.batchUpdate({ - documentId, - requestBody: { - requests: [ - { - insertText: { - text: afterImage, - location: { - segmentId: null, - index: beforeImage - ? index + beforeImage.length + 1 - : index + 1, // Adjust index to account for image insertion - }, - }, - }, - ], - }, - }); - } - } - } - } -} From 701ff6595b785ef3876983710e41a7a4ce511bc1 Mon Sep 17 00:00:00 2001 From: Joe Baker Date: Tue, 3 Dec 2024 11:55:19 +0000 Subject: [PATCH 02/11] chore: set sonar languages (#415) --- sonar-project.properties | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sonar-project.properties b/sonar-project.properties index 77fad2a59..46fe3ecbf 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,9 +8,12 @@ sonar.links.homepage=https://labs.thenational.academy/ sonar.sources=. sonar.exclusions=\ - apps/nextjs/src/lib/avo/Avo.ts - pnpm-lock.yaml - apps/nextjs/.storybook/public/mockServiceWorker.js + apps/nextjs/src/lib/avo/Avo.ts,\ + pnpm-lock.yaml,\ + apps/nextjs/.storybook/public/mockServiceWorker.js,\ + **/*.py,\ + **/*.sql + sonar.cpd.exclusions=\ **/*.test.ts, \ @@ -19,4 +22,5 @@ sonar.cpd.exclusions=\ sonar.tests=. sonar.test.inclusions=**/*.test.ts -sonar.javascript.lcov.reportPaths=packages/**/coverage/lcov.info,apps/nextjs/coverage/lcov.info \ No newline at end of file +sonar.javascript.lcov.reportPaths=packages/**/coverage/lcov.info,apps/nextjs/coverage/lcov.info +sonar.language=ts,tsx,js,jsx,html,css \ No newline at end of file From 00767a0148456f57f8fa21e0cb0149cf849f3574 Mon Sep 17 00:00:00 2001 From: Joe Baker Date: Tue, 3 Dec 2024 12:36:05 +0000 Subject: [PATCH 03/11] feat: test coverage (#406) --- .github/workflows/test.yml | 13 ++++++++++++- apps/nextjs/jest.config.js | 2 ++ packages/aila/jest.config.js | 2 ++ packages/ingest/jest.config.js | 2 ++ sonar-project.properties | 3 ++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cad1aa60e..149fec4e0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,4 +43,15 @@ jobs: inject-env-vars: true - name: Run tests - run: pnpm turbo test --cache-dir=".turbo" -- --maxWorkers=33% + run: pnpm turbo test --cache-dir=".turbo" -- --maxWorkers=33% --coverage + + # Run only on production branch + - name: Report coverage to SonarCloud + if: ${{ github.event.pull_request.merged == true && github.base_ref == 'production' }} + uses: sonarsource/sonarcloud-github-action@master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + -Dsonar.javascript.lcov.reportPaths=packages/**/coverage/lcov.info,apps/nextjs/coverage/lcov.info diff --git a/apps/nextjs/jest.config.js b/apps/nextjs/jest.config.js index d35dc0cb3..cd0dc24f9 100644 --- a/apps/nextjs/jest.config.js +++ b/apps/nextjs/jest.config.js @@ -9,6 +9,7 @@ const config = { { tsconfig: "tsconfig.test.json", useESM: true, + isolatedModules: true, }, ], "^.+\\.svg$": "/jest.svgTransform.js", @@ -30,6 +31,7 @@ const config = { rootDir: ".", resetMocks: true, setupFilesAfterEnv: ["/jest.setup.js"], + collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], collectCoverage: process.env.CI === "true" || process.env.COLLECT_TEST_COVERAGE === "true", coverageReporters: ["lcov", "text"], diff --git a/packages/aila/jest.config.js b/packages/aila/jest.config.js index 9d23cf9d6..e56bfce0b 100644 --- a/packages/aila/jest.config.js +++ b/packages/aila/jest.config.js @@ -9,6 +9,7 @@ const config = { { tsconfig: "tsconfig.test.json", useESM: true, + isolatedModules: true, }, ], }, @@ -26,6 +27,7 @@ const config = { collectCoverage: process.env.CI === "true" || process.env.COLLECT_TEST_COVERAGE === "true", coverageReporters: ["lcov", "text"], + collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], coverageDirectory: "coverage", }; diff --git a/packages/ingest/jest.config.js b/packages/ingest/jest.config.js index 9d23cf9d6..e56bfce0b 100644 --- a/packages/ingest/jest.config.js +++ b/packages/ingest/jest.config.js @@ -9,6 +9,7 @@ const config = { { tsconfig: "tsconfig.test.json", useESM: true, + isolatedModules: true, }, ], }, @@ -26,6 +27,7 @@ const config = { collectCoverage: process.env.CI === "true" || process.env.COLLECT_TEST_COVERAGE === "true", coverageReporters: ["lcov", "text"], + collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], coverageDirectory: "coverage", }; diff --git a/sonar-project.properties b/sonar-project.properties index 46fe3ecbf..2f5d65e8b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -23,4 +23,5 @@ sonar.cpd.exclusions=\ sonar.tests=. sonar.test.inclusions=**/*.test.ts sonar.javascript.lcov.reportPaths=packages/**/coverage/lcov.info,apps/nextjs/coverage/lcov.info -sonar.language=ts,tsx,js,jsx,html,css \ No newline at end of file + +sonar.language=ts,tsx,js,jsx,html,css From 7539661e22fa0e334138e0f796a700662e4e37e3 Mon Sep 17 00:00:00 2001 From: MG Date: Tue, 3 Dec 2024 15:23:52 +0000 Subject: [PATCH 04/11] fix: moderation persist scores [AI-696] (#418) --- .../src/features/moderation/AilaModeration.ts | 4 +++- .../moderation/moderators/OpenAiModerator.ts | 6 ++---- packages/core/src/models/moderations.ts | 3 +++ .../utils/ailaModeration/moderationSchema.ts | 21 +++++++++++-------- .../migration.sql | 2 ++ packages/db/prisma/schema.prisma | 1 + 6 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 packages/db/prisma/migrations/20241202151820_moderation_scores/migration.sql diff --git a/packages/aila/src/features/moderation/AilaModeration.ts b/packages/aila/src/features/moderation/AilaModeration.ts index b7d3c8a3e..ee0564143 100644 --- a/packages/aila/src/features/moderation/AilaModeration.ts +++ b/packages/aila/src/features/moderation/AilaModeration.ts @@ -71,6 +71,7 @@ export class AilaModeration implements AilaModerationFeature { appSessionId: chatId, messageId: lastAssistantMessage.id, categories: moderationResult.categories, + scores: moderationResult.scores, justification: moderationResult.justification, lesson: lessonPlan, }); @@ -195,10 +196,11 @@ export class AilaModeration implements AilaModerationFeature { messages: Message[]; lessonPlan: LooseLessonPlan; retries: number; - }) { + }): Promise { if (retries < 1) { return { categories: [], + scores: undefined, justification: "Failed to parse moderation response", }; } diff --git a/packages/aila/src/features/moderation/moderators/OpenAiModerator.ts b/packages/aila/src/features/moderation/moderators/OpenAiModerator.ts index d4cea0f55..10ed5b29c 100644 --- a/packages/aila/src/features/moderation/moderators/OpenAiModerator.ts +++ b/packages/aila/src/features/moderation/moderators/OpenAiModerator.ts @@ -123,10 +123,7 @@ export class OpenAiModerator extends AilaModerator { ); const log = aiLogger("aila:moderation:response"); - log.info( - "Moderation response: ", - JSON.stringify(moderationResponse, null, 2), - ); + log.info(JSON.stringify(moderationResponse)); const response = moderationResponseSchema.safeParse( JSON.parse(moderationResponse.choices[0]?.message.content ?? "null"), @@ -152,6 +149,7 @@ export class OpenAiModerator extends AilaModerator { return { justification, + scores, categories: categories.filter((category) => { /** * We only want to include the category if the parent category scores below a certain threshold. diff --git a/packages/core/src/models/moderations.ts b/packages/core/src/models/moderations.ts index 7f7acb858..c73087306 100644 --- a/packages/core/src/models/moderations.ts +++ b/packages/core/src/models/moderations.ts @@ -34,6 +34,7 @@ export class Moderations { appSessionId, messageId, categories, + scores, justification, lesson, }: { @@ -41,6 +42,7 @@ export class Moderations { appSessionId: string; messageId: string; categories: ModerationResult["categories"]; + scores: ModerationResult["scores"]; justification?: string; lesson: Snapshot; }): Promise { @@ -58,6 +60,7 @@ export class Moderations { userId, categories, justification, + scores, appSessionId, messageId, lessonSnapshotId, diff --git a/packages/core/src/utils/ailaModeration/moderationSchema.ts b/packages/core/src/utils/ailaModeration/moderationSchema.ts index 8c1bc6612..c649a5483 100644 --- a/packages/core/src/utils/ailaModeration/moderationSchema.ts +++ b/packages/core/src/utils/ailaModeration/moderationSchema.ts @@ -44,19 +44,21 @@ export const moderationCategoriesSchema = z.array( const likertScale = z.number().int().min(1).max(5); +const moderationScoresSchema = z.object({ + l: likertScale.describe("Language and discrimination score"), + v: likertScale.describe("Violence and crime score"), + u: likertScale.describe("Upsetting, disturbing and sensitive score"), + s: likertScale.describe("Nudity and sex score"), + p: likertScale.describe("Physical activity and safety score"), + t: likertScale.describe("Toxic score"), +}); + /** * Schema for the moderation response from the LLM. * Note: it's important that 'categories' is the last field in the schema */ export const moderationResponseSchema = z.object({ - scores: z.object({ - l: likertScale.describe("Language and discrimination score"), - v: likertScale.describe("Violence and crime score"), - u: likertScale.describe("Upsetting, disturbing and sensitive score"), - s: likertScale.describe("Nudity and sex score"), - p: likertScale.describe("Physical activity and safety score"), - t: likertScale.describe("Toxic score"), - }), + scores: moderationScoresSchema, justification: z.string().describe("Add justification for your scores."), categories: moderationCategoriesSchema, }); @@ -65,8 +67,9 @@ export const moderationResponseSchema = z.object({ * Schema for the moderation result, once parsed from the moderation response */ export const moderationResultSchema = z.object({ - categories: moderationCategoriesSchema, justification: z.string().optional(), + scores: moderationScoresSchema.optional(), + categories: moderationCategoriesSchema, }); export type ModerationResult = z.infer; diff --git a/packages/db/prisma/migrations/20241202151820_moderation_scores/migration.sql b/packages/db/prisma/migrations/20241202151820_moderation_scores/migration.sql new file mode 100644 index 000000000..e84b992a3 --- /dev/null +++ b/packages/db/prisma/migrations/20241202151820_moderation_scores/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."moderations" ADD COLUMN "scores" JSONB; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index aa9b59a59..4cabd577c 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -789,6 +789,7 @@ model Moderation { appSessionId String @map("app_session_id") messageId String @map("message_id") categories Json[] + scores Json? @map("scores") @db.JsonB justification String? lessonSnapshotId String? @map("lesson_snapshot_id") // A user's comment in relation to the moderation. Likely they are contesting it. From e36841ecde6a3205744d1bebc3891034dac90dcc Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:33:21 +0100 Subject: [PATCH 05/11] test: add stories for onboarding (#423) --- .../Onboarding/AcceptTermsForm.stories.tsx | 15 +++++++++++ .../components/Onboarding/AcceptTermsForm.tsx | 3 ++- .../LegacyUpgradeNotice.stories.tsx | 15 +++++++++++ .../Onboarding/TermsContent.stories.tsx | 15 +++++++++++ .../{ => Onboarding}/TermsContent.tsx | 0 .../components/SignUpSignInLayout.stories.tsx | 15 +++++++++++ apps/nextjs/src/mocks/clerk/nextjs.ts | 1 + .../src/mocks/clerk/nextjsComponents.tsx | 27 +++++++++++++++++-- 8 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx create mode 100644 apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx create mode 100644 apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx rename apps/nextjs/src/components/{ => Onboarding}/TermsContent.tsx (100%) create mode 100644 apps/nextjs/src/components/SignUpSignInLayout.stories.tsx diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx new file mode 100644 index 000000000..cca0f90ef --- /dev/null +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { AcceptTermsForm } from "./AcceptTermsForm"; + +const meta: Meta = { + title: "Pages/Onboarding/AcceptTermsForm", + component: AcceptTermsForm, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx index 54ce49d93..fd3a4cb79 100644 --- a/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx +++ b/apps/nextjs/src/components/Onboarding/AcceptTermsForm.tsx @@ -19,9 +19,10 @@ import Link from "next/link"; import Button from "@/components/Button"; import CheckBox from "@/components/CheckBox"; import SignUpSignInLayout from "@/components/SignUpSignInLayout"; -import TermsContent from "@/components/TermsContent"; import { trpc } from "@/utils/trpc"; +import TermsContent from "./TermsContent"; + export const AcceptTermsForm = () => { const [dropDownOpen, setDropDownOpen] = useState(true); const { isLoaded } = useUser(); diff --git a/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx new file mode 100644 index 000000000..d61d4b03f --- /dev/null +++ b/apps/nextjs/src/components/Onboarding/LegacyUpgradeNotice.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { LegacyUpgradeNotice } from "./LegacyUpgradeNotice"; + +const meta: Meta = { + title: "Pages/Onboarding/LegacyUpgradeNotice", + component: LegacyUpgradeNotice, +}; + +export default meta; +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 new file mode 100644 index 000000000..4065320f4 --- /dev/null +++ b/apps/nextjs/src/components/Onboarding/TermsContent.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import TermsContent from "./TermsContent"; + +const meta: Meta = { + title: "Components/Onboarding/TermsContent", + component: TermsContent, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/apps/nextjs/src/components/TermsContent.tsx b/apps/nextjs/src/components/Onboarding/TermsContent.tsx similarity index 100% rename from apps/nextjs/src/components/TermsContent.tsx rename to apps/nextjs/src/components/Onboarding/TermsContent.tsx diff --git a/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx b/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx new file mode 100644 index 000000000..63cda9985 --- /dev/null +++ b/apps/nextjs/src/components/SignUpSignInLayout.stories.tsx @@ -0,0 +1,15 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import SignUpSignInLayout from "./SignUpSignInLayout"; + +const meta: Meta = { + title: "Components/Layout/SignUpSignInLayout", + component: SignUpSignInLayout, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; diff --git a/apps/nextjs/src/mocks/clerk/nextjs.ts b/apps/nextjs/src/mocks/clerk/nextjs.ts index ea6c3ca74..e583ffc05 100644 --- a/apps/nextjs/src/mocks/clerk/nextjs.ts +++ b/apps/nextjs/src/mocks/clerk/nextjs.ts @@ -7,6 +7,7 @@ export { useAuth, useUser, useClerk, + useSession, SignedIn, SignedOut, ClerkProvider, diff --git a/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx b/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx index c96d21080..5b56737cd 100644 --- a/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx +++ b/apps/nextjs/src/mocks/clerk/nextjsComponents.tsx @@ -87,12 +87,35 @@ export const useAuth = () => { }; }; -export const SignedIn = ({ children }: { readonly children: React.ReactNode }) => { +export const useSession = () => { + const context = React.useContext(ClerkContext); + const mockSession = {}; + const session = context.isLoaded + ? context.isSignedIn + ? mockSession + : null + : undefined; + return { + isLoaded: context.isLoaded, + isSignedIn: context.isSignedIn, + session, + }; +}; + +export const SignedIn = ({ + children, +}: { + readonly children: React.ReactNode; +}) => { const context = React.useContext(ClerkContext); return context.isSignedIn ? children : null; }; -export const SignedOut = ({ children }: { readonly children: React.ReactNode }) => { +export const SignedOut = ({ + children, +}: { + readonly children: React.ReactNode; +}) => { const context = React.useContext(ClerkContext); return context.isSignedIn ? null : children; }; From edce94990a1ab4fcb1d6a6d57dbc304e9a52ca30 Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:33:44 +0100 Subject: [PATCH 06/11] test: add story for chat lhs header (#420) --- .../Chat/chat-left-hand-side.tsx | 1 + .../Chat/chat-lhs-header.stories.tsx | 55 +++++++++++++++++++ .../AppComponents/Chat/chat-lhs-header.tsx | 4 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx index ba3fab32e..9f78bc240 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx @@ -39,6 +39,7 @@ const ChatLeftHandSide = ({ setShowLessonMobile={setShowLessonMobile} showLessonMobile={showLessonMobile} isDemoUser={isDemoUser} + showStreamingStatus={process.env.NEXT_PUBLIC_ENVIRONMENT !== "prd"} />
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 new file mode 100644 index 000000000..8f2e48dbd --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + ChatContext, + type ChatContextProps, +} from "@/components/ContextProviders/ChatProvider"; + +import ChatLhsHeader from "./chat-lhs-header"; + +const ChatDecorator: Story["decorators"] = (Story, { parameters }) => ( + + + +); + +const meta: Meta = { + title: "Components/Chat/ChatLhsHeader", + component: ChatLhsHeader, + tags: ["autodocs"], + decorators: [ChatDecorator], + args: { + showStreamingStatus: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: {}, +}; + +export const NonProdStreamingStatus: Story = { + args: { + showStreamingStatus: true, + }, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const DemoBannerPadding: Story = { + args: { + isDemoUser: true, + }, +}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx index 45fadb88e..06176163f 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-lhs-header.tsx @@ -11,19 +11,21 @@ type ChatLhsHeaderProps = { setShowLessonMobile: (value: boolean) => void; showLessonMobile: boolean; isDemoUser: boolean; + showStreamingStatus: boolean; }; const ChatLhsHeader = ({ setShowLessonMobile, showLessonMobile, isDemoUser, + showStreamingStatus, }: Readonly) => { const router = useRouter(); const chat = useLessonChat(); return ( <>
- {process.env.NEXT_PUBLIC_ENVIRONMENT !== "prd" && ( + {showStreamingStatus && (
{chat.ailaStreamingStatus} From d40af98eac0da994393f4496b8fcd821084c9769 Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Tue, 3 Dec 2024 16:33:57 +0100 Subject: [PATCH 07/11] test: chat quick buttons stories (#417) --- .../Chat/chat-left-hand-side.tsx | 9 +- .../AppComponents/Chat/chat-panel.tsx | 18 +-- .../Chat/chat-quick-buttons.stories.tsx | 133 ++++++++++++++++++ .../AppComponents/Chat/chat-quick-buttons.tsx | 14 +- .../AppComponents/Chat/prompt-form.tsx | 10 +- .../analytics/lessonPlanTrackingContext.tsx | 2 +- 6 files changed, 156 insertions(+), 30 deletions(-) create mode 100644 apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx index 9f78bc240..d2ff39208 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-left-hand-side.tsx @@ -49,14 +49,9 @@ const ChatLeftHandSide = ({ demo={demo} /> - {!isDemoLocked && ( - - )} + {!isDemoLocked && }
- + ); diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx index fca32ed91..ccd24aa16 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx @@ -8,7 +8,6 @@ import useAnalytics from "@/lib/analytics/useAnalytics"; import ChatPanelDisclaimer from "./chat-panel-disclaimer"; interface ChatPanelProps { - isEmptyScreen: boolean; isDemoLocked: boolean; } @@ -18,13 +17,11 @@ function LockedPromptForm() { ); } -export function ChatPanel({ - isEmptyScreen, - isDemoLocked, -}: Readonly) { +export function ChatPanel({ isDemoLocked }: Readonly) { const chat = useLessonChat(); const { id, + messages, isLoading, input, setInput, @@ -35,11 +32,14 @@ export function ChatPanel({ } = chat; const { trackEvent } = useAnalytics(); - const containerClass = `grid w-full grid-cols-1 ${isEmptyScreen ? "sm:grid-cols-1" : ""} peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]`; + + const hasMessages = !!messages.length; + + const containerClass = `grid w-full grid-cols-1 ${hasMessages ? "sm:grid-cols-1" : ""} peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]`; return (
-
+
{!isDemoLocked && ( { @@ -57,7 +57,7 @@ export function ChatPanel({ input={input} setInput={setInput} ailaStreamingStatus={ailaStreamingStatus} - isEmptyScreen={isEmptyScreen} + hasMessages={hasMessages} queueUserAction={queueUserAction} queuedUserAction={queuedUserAction} /> @@ -73,7 +73,7 @@ export function ChatPanel({ const chatBoxWrap = cva(["mx-auto w-full "], { variants: { - isEmptyScreen: { + hasMessages: { false: "max-w-2xl ", true: "", }, 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 new file mode 100644 index 000000000..a3baefe15 --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.stories.tsx @@ -0,0 +1,133 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + ChatContext, + type ChatContextProps, +} from "@/components/ContextProviders/ChatProvider"; +import { lessonPlanTrackingContext } from "@/lib/analytics/lessonPlanTrackingContext"; + +import ChatQuickButtons from "./chat-quick-buttons"; + +const DummyMessage = {}; + +const ChatDecorator: Story["decorators"] = (Story, { parameters }) => ( + + + +); + +const LessonPlanTrackingContextDecorator: Story["decorators"] = (Story) => ( + {}, + onClickRetry: () => {}, + onClickStartFromExample: () => {}, + onClickStartFromFreeText: () => {}, + onStreamFinished: () => {}, + onSubmitText: () => {}, + }} + > + + +); + +const meta: Meta = { + title: "Components/Chat/ChatQuickButtons", + component: ChatQuickButtons, + tags: ["autodocs"], + decorators: [ChatDecorator, LessonPlanTrackingContextDecorator], +}; + +export default meta; +type Story = StoryObj; + +export const Idle: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Idle", + }, + }, +}; + +export const Loading: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Loading", + }, + }, +}; + +export const LoadingWithoutMessages: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Loading", + messages: [], + }, + }, +}; + +export const RequestMade: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "RequestMade", + }, + }, +}; + +export const StreamingLessonPlan: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const StreamingChatResponse: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingChatResponse", + }, + }, +}; + +export const Moderating: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Moderating", + }, + }, +}; + +export const StreamingWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const ModeratingWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "Moderating", + }, + }, +}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx index 5507dd44a..de37e5a74 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-quick-buttons.tsx @@ -12,16 +12,12 @@ import type { AilaStreamingStatus } from "./Chat/hooks/useAilaStreamingStatus"; import ChatButton from "./ui/chat-button"; import { IconRefresh, IconStop } from "./ui/icons"; -export type QuickActionButtonsProps = Readonly<{ - isEmptyScreen: boolean; -}>; - const shouldAllowStop = ( ailaStreamingStatus: AilaStreamingStatus, - isEmptyScreen: boolean, + hasMessages: boolean, queuedUserAction: string | null, ) => { - if (!isEmptyScreen) { + if (!hasMessages) { return false; } @@ -43,7 +39,7 @@ const shouldAllowStop = ( return false; }; -const QuickActionButtons = ({ isEmptyScreen }: QuickActionButtonsProps) => { +const QuickActionButtons = () => { const chat = useLessonChat(); const { trackEvent } = useAnalytics(); const lessonPlanTracking = useLessonPlanTracking(); @@ -57,6 +53,8 @@ const QuickActionButtons = ({ isEmptyScreen }: QuickActionButtonsProps) => { queuedUserAction, } = chat; + const hasMessages = !!messages.length; + const shouldAllowUserAction = ["Idle", "Moderating"].includes(ailaStreamingStatus) && !queuedUserAction; @@ -106,7 +104,7 @@ const QuickActionButtons = ({ isEmptyScreen }: QuickActionButtonsProps) => { {shouldAllowStop( ailaStreamingStatus, - isEmptyScreen, + hasMessages, queuedUserAction, ) && ( { onSubmit: (value: string) => void; - isEmptyScreen: boolean; + hasMessages: boolean; placeholder?: string; ailaStreamingStatus: AilaStreamingStatus; queuedUserAction?: string | null; @@ -29,7 +29,7 @@ export function PromptForm({ onSubmit, input, setInput, - isEmptyScreen, + hasMessages, placeholder, queuedUserAction, queueUserAction, @@ -93,7 +93,7 @@ export function PromptForm({ value={input} onChange={(e) => setInput(e.target.value)} placeholder={handlePlaceholder( - isEmptyScreen, + hasMessages, queuedUserAction ?? placeholder, )} spellCheck={false} @@ -119,11 +119,11 @@ export function PromptForm({ ); } -function handlePlaceholder(isEmptyScreen: boolean, placeholder?: string) { +function handlePlaceholder(hasMessages: boolean, placeholder?: string) { if (placeholder && !["continue", "regenerate"].includes(placeholder)) { return placeholder; } - return !isEmptyScreen + return !hasMessages ? "Type a subject, key stage and title" : "Type your response here"; } diff --git a/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx b/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx index 2f9e9a3af..d664640d4 100644 --- a/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx +++ b/apps/nextjs/src/lib/analytics/lessonPlanTrackingContext.tsx @@ -30,7 +30,7 @@ type LessonPlanTrackingContext = { onClickStartFromFreeText: (text: string) => void; }; -const lessonPlanTrackingContext = +export const lessonPlanTrackingContext = createContext(null); export type LessonPlanTrackingProviderProps = Readonly<{ From e6a0afb323a10939ebfe7ceb3e39462b0f7a7e6e Mon Sep 17 00:00:00 2001 From: Adam Howard <91115+codeincontext@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:07:40 +0100 Subject: [PATCH 08/11] test: add chat panel stories (#421) --- .../Chat/button-scroll-to-bottom.tsx | 40 ---- .../AppComponents/Chat/chat-panel.stories.tsx | 178 ++++++++++++++++++ .../AppComponents/Chat/chat-panel.tsx | 5 +- .../AppComponents/Chat/prompt-form.tsx | 19 +- apps/nextjs/src/lib/hooks/use-sidebar.tsx | 4 +- 5 files changed, 192 insertions(+), 54 deletions(-) delete mode 100644 apps/nextjs/src/components/AppComponents/Chat/button-scroll-to-bottom.tsx create mode 100644 apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx diff --git a/apps/nextjs/src/components/AppComponents/Chat/button-scroll-to-bottom.tsx b/apps/nextjs/src/components/AppComponents/Chat/button-scroll-to-bottom.tsx deleted file mode 100644 index c5fe3b3bc..000000000 --- a/apps/nextjs/src/components/AppComponents/Chat/button-scroll-to-bottom.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { - Button, - type ButtonProps, -} from "@/components/AppComponents/Chat/ui/button"; -import { IconArrowDown } from "@/components/AppComponents/Chat/ui/icons"; -import useAnalytics from "@/lib/analytics/useAnalytics"; -import { useAtBottom } from "@/lib/hooks/use-at-bottom"; -import { cn } from "@/lib/utils"; - -export function ButtonScrollToBottom({ - className, - ...props -}: Readonly) { - const isAtBottom = useAtBottom(); - const { trackEvent } = useAnalytics(); - 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 new file mode 100644 index 000000000..c2cff638c --- /dev/null +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.stories.tsx @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { + ChatContext, + type ChatContextProps, +} from "@/components/ContextProviders/ChatProvider"; +import { lessonPlanTrackingContext } from "@/lib/analytics/lessonPlanTrackingContext"; +import { SidebarContext } from "@/lib/hooks/use-sidebar"; + +import { ChatPanel } from "./chat-panel"; + +const DummyMessage = {}; + +const ChatDecorator: Story["decorators"] = (Story, { parameters }) => ( + + + +); + +const LessonPlanTrackingContextDecorator: Story["decorators"] = (Story) => ( + {}, + onClickRetry: () => {}, + onClickStartFromExample: () => {}, + onClickStartFromFreeText: () => {}, + onStreamFinished: () => {}, + onSubmitText: () => {}, + }} + > + + +); + +const SidebarContextDecorator: Story["decorators"] = (Story) => ( + {}, + isLoading: false, + isSidebarOpen: false, + }} + > + + +); + +const meta: Meta = { + title: "Components/Chat/ChatPanel", + component: ChatPanel, + tags: ["autodocs"], + decorators: [ + ChatDecorator, + LessonPlanTrackingContextDecorator, + SidebarContextDecorator, + ], + args: { + isDemoLocked: false, + }, +}; + +export default meta; +type Story = StoryObj; + +export const NoMessages: Story = { + args: {}, + parameters: { + chatContext: { + messages: [], + }, + }, +}; + +export const DemoLocked: Story = { + args: { + isDemoLocked: true, + }, +}; + +export const Idle: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Idle", + }, + }, +}; + +export const IdleWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "Idle", + }, + }, +}; + +export const Loading: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Loading", + }, + }, +}; + +export const RequestMade: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "RequestMade", + }, + }, +}; + +export const StreamingLessonPlan: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const StreamingChatResponse: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "StreamingChatResponse", + }, + }, +}; + +export const StreamingWithQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "StreamingLessonPlan", + }, + }, +}; + +export const Moderating: Story = { + args: {}, + parameters: { + chatContext: { + ailaStreamingStatus: "Moderating", + }, + }, +}; + +export const ModeratingWithRegenerateUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "regenerate", + ailaStreamingStatus: "Moderating", + }, + }, +}; + +export const CustomQueuedUserAction: Story = { + args: {}, + parameters: { + chatContext: { + queuedUserAction: "Increase the reading age of that section", + ailaStreamingStatus: "Moderating", + }, + }, +}; diff --git a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx index ccd24aa16..b2b58dcb2 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-panel.tsx @@ -1,6 +1,5 @@ import { cva } from "class-variance-authority"; -import { ButtonScrollToBottom } from "@/components/AppComponents/Chat/button-scroll-to-bottom"; import { PromptForm } from "@/components/AppComponents/Chat/prompt-form"; import { useLessonChat } from "@/components/ContextProviders/ChatProvider"; import useAnalytics from "@/lib/analytics/useAnalytics"; @@ -31,14 +30,12 @@ export function ChatPanel({ isDemoLocked }: Readonly) { queuedUserAction, } = chat; - const { trackEvent } = useAnalytics(); - const hasMessages = !!messages.length; + const { trackEvent } = useAnalytics(); const containerClass = `grid w-full grid-cols-1 ${hasMessages ? "sm:grid-cols-1" : ""} peer-[[data-state=open]]:group-[]:lg:pl-[250px] peer-[[data-state=open]]:group-[]:xl:pl-[300px]`; return (
-
{!isDemoLocked && ( { onSubmit: (value: string) => void; hasMessages: boolean; - placeholder?: string; ailaStreamingStatus: AilaStreamingStatus; queuedUserAction?: string | null; queueUserAction?: (action: string) => void; @@ -30,7 +29,6 @@ export function PromptForm({ input, setInput, hasMessages, - placeholder, queuedUserAction, queueUserAction, }: Readonly) { @@ -94,7 +92,7 @@ export function PromptForm({ onChange={(e) => setInput(e.target.value)} placeholder={handlePlaceholder( hasMessages, - queuedUserAction ?? placeholder, + queuedUserAction ?? undefined, )} spellCheck={false} className="min-h-[60px] w-full resize-none bg-transparent px-10 py-[1.3rem] text-base focus-within:outline-none" @@ -119,11 +117,14 @@ export function PromptForm({ ); } -function handlePlaceholder(hasMessages: boolean, placeholder?: string) { - if (placeholder && !["continue", "regenerate"].includes(placeholder)) { - return placeholder; +function handlePlaceholder(hasMessages: boolean, queuedUserAction?: string) { + if ( + queuedUserAction && + !["continue", "regenerate"].includes(queuedUserAction) + ) { + return queuedUserAction; } - return !hasMessages - ? "Type a subject, key stage and title" - : "Type your response here"; + return hasMessages + ? "Type your response here" + : "Type a subject, key stage and title"; } diff --git a/apps/nextjs/src/lib/hooks/use-sidebar.tsx b/apps/nextjs/src/lib/hooks/use-sidebar.tsx index b95c386ad..eaba67064 100644 --- a/apps/nextjs/src/lib/hooks/use-sidebar.tsx +++ b/apps/nextjs/src/lib/hooks/use-sidebar.tsx @@ -17,7 +17,9 @@ export interface SidebarContext { isLoading: boolean; } -const SidebarContext = createContext(undefined); +export const SidebarContext = createContext( + undefined, +); export function useSidebar() { const context = useContext(SidebarContext); From 1f4aa9dde84402df089ce4d6947472d389f576a0 Mon Sep 17 00:00:00 2001 From: Stef Lewandowski Date: Wed, 4 Dec 2024 18:18:19 +0000 Subject: [PATCH 09/11] fix: upgrade typescript + prettier + eslint with a single shared linting config (#424) --- .eslintrc.cjs | 13 - .github/actions/ref_from_sha/action.yml | 2 +- ...branch_from_sha.js => branch_from_sha.cjs} | 0 .../ref_from_sha/{index.js => index.cjs} | 4 +- .../{pr_from_sha.js => pr_from_sha.cjs} | 0 .../ref_from_sha/test/{e2e.js => e2e.cjs} | 4 +- apps/nextjs/.eslintrc.cjs | 24 - .../{jest.config.js => jest.config.mjs} | 16 +- apps/nextjs/{jest.setup.js => jest.setup.cjs} | 0 apps/nextjs/jest.static.d.ts | 1 + ....svgTransform.js => jest.svgTransform.mjs} | 2 +- apps/nextjs/next.config.js | 3 +- apps/nextjs/package.json | 38 +- ...ig_helpers.js => build_config_helpers.cjs} | 2 +- apps/nextjs/scripts/increase-listeners.js | 18 - apps/nextjs/scripts/preload-chat-routes.mjs | 1 + .../ai-apps/lesson-planner/state/actions.ts | 1 + .../fixtures/FixtureRecordOpenAiClient.ts | 2 +- .../AppComponents/Chat/chat-message/index.tsx | 4 +- .../AppComponents/Chat/ui/textarea.tsx | 32 +- .../AppComponents/QuizDesigner/ErrorBox.tsx | 2 + .../ContentOptions/ModalFooterButtons.tsx | 2 + apps/nextjs/src/lib/hooks/use-sidebar.tsx | 6 +- .../nextjs/src/middlewares/auth.middleware.ts | 1 + .../nextjs/src/mocks/clerk/ClerkDecorator.tsx | 2 +- apps/nextjs/tailwind.config.cjs | 1 - apps/nextjs/tests-e2e/helpers/auth/index.ts | 4 +- .../tests-e2e/tests/aila-chat/helpers.ts | 2 +- apps/nextjs/tests-e2e/tests/auth.test.ts | 4 +- apps/nextjs/tsconfig.json | 7 +- apps/nextjs/tsconfig.test.json | 4 +- docs/README.md | 1 - docs/diagrams/chat-sequence.md | 78 - eslint.config.mjs | 3 + package.json | 19 +- packages/aila/.eslintrc.cjs | 7 - packages/aila/.prettierignore | 2 +- .../jest.config.js => aila/jest.config.mjs} | 21 +- packages/aila/package.json | 31 +- packages/aila/src/core/Aila.test.ts | 4 +- .../categorisers/AilaCategorisation.ts | 2 +- .../moderation/moderators/OpenAiModerator.ts | 2 +- packages/aila/src/protocol/schema.ts | 16 +- packages/aila/tsconfig.test.json | 4 +- packages/api/.eslintrc.cjs | 7 - packages/api/package.json | 18 +- packages/api/src/router/lesson.ts | 6 +- packages/core/.eslintrc.cjs | 7 - packages/core/package.json | 12 +- .../import-new-lessons/importNewLessons.ts | 1 - packages/db/.eslintrc.cjs | 7 - packages/db/client/index.ts | 2 +- packages/db/package.json | 21 +- packages/db/schemas/lesson-with-snippets.ts | 2 - packages/eslint-config-custom/index.js | 73 - packages/eslint-config-custom/nextjs.js | 3 - packages/eslint-config-custom/package.json | 38 - .../patch/modern-module-resolution.js | 3 - .../eslint-config-custom/react-library.js | 23 - packages/eslint-config/package.json | 42 + packages/eslint-config/src/ignores.mjs | 3 + packages/eslint-config/src/index.mjs | 140 + packages/eslint-config/src/plugins.d.ts | 3 + packages/eslint-config/src/rules.mjs | 64 + packages/eslint-config/tsconfig.json | 14 + packages/exports/.eslintrc.cjs | 7 - packages/exports/package.json | 16 +- packages/ingest/.eslintrc.cjs | 7 - .../jest.config.js => ingest/jest.config.mjs} | 14 +- packages/ingest/package.json | 28 +- .../ingest/src/openai-batches/customId.ts | 8 - .../src/steps/5-lp-parts-embed-start.ts | 4 +- packages/ingest/tsconfig.json | 6 +- packages/ingest/tsconfig.test.json | 4 +- packages/logger/.eslintrc.cjs | 7 - packages/logger/browser.ts | 1 + packages/logger/eslint.config.mjs | 24 + packages/logger/package.json | 9 +- packages/logger/tsconfig.json | 2 +- .../prettier-config/{index.js => index.mjs} | 3 +- packages/prettier-config/package.json | 15 +- pnpm-lock.yaml | 2906 ++++++++++------- tsconfig.json | 9 +- tsconfig.test.json | 6 +- 84 files changed, 2272 insertions(+), 1685 deletions(-) delete mode 100644 .eslintrc.cjs rename .github/actions/ref_from_sha/{branch_from_sha.js => branch_from_sha.cjs} (100%) rename .github/actions/ref_from_sha/{index.js => index.cjs} (95%) rename .github/actions/ref_from_sha/{pr_from_sha.js => pr_from_sha.cjs} (100%) rename .github/actions/ref_from_sha/test/{e2e.js => e2e.cjs} (90%) delete mode 100644 apps/nextjs/.eslintrc.cjs rename apps/nextjs/{jest.config.js => jest.config.mjs} (72%) rename apps/nextjs/{jest.setup.js => jest.setup.cjs} (100%) rename apps/nextjs/{jest.svgTransform.js => jest.svgTransform.mjs} (89%) rename apps/nextjs/scripts/{build_config_helpers.js => build_config_helpers.cjs} (99%) delete mode 100644 apps/nextjs/scripts/increase-listeners.js delete mode 100644 docs/README.md delete mode 100644 docs/diagrams/chat-sequence.md create mode 100644 eslint.config.mjs delete mode 100644 packages/aila/.eslintrc.cjs rename packages/{ingest/jest.config.js => aila/jest.config.mjs} (66%) delete mode 100644 packages/api/.eslintrc.cjs delete mode 100644 packages/core/.eslintrc.cjs delete mode 100644 packages/db/.eslintrc.cjs delete mode 100644 packages/eslint-config-custom/index.js delete mode 100644 packages/eslint-config-custom/nextjs.js delete mode 100644 packages/eslint-config-custom/package.json delete mode 100644 packages/eslint-config-custom/patch/modern-module-resolution.js delete mode 100644 packages/eslint-config-custom/react-library.js create mode 100644 packages/eslint-config/package.json create mode 100644 packages/eslint-config/src/ignores.mjs create mode 100644 packages/eslint-config/src/index.mjs create mode 100644 packages/eslint-config/src/plugins.d.ts create mode 100644 packages/eslint-config/src/rules.mjs create mode 100644 packages/eslint-config/tsconfig.json delete mode 100644 packages/exports/.eslintrc.cjs delete mode 100644 packages/ingest/.eslintrc.cjs rename packages/{aila/jest.config.js => ingest/jest.config.mjs} (71%) delete mode 100644 packages/logger/.eslintrc.cjs create mode 100644 packages/logger/eslint.config.mjs rename packages/prettier-config/{index.js => index.mjs} (89%) diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 8e608153e..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,13 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -module.exports = { - root: true, - extends: ["eslint-config-custom"], - parserOptions: { - tsconfigRootDir: __dirname, - project: [ - "./tsconfig.json", - "./apps/*/tsconfig.json", - "./packages/*/tsconfig.json", - ], - }, -}; diff --git a/.github/actions/ref_from_sha/action.yml b/.github/actions/ref_from_sha/action.yml index a589b58af..06e9edcc4 100644 --- a/.github/actions/ref_from_sha/action.yml +++ b/.github/actions/ref_from_sha/action.yml @@ -13,4 +13,4 @@ outputs: description: "The number of the PR if one is found" runs: using: "node20" - main: "index.js" + main: "index.cjs" diff --git a/.github/actions/ref_from_sha/branch_from_sha.js b/.github/actions/ref_from_sha/branch_from_sha.cjs similarity index 100% rename from .github/actions/ref_from_sha/branch_from_sha.js rename to .github/actions/ref_from_sha/branch_from_sha.cjs diff --git a/.github/actions/ref_from_sha/index.js b/.github/actions/ref_from_sha/index.cjs similarity index 95% rename from .github/actions/ref_from_sha/index.js rename to .github/actions/ref_from_sha/index.cjs index 9356f47e3..393ca3ef3 100644 --- a/.github/actions/ref_from_sha/index.js +++ b/.github/actions/ref_from_sha/index.cjs @@ -8,8 +8,8 @@ const core = require("@actions/core"); const github = require("@actions/github"); -const prFromSha = require("./pr_from_sha"); -const branchFromSha = require("./branch_from_sha"); +const prFromSha = require("./pr_from_sha.cjs"); +const branchFromSha = require("./branch_from_sha.cjs"); async function run() { try { diff --git a/.github/actions/ref_from_sha/pr_from_sha.js b/.github/actions/ref_from_sha/pr_from_sha.cjs similarity index 100% rename from .github/actions/ref_from_sha/pr_from_sha.js rename to .github/actions/ref_from_sha/pr_from_sha.cjs diff --git a/.github/actions/ref_from_sha/test/e2e.js b/.github/actions/ref_from_sha/test/e2e.cjs similarity index 90% rename from .github/actions/ref_from_sha/test/e2e.js rename to .github/actions/ref_from_sha/test/e2e.cjs index 66d036752..475e3933a 100644 --- a/.github/actions/ref_from_sha/test/e2e.js +++ b/.github/actions/ref_from_sha/test/e2e.cjs @@ -1,7 +1,7 @@ const github = require("@actions/github"); -const prFromSha = require("../pr_from_sha"); -const branchFromSha = require("../branch_from_sha"); +const prFromSha = require("../pr_from_sha.cjs"); +const branchFromSha = require("../branch_from_sha.cjs"); const githubToken = process.env.GITHUB_TOKEN; diff --git a/apps/nextjs/.eslintrc.cjs b/apps/nextjs/.eslintrc.cjs deleted file mode 100644 index 9b9cbd01b..000000000 --- a/apps/nextjs/.eslintrc.cjs +++ /dev/null @@ -1,24 +0,0 @@ -/** @type {import("eslint").Linter.Config} */ -module.exports = { - extends: ["../../.eslintrc.cjs", "next", "plugin:storybook/recommended"], - rules: { - "react/prefer-read-only-props": "error", - "react/jsx-no-useless-fragment": "warn", - "no-restricted-imports": [ - "error", - { - paths: [ - { - name: "posthog-js/react", - importNames: ["usePostHog"], - message: - "usePostHog doesn't support multiple PostHog instances, use useAnalytics instead", - }, - ], - }, - ], - }, - parserOptions: { - project: __dirname + "/tsconfig.json", - }, -}; diff --git a/apps/nextjs/jest.config.js b/apps/nextjs/jest.config.mjs similarity index 72% rename from apps/nextjs/jest.config.js rename to apps/nextjs/jest.config.mjs index cd0dc24f9..7898f8abb 100644 --- a/apps/nextjs/jest.config.js +++ b/apps/nextjs/jest.config.mjs @@ -1,5 +1,9 @@ -const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("./tsconfig.test.json"); +import { readFile } from "fs/promises"; +import { pathsToModuleNameMapper } from "ts-jest"; + +const tsconfig = JSON.parse( + await readFile(new URL("./tsconfig.test.json", import.meta.url)), +); /** @type {import('ts-jest').JestConfigWithTsJest} */ const config = { @@ -12,12 +16,12 @@ const config = { isolatedModules: true, }, ], - "^.+\\.svg$": "/jest.svgTransform.js", + "^.+\\.svg$": "/jest.svgTransform.mjs", "^.+\\.(css|scss|png|jpg|jpeg|gif|webp|avif)$": "jest-transform-stub", }, preset: "ts-jest/presets/default-esm", moduleNameMapper: { - ...pathsToModuleNameMapper(compilerOptions.paths, { + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, { prefix: "/src/", }), "^(\\.{1,2}/.*)\\.js$": "$1", @@ -30,7 +34,7 @@ const config = { moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], rootDir: ".", resetMocks: true, - setupFilesAfterEnv: ["/jest.setup.js"], + setupFilesAfterEnv: ["/jest.setup.cjs"], collectCoverageFrom: ["src/**/*.{ts,tsx,js,jsx}"], collectCoverage: process.env.CI === "true" || process.env.COLLECT_TEST_COVERAGE === "true", @@ -38,4 +42,4 @@ const config = { coverageDirectory: "coverage", }; -module.exports = config; +export default config; diff --git a/apps/nextjs/jest.setup.js b/apps/nextjs/jest.setup.cjs similarity index 100% rename from apps/nextjs/jest.setup.js rename to apps/nextjs/jest.setup.cjs diff --git a/apps/nextjs/jest.static.d.ts b/apps/nextjs/jest.static.d.ts index 95daa5e7f..ded555fbb 100644 --- a/apps/nextjs/jest.static.d.ts +++ b/apps/nextjs/jest.static.d.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ declare module "*.svg" { const content: any; export default content; diff --git a/apps/nextjs/jest.svgTransform.js b/apps/nextjs/jest.svgTransform.mjs similarity index 89% rename from apps/nextjs/jest.svgTransform.js rename to apps/nextjs/jest.svgTransform.mjs index 16506e1c5..685e8670c 100644 --- a/apps/nextjs/jest.svgTransform.js +++ b/apps/nextjs/jest.svgTransform.mjs @@ -1,4 +1,4 @@ -module.exports = { +export default { process() { console.log("Called jest svg transform"); return { code: "module.exports = {};" }; diff --git a/apps/nextjs/next.config.js b/apps/nextjs/next.config.js index 6f9dad15f..0dd375c38 100644 --- a/apps/nextjs/next.config.js +++ b/apps/nextjs/next.config.js @@ -1,9 +1,10 @@ +// This file should be in Common JS format to be compatible with Next.js const { getAppVersion, getReleaseStage, RELEASE_STAGE_PRODUCTION, RELEASE_STAGE_TESTING, -} = require("./scripts/build_config_helpers.js"); +} = require("./scripts/build_config_helpers.cjs"); const path = require("path"); const { PHASE_PRODUCTION_BUILD, PHASE_TEST } = require("next/constants"); diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index f78bce278..3547f6d63 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -1,26 +1,27 @@ { "name": "@oakai/nextjs", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "build": "next build", "build:dev": "pnpm with-env next build", "check": "tsc --noEmit", "clean": "rm -rf .next .turbo node_modules", - "dev": "FORCE_COLOR=1 SENTRY_SUPPRESS_TURBOPACK_WARNING=1 pnpm with-env node scripts/increase-listeners.js next dev --port 2525 --turbo | pino-pretty -C", - "dev:sentry": "FORCE_COLOR=1 pnpm with-env node scripts/increase-listeners.js next dev --port 2525 | pino-pretty -C", + "dev": "FORCE_COLOR=1 SENTRY_SUPPRESS_TURBOPACK_WARNING=1 pnpm with-env next dev --port 2525 --turbo | pino-pretty -C", + "dev:sentry": "FORCE_COLOR=1 pnpm with-env next dev --port 2525 | pino-pretty -C", "dev-trace-deprecation": "NODE_OPTIONS=\"--trace-deprecation\" next dev --port 2525 | pino-pretty -C", - "lint": "next lint", + "lint": "eslint .", "lint-fix": "next lint --fix", + "lint-debug": "eslint --debug .", "start": "next start", "type-check": "tsc --noEmit", - "test": "pnpm with-env jest --colors --config jest.config.js", - "test:seq": "pnpm with-env jest --colors --config jest.config.js --verbose --runInBand --no-cache", + "test": "pnpm with-env jest --colors --config jest.config.mjs", + "test:seq": "pnpm with-env jest --colors --config jest.config.mjs --verbose --runInBand --no-cache", "test-e2e": "pnpm with-env playwright test", "test-e2e-ui": "pnpm with-env playwright test --ui", "test-e2e-ui-serve": "pnpm build && pnpm start --port 4848 --turbo", "test-e2e-ui-built": "PORT=4848 pnpm with-env playwright test --ui", - "test-coverage": "COLLECT_TEST_COVERAGE=true pnpm with-env jest --colors --config jest.config.js --coverage", + "test-coverage": "COLLECT_TEST_COVERAGE=true pnpm with-env jest --colors --config jest.config.mjs --coverage", "with-env": "dotenv -e ../../.env --", "aila": "tsx scripts/aila-cli.ts", "storybook": "dotenv -e ../../.env -- storybook dev -p 6006 --no-open", @@ -43,14 +44,15 @@ "@oakai/api": "*", "@oakai/core": "*", "@oakai/db": "*", + "@oakai/eslint-config": "*", "@oakai/exports": "*", "@oakai/logger": "*", "@oakai/prettier-config": "*", "@oaknational/oak-components": "^1.50.0", "@oaknational/oak-consent-client": "^2.1.0", "@portabletext/react": "^3.1.0", - "@prisma/client": "5.16.1", - "@prisma/extension-accelerate": "^1.0.0", + "@prisma/client": "^5.16.1", + "@prisma/extension-accelerate": "^1.2.1", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-tooltip": "^1.0.7", @@ -141,8 +143,8 @@ "@storybook/test": "^8.4.1", "@tailwindcss/typography": "^0.5.10", "@types/file-saver": "^2.0.6", - "@types/jest": "^29.5.12", - "@types/node": "^18.17.0", + "@types/jest": "^29.5.14", + "@types/node": "^20.9.0", "@types/react": "^18.2.37", "@types/react-dom": "^18.2.15", "@types/styled-components": "^5.1.34", @@ -150,17 +152,14 @@ "avo": "^3.2.11", "concurrently": "^8.2.2", "dotenv-cli": "^6.0.0", - "eslint": "^8.56.0", - "eslint-config-next": "15.0.1", - "eslint-plugin-storybook": "^0.8.0", "graphql": "^16.9.0", "jest": "^29.7.0", "msw": "^2.6.5", "msw-storybook-addon": "^2.0.4", "postcss": "^8.4.32", "tailwindcss": "^3.3.7", - "ts-jest": "^29.1.4", - "typescript": "5.3.3", + "ts-jest": "^29.2.5", + "typescript": "5.7.2", "web-streams-polyfill": "^4.0.0" }, "engines": { @@ -171,5 +170,12 @@ "workerDirectory": [ ".storybook/public" ] + }, + "eslintConfig": { + "extends": "@oakai/eslint-config", + "parserOptions": { + "project": "./tsconfig.json" + }, + "rules": {} } } diff --git a/apps/nextjs/scripts/build_config_helpers.js b/apps/nextjs/scripts/build_config_helpers.cjs similarity index 99% rename from apps/nextjs/scripts/build_config_helpers.js rename to apps/nextjs/scripts/build_config_helpers.cjs index 4016a0c86..ca4a2b30f 100644 --- a/apps/nextjs/scripts/build_config_helpers.js +++ b/apps/nextjs/scripts/build_config_helpers.cjs @@ -7,7 +7,7 @@ const { existsSync, readFileSync } = require("fs"); * @returns {(string|null)} The SHA if found, or "no_git_state" if mid-merge, or `null` if it cannot be determined. */ function getLocalGitRef() { - if (existsSync("../../.git")) { + if (existsSync("../../.git/HEAD")) { const rev = readFileSync("../../.git/HEAD") .toString() .trim() diff --git a/apps/nextjs/scripts/increase-listeners.js b/apps/nextjs/scripts/increase-listeners.js deleted file mode 100644 index 7d9e0c25a..000000000 --- a/apps/nextjs/scripts/increase-listeners.js +++ /dev/null @@ -1,18 +0,0 @@ -/**** - Because we have a reasonably complex Next.js project now, - we're sometimes running into the default max listeners limit. - This script increases the limit to 20, which should be enough - so that we don't run into this issue. - - Potentially, if we decide to move to Turbopack for compilation - in local development, we could remove this script. - -***/ - -// Increase the limit of max listeners -require("events").EventEmitter.defaultMaxListeners = 20; - -// Run the original command -require("child_process").spawn(process.argv[2], process.argv.slice(3), { - stdio: "inherit", -}); diff --git a/apps/nextjs/scripts/preload-chat-routes.mjs b/apps/nextjs/scripts/preload-chat-routes.mjs index 382413a45..de599b985 100644 --- a/apps/nextjs/scripts/preload-chat-routes.mjs +++ b/apps/nextjs/scripts/preload-chat-routes.mjs @@ -75,6 +75,7 @@ const preBuildRoutes = async ( console.log("All routes pre-built successfully"); console.timeEnd(timerId); } catch (error) { + console.error(error); if (retryCount < maxRetries) { console.log( `Retrying pre-build (attempt ${retryCount + 1} of ${maxRetries})...`, diff --git a/apps/nextjs/src/ai-apps/lesson-planner/state/actions.ts b/apps/nextjs/src/ai-apps/lesson-planner/state/actions.ts index 16ef00a41..3b68c7c5f 100644 --- a/apps/nextjs/src/ai-apps/lesson-planner/state/actions.ts +++ b/apps/nextjs/src/ai-apps/lesson-planner/state/actions.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-duplicate-enum-values */ import type { RateLimitInfo } from "@oakai/api/src/types"; import type { KeyStageName, SubjectName } from "@oakai/core"; import type { diff --git a/apps/nextjs/src/app/api/chat/fixtures/FixtureRecordOpenAiClient.ts b/apps/nextjs/src/app/api/chat/fixtures/FixtureRecordOpenAiClient.ts index cb0d5d686..68d340390 100644 --- a/apps/nextjs/src/app/api/chat/fixtures/FixtureRecordOpenAiClient.ts +++ b/apps/nextjs/src/app/api/chat/fixtures/FixtureRecordOpenAiClient.ts @@ -3,7 +3,7 @@ import { createOpenAIClient } from "@oakai/core/src/llm/openai"; import { aiLogger } from "@oakai/logger"; import fs from "fs/promises"; import type OpenAI from "openai"; -import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources"; +import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/index.mjs"; const log = aiLogger("fixtures"); 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 06359a59c..4685bc77e 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/chat-message/index.tsx @@ -1,6 +1,6 @@ // Inspired by Chatbot-UI and modified to fit the needs of this project // @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx -import type { ReactNode } from "react"; +import type { ReactNode, JSX } from "react"; import { useState } from "react"; import type { MessagePart } from "@oakai/aila/src/protocol/jsonPatchProtocol"; @@ -200,7 +200,7 @@ function MessageWrapper({ function MessageTextWrapper({ children }: Readonly<{ children: ReactNode }>) { return ( -
+
{children}
); diff --git a/apps/nextjs/src/components/AppComponents/Chat/ui/textarea.tsx b/apps/nextjs/src/components/AppComponents/Chat/ui/textarea.tsx index e4eb3ebb2..4878582fd 100644 --- a/apps/nextjs/src/components/AppComponents/Chat/ui/textarea.tsx +++ b/apps/nextjs/src/components/AppComponents/Chat/ui/textarea.tsx @@ -2,23 +2,21 @@ import * as React from "react"; import { cn } from "@/lib/utils"; -interface TextareaProps - extends React.TextareaHTMLAttributes {} - -const Textarea = React.forwardRef( - ({ className, ...props }, ref) => { - return ( -