diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdbe10be6f..628548d0bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,6 +166,8 @@ jobs: SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE_RC }} SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE_RC }} APPZI_URL: ${{ secrets.APPZI_URL_RC }} + LEARN_AI_RECOMMENDATION_ENDPOINT: ${{ secrets.LEARN_AI_RECOMMENDATION_ENDPOINT_RC }} + LEARN_AI_SYLLABUS_ENDPOINT: ${{ secrets.LEARN_AI_SYLLABUS_ENDPOINT_RC }} VERSION: ${{ github.sha }} run: | docker build \ @@ -185,6 +187,8 @@ jobs: --build-arg NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$SENTRY_PROFILES_SAMPLE_RATE \ --build-arg NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE \ --build-arg NEXT_PUBLIC_APPZI_URL=$APPZI_URL \ + --build-arg NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$LEARN_AI_RECOMMENDATION_ENDPOINT \ + --build-arg NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$LEARN_AI_SYLLABUS_ENDPOINT \ --build-arg NEXT_PUBLIC_VERSION=$VERSION \ -t mitodl/mit-learn-frontend:$VERSION . diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 4ca9d32e83..549be20b07 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -58,6 +58,8 @@ jobs: SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE_PROD }} SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE_PROD }} APPZI_URL: ${{ secrets.APPZI_URL_PROD }} + LEARN_AI_RECOMMENDATION_ENDPOINT: ${{ secrets.LEARN_AI_RECOMMENDATION_ENDPOINT_PROD }} + LEARN_AI_SYLLABUS_ENDPOINT: ${{ secrets.LEARN_AI_SYLLABUS_ENDPOINT_PROD }} VERSION: ${{ github.sha }} run: | heroku container:push web \ @@ -79,6 +81,8 @@ jobs: NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$SENTRY_PROFILES_SAMPLE_RATE,\ NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE,\ NEXT_PUBLIC_APPZI_URL=$APPZI_URL,\ + NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$LEARN_AI_RECOMMENDATION_ENDPOINT,\ + NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$LEARN_AI_SYLLABUS_ENDPOINT,\ NEXT_PUBLIC_VERSION=$VERSION \ --context-path . diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 8983c24c0a..b01a589575 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -58,6 +58,8 @@ jobs: SENTRY_PROFILES_SAMPLE_RATE: ${{ secrets.SENTRY_PROFILES_SAMPLE_RATE_RC }} SENTRY_TRACES_SAMPLE_RATE: ${{ secrets.SENTRY_TRACES_SAMPLE_RATE_RC }} APPZI_URL: ${{ secrets.APPZI_URL_RC }} + LEARN_AI_RECOMMENDATION_ENDPOINT: ${{ secrets.LEARN_AI_RECOMMENDATION_ENDPOINT_RC }} + LEARN_AI_SYLLABUS_ENDPOINT: ${{ secrets.LEARN_AI_SYLLABUS_ENDPOINT_RC }} VERSION: ${{ github.sha }} run: | heroku container:push web \ @@ -79,6 +81,8 @@ jobs: NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$SENTRY_PROFILES_SAMPLE_RATE,\ NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE,\ NEXT_PUBLIC_APPZI_URL=$APPZI_URL,\ + NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$LEARN_AI_RECOMMENDATION_ENDPOINT,\ + NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$LEARN_AI_SYLLABUS_ENDPOINT,\ NEXT_PUBLIC_VERSION=$VERSION \ --context-path . diff --git a/RELEASE.rst b/RELEASE.rst index b279ef2b86..77b0575d4c 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,24 @@ Release Notes ============= +Version 0.30.5 +-------------- + +- Fixed SCIM search for large queries (#2049) +- Latest smoot-design (#2059) +- Recommendation bot styling and text updates (#2058) +- Open Learning Library CSV update (#2014) +- Update dependency litellm to v1.61.5 (#2056) +- Update opensearchproject/opensearch Docker tag to v2.19.0 (#2055) +- Update dependency tiktoken to ^0.9.0 (#2054) +- Update dependency litellm to v1.61.4 (#2053) +- Update Node.js to v22.14.0 (#2052) +- Celery task to embed new contentfiles (#2044) +- Design update for the homepage AskTIM action (#2047) +- Point the chatbots to the Learn AI endpoints (#2045) +- Recommendation bot UI (#2019) +- Update React Query to v5 (#2043) + Version 0.30.4 (Released February 12, 2025) -------------- diff --git a/docker-compose.apps.yml b/docker-compose.apps.yml index 8421517e13..91864b1b0b 100644 --- a/docker-compose.apps.yml +++ b/docker-compose.apps.yml @@ -30,7 +30,7 @@ services: profiles: - frontend working_dir: /src - image: node:22.13 + image: node:22.14 entrypoint: ["/bin/sh", "-c"] command: - | diff --git a/docker-compose.opensearch.base.yml b/docker-compose.opensearch.base.yml index f722ca7a47..15ef3a0e4c 100644 --- a/docker-compose.opensearch.base.yml +++ b/docker-compose.opensearch.base.yml @@ -1,6 +1,6 @@ services: opensearch: - image: opensearchproject/opensearch:2.18.0 + image: opensearchproject/opensearch:2.19.0 environment: - "cluster.name=opensearch-cluster" - "bootstrap.memory_lock=true" # along with the memlock settings below, disables swapping diff --git a/env/frontend.env b/env/frontend.env index dac69ec966..4efd9adbf8 100644 --- a/env/frontend.env +++ b/env/frontend.env @@ -18,3 +18,6 @@ NEXT_PUBLIC_DEFAULT_SEARCH_SLOP=6 NEXT_PUBLIC_DEFAULT_SEARCH_STALENESS_PENALTY=2.5 NEXT_PUBLIC_DEFAULT_SEARCH_MINIMUM_SCORE_CUTOFF=0 NEXT_PUBLIC_DEFAULT_SEARCH_MAX_INCOMPLETENESS_PENALTY=90 + +NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=https://api-learn-ai-qa.ol.mit.edu/http/recommendation_agent/ +NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=https://api-learn-ai-qa.ol.mit.edu/http/syllabus_agent/ diff --git a/frontends/api/package.json b/frontends/api/package.json index 71f50bda02..a0f8d37bd3 100644 --- a/frontends/api/package.json +++ b/frontends/api/package.json @@ -27,7 +27,7 @@ "ol-test-utilities": "0.0.0" }, "dependencies": { - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.66.0", "axios": "^1.6.3" } } diff --git a/frontends/api/src/hooks/articles/index.test.ts b/frontends/api/src/hooks/articles/index.test.ts index a1068278a1..a186aa30e9 100644 --- a/frontends/api/src/hooks/articles/index.test.ts +++ b/frontends/api/src/hooks/articles/index.test.ts @@ -71,9 +71,9 @@ describe("Article CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("post", url, requestData) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith( - articleKeys.listRoot(), - ) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: articleKeys.listRoot(), + }) }) test("useArticlePartialUpdate calls correct API", async () => { @@ -89,7 +89,9 @@ describe("Article CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) const { id, ...patchData } = article expect(makeRequest).toHaveBeenCalledWith("patch", url, patchData) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith(articleKeys.root) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: articleKeys.root, + }) }) test("useArticleDestroy calls correct API", async () => { @@ -104,8 +106,8 @@ describe("Article CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith( - articleKeys.listRoot(), - ) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: articleKeys.listRoot(), + }) }) }) diff --git a/frontends/api/src/hooks/articles/index.ts b/frontends/api/src/hooks/articles/index.ts index ce366ed0c1..dadf2bfda3 100644 --- a/frontends/api/src/hooks/articles/index.ts +++ b/frontends/api/src/hooks/articles/index.ts @@ -1,9 +1,4 @@ -import { - UseQueryOptions, - useMutation, - useQuery, - useQueryClient, -} from "@tanstack/react-query" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { articlesApi } from "../../clients" import type { @@ -14,7 +9,7 @@ import { articleQueries, articleKeys } from "./queries" const useArticleList = ( params: ArticleListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...articleQueries.list(params), @@ -40,7 +35,7 @@ const useArticleCreate = () => { .articlesCreate({ ArticleRequest: data }) .then((response) => response.data), onSuccess: () => { - client.invalidateQueries(articleKeys.listRoot()) + client.invalidateQueries({ queryKey: articleKeys.listRoot() }) }, }) } @@ -49,7 +44,7 @@ const useArticleDestroy = () => { return useMutation({ mutationFn: (id: number) => articlesApi.articlesDestroy({ id }), onSuccess: () => { - client.invalidateQueries(articleKeys.listRoot()) + client.invalidateQueries({ queryKey: articleKeys.listRoot() }) }, }) } @@ -64,7 +59,7 @@ const useArticlePartialUpdate = () => { }) .then((response) => response.data), onSuccess: (_data) => { - client.invalidateQueries(articleKeys.root) + client.invalidateQueries({ queryKey: articleKeys.root }) }, }) } diff --git a/frontends/api/src/hooks/articles/queries.ts b/frontends/api/src/hooks/articles/queries.ts index 6d982c60cb..c045dacaff 100644 --- a/frontends/api/src/hooks/articles/queries.ts +++ b/frontends/api/src/hooks/articles/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { articlesApi } from "../../clients" import type { ArticlesApiArticlesListRequest as ArticleListRequest } from "../../generated/v1" @@ -12,16 +12,16 @@ const articleKeys = { const articleQueries = { list: (params: ArticleListRequest) => - ({ + queryOptions({ queryKey: articleKeys.list(params), queryFn: () => articlesApi.articlesList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: articleKeys.detail(id), queryFn: () => articlesApi.articlesRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), } export { articleQueries, articleKeys } diff --git a/frontends/api/src/hooks/channels/index.ts b/frontends/api/src/hooks/channels/index.ts index 87a2293a13..ad09842b0b 100644 --- a/frontends/api/src/hooks/channels/index.ts +++ b/frontends/api/src/hooks/channels/index.ts @@ -1,9 +1,4 @@ -import { - UseQueryOptions, - useMutation, - useQuery, - useQueryClient, -} from "@tanstack/react-query" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { channelsApi } from "../../clients" import type { @@ -14,7 +9,7 @@ import { channelKeys, channelQueries } from "./queries" const useChannelsList = ( params: ChannelsApiChannelsListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...channelQueries.list(params), @@ -45,7 +40,7 @@ const useChannelPartialUpdate = () => { }) .then((response) => response.data), onSuccess: (_data) => { - client.invalidateQueries(channelKeys.root) + client.invalidateQueries({ queryKey: channelKeys.root }) }, }) } diff --git a/frontends/api/src/hooks/channels/queries.ts b/frontends/api/src/hooks/channels/queries.ts index 3dd8835c59..f72bcef70a 100644 --- a/frontends/api/src/hooks/channels/queries.ts +++ b/frontends/api/src/hooks/channels/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { channelsApi } from "../../clients" import type { ChannelsApiChannelsListRequest as FieldsApiListRequest } from "../../generated/v0" @@ -22,34 +22,34 @@ const channelKeys = { const channelQueries = { list: (params: FieldsApiListRequest) => - ({ + queryOptions({ queryKey: channelKeys.list(params), queryFn: () => channelsApi.channelsList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: channelKeys.detail(id), queryFn: () => channelsApi.channelsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), detailByType: (channelType: string, name: string) => - ({ + queryOptions({ queryKey: channelKeys.detailByType(channelType, name), queryFn: () => { return channelsApi .channelsTypeRetrieve({ channel_type: channelType, name: name }) .then((res) => res.data) }, - }) satisfies QueryOptions, + }), countsByType: (channelType: string) => - ({ + queryOptions({ queryKey: channelKeys.countsByType(channelType), queryFn: () => { return channelsApi .channelsCountsList({ channel_type: channelType }) .then((res) => res.data) }, - }) satisfies QueryOptions, + }), } export { channelQueries, channelKeys } diff --git a/frontends/api/src/hooks/learningPaths/index.test.ts b/frontends/api/src/hooks/learningPaths/index.test.ts index 782e9fd9b3..61b7274ec4 100644 --- a/frontends/api/src/hooks/learningPaths/index.test.ts +++ b/frontends/api/src/hooks/learningPaths/index.test.ts @@ -157,10 +157,9 @@ describe("LearningPath CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("post", url, requestData) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ - "learningPaths", - "list", - ]) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["learningPaths", "list"], + }) }) test("useLearningPathDestroy calls correct API", async () => { @@ -178,14 +177,12 @@ describe("LearningPath CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("delete", url, undefined) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ - "learningPaths", - "list", - ]) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ - "learningPaths", - "membershipList", - ]) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["learningPaths", "list"], + }) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["learningPaths", "membershipList"], + }) }) test("useLearningPathUpdate calls correct API", async () => { @@ -203,14 +200,11 @@ describe("LearningPath CRUD", () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(makeRequest).toHaveBeenCalledWith("patch", url, patch) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ - "learningPaths", - "list", - ]) - expect(queryClient.invalidateQueries).toHaveBeenCalledWith([ - "learningPaths", - "detail", - path.id, - ]) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["learningPaths", "list"], + }) + expect(queryClient.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ["learningPaths", "detail", path.id], + }) }) }) diff --git a/frontends/api/src/hooks/learningPaths/index.ts b/frontends/api/src/hooks/learningPaths/index.ts index 9772274d3b..08210ebea6 100644 --- a/frontends/api/src/hooks/learningPaths/index.ts +++ b/frontends/api/src/hooks/learningPaths/index.ts @@ -1,5 +1,4 @@ import { - UseQueryOptions, useQuery, useInfiniteQuery, useQueryClient, @@ -18,7 +17,7 @@ import { useUserHasPermission, Permission } from "api/hooks/user" const useLearningPathsList = ( params: ListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...learningPathQueries.list(params), @@ -28,15 +27,11 @@ const useLearningPathsList = ( const useInfiniteLearningPathItems = ( params: ItemsListRequest, - options: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useInfiniteQuery({ ...learningPathQueries.infiniteItems(params.learning_resource_id, params), - // TODO: in v5, co-locate this with the query options via infiniteQueryOptions - getNextPageParam: (lastPage) => { - return lastPage.next ?? undefined - }, - ...options, + ...opts, }) } @@ -56,7 +51,7 @@ const useLearningPathCreate = () => { LearningPathResourceRequest: params, }), onSettled: () => { - queryClient.invalidateQueries(learningPathKeys.listRoot()) + queryClient.invalidateQueries({ queryKey: learningPathKeys.listRoot() }) }, }) } @@ -72,8 +67,10 @@ const useLearningPathUpdate = () => { PatchedLearningPathResourceRequest: params, }), onSettled: (data, err, vars) => { - queryClient.invalidateQueries(learningPathKeys.listRoot()) - queryClient.invalidateQueries(learningPathKeys.detail(vars.id)) + queryClient.invalidateQueries({ queryKey: learningPathKeys.listRoot() }) + queryClient.invalidateQueries({ + queryKey: learningPathKeys.detail(vars.id), + }) }, }) } @@ -84,8 +81,10 @@ const useLearningPathDestroy = () => { mutationFn: (params: DestroyRequest) => learningPathsApi.learningpathsDestroy(params), onSettled: () => { - queryClient.invalidateQueries(learningPathKeys.listRoot()) - queryClient.invalidateQueries(learningPathKeys.membershipList()) + queryClient.invalidateQueries({ queryKey: learningPathKeys.listRoot() }) + queryClient.invalidateQueries({ + queryKey: learningPathKeys.membershipList(), + }) }, }) } @@ -107,9 +106,9 @@ const useLearningPathListItemMove = () => { }) }, onSettled: (_data, _err, vars) => { - queryClient.invalidateQueries( - learningPathKeys.infiniteItemsRoot(vars.parent), - ) + queryClient.invalidateQueries({ + queryKey: learningPathKeys.infiniteItemsRoot(vars.parent), + }) }, }) } diff --git a/frontends/api/src/hooks/learningPaths/queries.ts b/frontends/api/src/hooks/learningPaths/queries.ts index 84cf682a0d..f70fd76727 100644 --- a/frontends/api/src/hooks/learningPaths/queries.ts +++ b/frontends/api/src/hooks/learningPaths/queries.ts @@ -6,7 +6,7 @@ import type { PaginatedLearningPathRelationshipList, } from "../../generated/v1" import { clearListMemberships } from "../learningResources/queries" -import { QueryOptions, UseInfiniteQueryOptions } from "@tanstack/react-query" +import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query" const learningPathKeys = { root: ["learningPaths"], @@ -24,21 +24,21 @@ const learningPathKeys = { const learningPathQueries = { list: (params: ListRequest) => - ({ + queryOptions({ queryKey: learningPathKeys.list(params), queryFn: () => learningPathsApi.learningpathsList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: learningPathKeys.detail(id), queryFn: () => learningPathsApi.learningpathsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), infiniteItems: (id: number, listingParams: ItemsListRequest) => - ({ + infiniteQueryOptions({ queryKey: learningPathKeys.infiniteItems(id, listingParams), - queryFn: async ({ pageParam }: { pageParam?: string } = {}) => { + queryFn: async ({ pageParam }) => { // Use generated API for first request, then use next parameter const request = pageParam ? axiosInstance.request({ @@ -55,13 +55,18 @@ const learningPathQueries = { })), } }, - }) satisfies UseInfiniteQueryOptions, + // Casting is so infiniteQueryOptions can infer the correct type for initialPageParam + initialPageParam: null as string | null, + getNextPageParam: (lastPage) => { + return lastPage.next ?? undefined + }, + }), membershipList: () => - ({ + queryOptions({ queryKey: learningPathKeys.membershipList(), queryFn: () => learningPathsApi.learningpathsMembershipList().then((res) => res.data), - }) satisfies QueryOptions, + }), } export { learningPathQueries, learningPathKeys } diff --git a/frontends/api/src/hooks/learningResources/index.ts b/frontends/api/src/hooks/learningResources/index.ts index b2edc21653..b8dad9135a 100644 --- a/frontends/api/src/hooks/learningResources/index.ts +++ b/frontends/api/src/hooks/learningResources/index.ts @@ -1,5 +1,5 @@ import { - UseQueryOptions, + keepPreviousData, useMutation, useQuery, useQueryClient, @@ -30,7 +30,7 @@ import { useCallback } from "react" const useLearningResourcesList = ( params: LRListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...learningResourceQueries.list(params), @@ -61,10 +61,7 @@ const useFeaturedLearningResourcesList = (params: FeaturedListParams = {}) => { return useQuery(learningResourceQueries.featured(params)) } -const useLearningResourceTopic = ( - id: number, - opts: Pick = {}, -) => { +const useLearningResourceTopic = (id: number, opts?: { enabled?: boolean }) => { return useQuery({ ...topicQueries.detail(id), ...opts, @@ -73,7 +70,7 @@ const useLearningResourceTopic = ( const useLearningResourceTopics = ( params: TopicsListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...topicQueries.list(params), @@ -83,11 +80,13 @@ const useLearningResourceTopics = ( const useLearningResourcesSearch = ( params: LRSearchRequest, - opts?: Pick, + opts?: { + keepPreviousData?: boolean + }, ) => { return useQuery({ ...learningResourceQueries.search(params), - ...opts, + placeholderData: opts?.keepPreviousData ? keepPreviousData : undefined, }) } @@ -98,7 +97,7 @@ const useLearningResourceSetUserListRelationships = () => { params: LearningResourcesApiLearningResourcesUserlistsPartialUpdateRequest, ) => learningResourcesApi.learningResourcesUserlistsPartialUpdate(params), onSettled: () => { - queryClient.invalidateQueries(userlistKeys.membershipList()) + queryClient.invalidateQueries({ queryKey: userlistKeys.membershipList() }) }, }) } @@ -111,14 +110,16 @@ const useLearningResourceSetLearningPathRelationships = () => { ) => learningResourcesApi.learningResourcesLearningPathsPartialUpdate(params), onSettled: () => { - queryClient.invalidateQueries(learningPathKeys.membershipList()) + queryClient.invalidateQueries({ + queryKey: learningPathKeys.membershipList(), + }) }, }) } const useOfferorsList = ( params: OfferorsApiOfferorsListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...offerorQueries.list(params), @@ -128,7 +129,7 @@ const useOfferorsList = ( const usePlatformsList = ( params: PlatformsApiPlatformsListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...platformsQueries.list(params), @@ -142,7 +143,7 @@ const useSchoolsList = () => { const useSimilarLearningResources = ( id: number, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...learningResourceQueries.similar(id), @@ -152,7 +153,7 @@ const useSimilarLearningResources = ( const useVectorSimilarLearningResources = ( id: number, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...learningResourceQueries.vectorSimilar(id), diff --git a/frontends/api/src/hooks/learningResources/queries.ts b/frontends/api/src/hooks/learningResources/queries.ts index a2e0c4a2b5..5cde01af0f 100644 --- a/frontends/api/src/hooks/learningResources/queries.ts +++ b/frontends/api/src/hooks/learningResources/queries.ts @@ -7,7 +7,7 @@ import { schoolsApi, featuredApi, } from "../../clients" -import axiosInstance from "../../axios" + import type { LearningResource, LearningResourcesApiLearningResourcesListRequest as LearningResourcesListRequest, @@ -17,9 +17,9 @@ import type { PlatformsApiPlatformsListRequest, FeaturedApiFeaturedListRequest as FeaturedListParams, LearningResourcesApiLearningResourcesItemsListRequest as ItemsListRequest, - PaginatedLearningResourceRelationshipList, + LearningResourcesSearchResponse, } from "../../generated/v1" -import type { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" /* List memberships were previously determined in the learningResourcesApi * from user_list_parents and learning_path_parents on each resource. @@ -29,7 +29,9 @@ import type { QueryOptions } from "@tanstack/react-query" * Removing here to ensure they are not depended on anywhere, though they can * be removed from the GET APIs TODO. */ -export const clearListMemberships = (resource: LearningResource) => ({ +export const clearListMemberships = ( + resource: LearningResource, +): LearningResource => ({ ...resource, user_list_parents: [], learning_path_parents: [], @@ -133,60 +135,51 @@ const offerorsKeys = { const learningResourceQueries = { detail: (id: number) => - ({ + queryOptions({ queryKey: learningResourceKeys.detail(id), queryFn: () => learningResourcesApi .learningResourcesRetrieve({ id }) .then((res) => clearListMemberships(res.data)), - }) satisfies QueryOptions, - items: (id: number, params: ItemsListRequest) => { - return { + }), + items: (id: number, params: ItemsListRequest) => + queryOptions({ queryKey: learningResourceKeys.items(id, params), - queryFn: ({ pageParam }: { pageParam?: string } = {}) => { - // Use generated API for first request, then use next parameter - const request = pageParam - ? axiosInstance.request({ - method: "get", - url: pageParam, - }) - : learningResourcesApi.learningResourcesItemsList(params) - return request.then((res) => - res.data.results.map((rel) => clearListMemberships(rel.resource)), - ) + queryFn: () => { + return learningResourcesApi + .learningResourcesItemsList(params) + .then((res) => + res.data.results.map((rel) => clearListMemberships(rel.resource)), + ) }, - } satisfies QueryOptions - }, - similar: (id: number) => { - return { + }), + similar: (id: number) => + queryOptions({ queryKey: learningResourceKeys.similar(id), queryFn: () => learningResourcesApi .learningResourcesSimilarList({ id }) .then((res) => res.data.map(clearListMemberships)), - } satisfies QueryOptions - }, - vectorSimilar: (id: number) => { - return { + }), + vectorSimilar: (id: number) => + queryOptions({ queryKey: learningResourceKeys.vectorSimilar(id), queryFn: () => learningResourcesApi .learningResourcesVectorSimilarList({ id }) .then((res) => res.data.map(clearListMemberships)), - } satisfies QueryOptions - }, - list: (params: LearningResourcesListRequest) => { - return { + }), + list: (params: LearningResourcesListRequest) => + queryOptions({ queryKey: learningResourceKeys.list(params), queryFn: () => learningResourcesApi.learningResourcesList(params).then((res) => ({ ...res.data, results: res.data.results.map(clearListMemberships), })), - } satisfies QueryOptions - }, - featured: (params: FeaturedListParams = {}) => { - return { + }), + featured: (params: FeaturedListParams = {}) => + queryOptions({ queryKey: learningResourceKeys.featured(params), queryFn: () => featuredApi.featuredList(params).then((res) => { @@ -197,62 +190,55 @@ const learningResourceQueries = { ), } }), - } satisfies QueryOptions - }, - search: (params: LearningResourcesSearchRetrieveRequest) => { - return { + }), + search: (params: LearningResourcesSearchRetrieveRequest) => + queryOptions({ queryKey: learningResourceKeys.search(params), queryFn: () => - learningResourcesSearchApi - .learningResourcesSearchRetrieve(params) - .then((res) => ({ - ...res.data, - results: res.data.results.map(clearListMemberships), - })), - } satisfies QueryOptions - }, + learningResourcesSearchApi.learningResourcesSearchRetrieve(params).then( + (res) => + ({ + ...res.data, + results: res.data.results.map(clearListMemberships), + }) as LearningResourcesSearchResponse, + ), + }), } const topicQueries = { - list: (params: TopicsListRequest) => { - return { + list: (params: TopicsListRequest) => + queryOptions({ queryKey: topicsKeys.list(params), queryFn: () => topicsApi.topicsList(params).then((res) => res.data), - } satisfies QueryOptions - }, - detail: (id: number) => { - return { + }), + detail: (id: number) => + queryOptions({ queryKey: topicsKeys.detail(id), queryFn: () => topicsApi.topicsRetrieve({ id }).then((res) => res.data), - } satisfies QueryOptions - }, + }), } const platformsQueries = { - list: (params: PlatformsApiPlatformsListRequest) => { - return { - queryKey: platformsKeys.list(params), - queryFn: () => platformsApi.platformsList(params).then((res) => res.data), - } satisfies QueryOptions - }, + list: (params: PlatformsApiPlatformsListRequest) => ({ + queryKey: platformsKeys.list(params), + queryFn: () => platformsApi.platformsList(params).then((res) => res.data), + }), } const schoolQueries = { - list: () => { - return { + list: () => + queryOptions({ queryKey: schoolKeys.list(), queryFn: () => schoolsApi.schoolsList().then((res) => res.data), - } satisfies QueryOptions - }, + }), } const offerorQueries = { - list: (params: OfferorsApiOfferorsListRequest) => { - return { + list: (params: OfferorsApiOfferorsListRequest) => + queryOptions({ queryKey: offerorsKeys.list(params), queryFn: () => offerorsApi.offerorsList(params).then((res) => res.data), - } satisfies QueryOptions - }, + }), } export { diff --git a/frontends/api/src/hooks/newsEvents/queries.ts b/frontends/api/src/hooks/newsEvents/queries.ts index fd6ddd108b..61b0d5a46d 100644 --- a/frontends/api/src/hooks/newsEvents/queries.ts +++ b/frontends/api/src/hooks/newsEvents/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { newsEventsApi } from "../../clients" import type { NewsEventsApiNewsEventsListRequest } from "../../generated/v0" @@ -15,17 +15,17 @@ const newsEventsKeys = { const newsEventsQueries = { list: (params: NewsEventsApiNewsEventsListRequest) => - ({ + queryOptions({ queryKey: newsEventsKeys.list(params), queryFn: () => newsEventsApi.newsEventsList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: newsEventsKeys.detail(id), queryFn: () => newsEventsApi.newsEventsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), } export { newsEventsQueries, newsEventsKeys } diff --git a/frontends/api/src/hooks/programLetters/queries.ts b/frontends/api/src/hooks/programLetters/queries.ts index 0606045189..3e1e044b21 100644 --- a/frontends/api/src/hooks/programLetters/queries.ts +++ b/frontends/api/src/hooks/programLetters/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { programLettersApi } from "../../clients" const programLetterKeys = { @@ -8,13 +8,13 @@ const programLetterKeys = { } const programLetterQueries = { detail: (id: string) => - ({ + queryOptions({ queryKey: programLetterKeys.detail(id), queryFn: () => programLettersApi .programLettersRetrieve({ id }) .then((res) => res.data), - }) satisfies QueryOptions, + }), } export { programLetterQueries, programLetterKeys } diff --git a/frontends/api/src/hooks/searchSubscription/index.ts b/frontends/api/src/hooks/searchSubscription/index.ts index 41ab452940..bc334e84b9 100644 --- a/frontends/api/src/hooks/searchSubscription/index.ts +++ b/frontends/api/src/hooks/searchSubscription/index.ts @@ -1,9 +1,4 @@ -import { - useMutation, - UseQueryOptions, - useQueryClient, - useQuery, -} from "@tanstack/react-query" +import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query" import { searchSubscriptionQueries, searchSubscriptionKeys } from "./keyFactory" import type { LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionSubscribeCreateRequest as subscriptionCreateRequest } from "../../generated/v1" import { searchSubscriptionApi } from "../../clients" @@ -17,14 +12,14 @@ const useSearchSubscriptionCreate = () => { .learningResourcesUserSubscriptionSubscribeCreate(params) .then((res) => res.data), onSuccess: (_data) => { - queryClient.invalidateQueries(searchSubscriptionKeys.root) + queryClient.invalidateQueries({ queryKey: searchSubscriptionKeys.root }) }, }) } const useSearchSubscriptionList = ( params = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...searchSubscriptionQueries.list(params), @@ -42,7 +37,7 @@ const useSearchSubscriptionDelete = () => { .then((res) => res.data) }, onSuccess: (_data) => { - queryClient.invalidateQueries(searchSubscriptionKeys.root) + queryClient.invalidateQueries({ queryKey: searchSubscriptionKeys.root }) }, }) } diff --git a/frontends/api/src/hooks/searchSubscription/keyFactory.ts b/frontends/api/src/hooks/searchSubscription/keyFactory.ts index 9ecc571a2a..4e5c4b492f 100644 --- a/frontends/api/src/hooks/searchSubscription/keyFactory.ts +++ b/frontends/api/src/hooks/searchSubscription/keyFactory.ts @@ -1,6 +1,6 @@ import { searchSubscriptionApi } from "../../clients" import type { LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionCheckListRequest as subscriptionCheckListRequest } from "../../generated/v1" -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" const searchSubscriptionKeys = { root: ["searchSubscriptions"], @@ -13,13 +13,13 @@ const searchSubscriptionKeys = { const searchSubscriptionQueries = { list: (params: subscriptionCheckListRequest) => - ({ + queryOptions({ queryKey: searchSubscriptionKeys.list(params), queryFn: () => searchSubscriptionApi .learningResourcesUserSubscriptionCheckList(params) .then((res) => res.data), - }) satisfies QueryOptions, + }), } export { searchSubscriptionQueries, searchSubscriptionKeys } diff --git a/frontends/api/src/hooks/testimonials/index.ts b/frontends/api/src/hooks/testimonials/index.ts index eb1759bbb0..aabcc52f6a 100644 --- a/frontends/api/src/hooks/testimonials/index.ts +++ b/frontends/api/src/hooks/testimonials/index.ts @@ -1,11 +1,11 @@ -import { UseQueryOptions, useQuery } from "@tanstack/react-query" +import { useQuery } from "@tanstack/react-query" import type { TestimonialsApiTestimonialsListRequest } from "../../generated/v0" import { testimonialsQueries } from "./queries" const useTestimonialList = ( params: TestimonialsApiTestimonialsListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...testimonialsQueries.list(params), diff --git a/frontends/api/src/hooks/testimonials/queries.ts b/frontends/api/src/hooks/testimonials/queries.ts index 4fa8fac21c..24dc7fe7c3 100644 --- a/frontends/api/src/hooks/testimonials/queries.ts +++ b/frontends/api/src/hooks/testimonials/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { testimonialsApi } from "../../clients" import type { TestimonialsApiTestimonialsListRequest as TestimonialsListRequest } from "../../generated/v0" @@ -15,17 +15,17 @@ const testimonialKeys = { const testimonialsQueries = { list: (params: TestimonialsListRequest) => - ({ + queryOptions({ queryKey: testimonialKeys.list(params), queryFn: () => testimonialsApi.testimonialsList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: testimonialKeys.detail(id), queryFn: () => testimonialsApi.testimonialsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), } export { testimonialsQueries, testimonialKeys } diff --git a/frontends/api/src/hooks/userLists/index.ts b/frontends/api/src/hooks/userLists/index.ts index 96cdcc1b37..83c29f24d5 100644 --- a/frontends/api/src/hooks/userLists/index.ts +++ b/frontends/api/src/hooks/userLists/index.ts @@ -1,5 +1,4 @@ import { - UseQueryOptions, useInfiniteQuery, useMutation, useQuery, @@ -18,7 +17,7 @@ import { useUserIsAuthenticated } from "api/hooks/user" const useUserListList = ( params: ListRequest = {}, - opts: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useQuery({ ...userlistQueries.list(params), @@ -38,7 +37,7 @@ const useUserListCreate = () => { UserListRequest: params, }), onSettled: () => { - queryClient.invalidateQueries(userlistKeys.listRoot()) + queryClient.invalidateQueries({ queryKey: userlistKeys.listRoot() }) }, }) } @@ -51,8 +50,8 @@ const useUserListUpdate = () => { PatchedUserListRequest: params, }), onSettled: (_data, _err, vars) => { - queryClient.invalidateQueries(userlistKeys.listRoot()) - queryClient.invalidateQueries(userlistKeys.detail(vars.id)) + queryClient.invalidateQueries({ queryKey: userlistKeys.listRoot() }) + queryClient.invalidateQueries({ queryKey: userlistKeys.detail(vars.id) }) }, }) } @@ -63,22 +62,19 @@ const useUserListDestroy = () => { mutationFn: (params: DestroyRequest) => userListsApi.userlistsDestroy(params), onSettled: () => { - queryClient.invalidateQueries(userlistKeys.listRoot()) - queryClient.invalidateQueries(userlistKeys.membershipList()) + queryClient.invalidateQueries({ queryKey: userlistKeys.listRoot() }) + queryClient.invalidateQueries({ queryKey: userlistKeys.membershipList() }) }, }) } const useInfiniteUserListItems = ( params: ItemsListRequest, - options: Pick = {}, + opts?: { enabled?: boolean }, ) => { return useInfiniteQuery({ ...userlistQueries.infiniteItems(params.userlist_id, params), - getNextPageParam: (lastPage) => { - return lastPage.next ?? undefined - }, - ...options, + ...opts, }) } @@ -98,7 +94,9 @@ const useUserListListItemMove = () => { }) }, onSettled: (_data, _err, vars) => { - queryClient.invalidateQueries(userlistKeys.infiniteItemsRoot(vars.parent)) + queryClient.invalidateQueries({ + queryKey: userlistKeys.infiniteItemsRoot(vars.parent), + }) }, }) } diff --git a/frontends/api/src/hooks/userLists/queries.ts b/frontends/api/src/hooks/userLists/queries.ts index 21c1662a34..7d801fb993 100644 --- a/frontends/api/src/hooks/userLists/queries.ts +++ b/frontends/api/src/hooks/userLists/queries.ts @@ -6,7 +6,7 @@ import type { } from "../../generated/v1" import { userListsApi } from "../../clients" import { clearListMemberships } from "../learningResources/queries" -import { QueryOptions } from "@tanstack/react-query" +import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query" const userlistKeys = { root: ["userLists"], @@ -24,42 +24,49 @@ const userlistKeys = { const userlistQueries = { list: (params: ListRequest) => - ({ + queryOptions({ queryKey: userlistKeys.list(params), queryFn: () => userListsApi.userlistsList(params).then((res) => res.data), - }) satisfies QueryOptions, + }), detail: (id: number) => - ({ + queryOptions({ queryKey: userlistKeys.detail(id), queryFn: () => userListsApi.userlistsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, - infiniteItems: (id: number, listingParams: ItemsListRequest) => ({ - queryKey: userlistKeys.infiniteItems(id, listingParams), - queryFn: async ({ pageParam }: { pageParam?: string } = {}) => { - // Use generated API for first request, then use next parameter - const request = pageParam - ? axiosInstance.request({ - method: "get", - url: pageParam, - }) - : userListsApi.userlistsItemsList(listingParams) - const { data } = await request - return { - ...data, - results: data.results.map((relation) => ({ - ...relation, - resource: clearListMemberships(relation.resource), - })), - } - }, + }), + infiniteItems: (id: number, listingParams: ItemsListRequest) => + infiniteQueryOptions({ + queryKey: userlistKeys.infiniteItems(id, listingParams), + queryFn: async ({ + pageParam, + }): Promise => { + // Use generated API for first request, then use next parameter + const request = pageParam + ? axiosInstance.request({ + method: "get", + url: pageParam, + }) + : userListsApi.userlistsItemsList(listingParams) + const { data } = await request + return { + ...data, + results: data.results.map((relation) => ({ + ...relation, + resource: clearListMemberships(relation.resource), + })), + } + }, + // Casting is so infiniteQueryOptions can infer the correct type for initialPageParam + initialPageParam: null as string | null, + getNextPageParam: (lastPage) => { + return lastPage.next ?? undefined + }, + }), + membershipList: () => ({ + queryKey: userlistKeys.membershipList(), + queryFn: () => + userListsApi.userlistsMembershipList().then((res) => res.data), }), - membershipList: () => - ({ - queryKey: userlistKeys.membershipList(), - queryFn: () => - userListsApi.userlistsMembershipList().then((res) => res.data), - }) satisfies QueryOptions, } export { userlistQueries, userlistKeys } diff --git a/frontends/api/src/hooks/widget_lists/index.ts b/frontends/api/src/hooks/widget_lists/index.ts index fff8f1fe0f..121d9e81ea 100644 --- a/frontends/api/src/hooks/widget_lists/index.ts +++ b/frontends/api/src/hooks/widget_lists/index.ts @@ -26,7 +26,7 @@ const useMutateWidgetsList = (id: number) => { .then((response) => response.data), onSuccess: (_data) => { - client.invalidateQueries(widgetListKeys.root) + client.invalidateQueries({ queryKey: widgetListKeys.root }) }, }) } diff --git a/frontends/api/src/hooks/widget_lists/queries.ts b/frontends/api/src/hooks/widget_lists/queries.ts index 98f431b29c..211c7c2139 100644 --- a/frontends/api/src/hooks/widget_lists/queries.ts +++ b/frontends/api/src/hooks/widget_lists/queries.ts @@ -1,4 +1,4 @@ -import { QueryOptions } from "@tanstack/react-query" +import { queryOptions } from "@tanstack/react-query" import { widgetListsApi } from "../../clients" const widgetListKeys = { @@ -9,11 +9,11 @@ const widgetListKeys = { const widgetListQueries = { detail: (id: number) => - ({ + queryOptions({ queryKey: widgetListKeys.detail(id), queryFn: () => widgetListsApi.widgetListsRetrieve({ id }).then((res) => res.data), - }) satisfies QueryOptions, + }), } export { widgetListQueries, widgetListKeys } diff --git a/frontends/api/src/ssr/usePrefetchWarnings.test.ts b/frontends/api/src/ssr/usePrefetchWarnings.test.ts index 0eae224862..e03f88a856 100644 --- a/frontends/api/src/ssr/usePrefetchWarnings.test.ts +++ b/frontends/api/src/ssr/usePrefetchWarnings.test.ts @@ -44,12 +44,12 @@ describe("SSR prefetch warnings", () => { [ expect.objectContaining({ disabled: false, - initialStatus: "loading", + status: "pending", key: learningResourceQueries.detail(1).queryKey, observerCount: 1, }), ], - ["hash", "initialStatus", "status", "observerCount", "disabled"], + ["hash", "status", "observerCount", "disabled"], ) }) @@ -104,15 +104,14 @@ describe("SSR prefetch warnings", () => { expect(console.table).toHaveBeenCalledWith( [ { - disabled: false, + disabled: true, hash: JSON.stringify(learningResourceQueries.detail(1).queryKey), - initialStatus: "success", key: learningResourceQueries.detail(1).queryKey, observerCount: 0, status: "success", }, ], - ["hash", "initialStatus", "status", "observerCount", "disabled"], + ["hash", "status", "observerCount", "disabled"], ) }) }) diff --git a/frontends/api/src/ssr/usePrefetchWarnings.ts b/frontends/api/src/ssr/usePrefetchWarnings.ts index 000d80261c..05fbc4c6b3 100644 --- a/frontends/api/src/ssr/usePrefetchWarnings.ts +++ b/frontends/api/src/ssr/usePrefetchWarnings.ts @@ -9,11 +9,10 @@ const logQueries = (...args: [...string[], Query[]]) => { key: query.queryKey, hash: query.queryHash, disabled: query.isDisabled(), - initialStatus: query.initialState.status, status: query.state.status, observerCount: query.getObserversCount(), })), - ["hash", "initialStatus", "status", "observerCount", "disabled"], + ["hash", "status", "observerCount", "disabled"], ) } @@ -52,14 +51,14 @@ export const usePrefetchWarnings = ({ const cache = queryClient.getQueryCache() const queries = cache.getAll() - const exempted = [...exemptions, ...PREFETCH_EXEMPT_QUERIES].map((key) => - cache.find(key), + const exempted = [...exemptions, ...PREFETCH_EXEMPT_QUERIES].map( + (queryKey) => cache.find({ queryKey }), ) const potentialPrefetches = queries.filter( (query) => !exempted.includes(query) && - query.initialState.status !== "success" && + query.state.status !== "success" && !query.isDisabled(), ) @@ -75,9 +74,8 @@ export const usePrefetchWarnings = ({ const unusedPrefetches = queries.filter( (query) => !exempted.includes(query) && - query.initialState.status === "success" && - query.getObserversCount() === 0 && - !query.isDisabled(), + query.state.status === "success" && + query.getObserversCount() === 0, ) if (unusedPrefetches.length > 0) { diff --git a/frontends/main/Dockerfile.web b/frontends/main/Dockerfile.web index 1d495f0535..e4a500f251 100644 --- a/frontends/main/Dockerfile.web +++ b/frontends/main/Dockerfile.web @@ -17,6 +17,8 @@ # --build-arg NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE= \ # --build-arg NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE= \ # --build-arg NEXT_PUBLIC_APPZI_URL= \ +# --build-arg NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT= \ +# --build-arg NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT= \ # --build-arg NEXT_PUBLIC_VERSION= \ # -t mitodl/mit-learn-frontend:latest . @@ -45,6 +47,8 @@ # NEXT_PUBLIC_SENTRY_PROFILES_SAMPLE_RATE=$SENTRY_PROFILES_SAMPLE_RATE,\ # NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$SENTRY_TRACES_SAMPLE_RATE,\ # NEXT_PUBLIC_APPZI_URL=$APPZI_URL,\ +# NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$LEARN_AI_RECOMMENDATION_ENDPOINT,\ +# NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$LEARN_AI_SYLLABUS_ENDPOINT,\ # NEXT_PUBLIC_VERSION=$VERSION \ # --context-path . @@ -57,6 +61,7 @@ FROM node:22-alpine RUN apk update RUN apk add --no-cache libc6-compat +RUN apk add --no-cache git WORKDIR /app @@ -136,6 +141,11 @@ ENV NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE=$NEXT_PUBLIC_SENTRY_TRACES_SAMPLE_RATE ARG NEXT_PUBLIC_APPZI_URL ENV NEXT_PUBLIC_APPZI_URL=$NEXT_PUBLIC_APPZI_URL +ARG NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT +ENV NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT=$NEXT_PUBLIC_LEARN_AI_RECOMMENDATION_ENDPOINT +ARG NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT +ENV NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT=$NEXT_PUBLIC_LEARN_AI_SYLLABUS_ENDPOINT + ENV NEXT_PUBLIC_DEFAULT_SEARCH_MODE="phrase" ENV NEXT_PUBLIC_DEFAULT_SEARCH_SLOP="6" ENV NEXT_PUBLIC_DEFAULT_SEARCH_STALENESS_PENALTY="2.5" diff --git a/frontends/main/package.json b/frontends/main/package.json index 2b3985f5ce..7de0302f82 100644 --- a/frontends/main/package.json +++ b/frontends/main/package.json @@ -14,13 +14,11 @@ "@emotion/cache": "^11.13.1", "@emotion/styled": "^11.11.0", "@mitodl/course-search-utils": "3.3.2", - "@mitodl/smoot-design": "^3.0.1", + "@mitodl/smoot-design": "^3.3.0", "@next/bundle-analyzer": "^14.2.15", - "@nlux/react": "^2.17.1", - "@nlux/themes": "^2.17.1", "@remixicon/react": "^4.2.0", "@sentry/nextjs": "^8.36.0", - "@tanstack/react-query": "^4.36.1", + "@tanstack/react-query": "^5.66", "api": "workspace:*", "classnames": "^2.5.1", "formik": "^2.4.6", diff --git a/frontends/main/public/images/icons/tim.svg b/frontends/main/public/images/icons/tim.svg new file mode 100644 index 0000000000..53690473f8 --- /dev/null +++ b/frontends/main/public/images/icons/tim.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/frontends/main/src/app-pages/ChatPage/ChatPage.tsx b/frontends/main/src/app-pages/ChatPage/ChatPage.tsx index 056719b8ab..c3744660a3 100644 --- a/frontends/main/src/app-pages/ChatPage/ChatPage.tsx +++ b/frontends/main/src/app-pages/ChatPage/ChatPage.tsx @@ -1,61 +1,61 @@ "use client" -import React, { useMemo } from "react" -import { makeSend } from "./send" -import { FeatureFlags } from "@/common/feature_flags" -import { useFeatureFlagEnabled } from "posthog-js/react" -import StyledContainer from "@/page-components/StyledContainer/StyledContainer" +import React from "react" import { styled } from "ol-components" -import { NluxAiChat } from "@/page-components/Nlux-AiChat/AiChat" +import { getCsrfToken } from "@/common/utils" +import { AiChat, AiChatProps } from "@mitodl/smoot-design/ai" -const StyledChat = styled(NluxAiChat)({ - maxHeight: "60vh", - flex: 1, +const Container = styled.div({ + margin: "40px auto", + width: "60%", }) -const CONVERSATION_OPTIONS = { - conversationStarters: [ - { - prompt: - "I'm interested in courses on quantum computing that offer certificates.", - }, - { - prompt: - "I want to learn about global warming, can you recommend any videos?", - }, - { - prompt: - "I am curious about AI applications for business. Do you have any free courses about that?", - }, - { - prompt: - "I would like to learn about linear regression, preferably at no cost.", - }, - ], -} +const INITIAL_MESSAGES: AiChatProps["initialMessages"] = [ + { + content: "What do you want to learn about today?", + role: "assistant", + }, +] + +export const STARTERS = [ + { + content: + "I'm interested in courses on quantum computing that offer certificates.", + }, + { + content: + "I want to learn about global warming, can you recommend any videos?", + }, + { + content: + "I am curious about AI applications for business. Do you have any free courses about that?", + }, + { + content: + "I would like to learn about linear regression, preferably at no cost.", + }, +] const ChatPage = () => { - const recommendationBotEnabled = useFeatureFlagEnabled( - FeatureFlags.RecommendationBot, - ) - const send = useMemo(() => { - return makeSend({ url: "/api/v0/chat_agent/" }) - }, []) return ( - - { - // eslint-disable-next-line no-constant-condition - recommendationBotEnabled ? ( - - ) : ( - <> - ) - } - + + ({ + message: messages[messages.length - 1].content, + }), + }} + /> + ) } diff --git a/frontends/main/src/app-pages/ChatPage/send.ts b/frontends/main/src/app-pages/ChatPage/send.ts deleted file mode 100644 index edcc672760..0000000000 --- a/frontends/main/src/app-pages/ChatPage/send.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { NluxAiChatProps } from "@/page-components/Nlux-AiChat/AiChat" - -function getCookie(name: string) { - const value = `; ${document.cookie}` - const parts = value.split(`; ${name}=`) - if (parts.length === 2) { - return parts.pop()?.split(";").shift() - } -} - -type EndpointOpts = { - url: "/api/v0/chat_agent/" - extraBody?: Record -} - -const makeRequest = async (opts: EndpointOpts, message: string) => - fetch(`${process.env.NEXT_PUBLIC_MITOL_API_BASE_URL}${opts.url}`, { - method: "POST", - headers: { - "Content-Type": "application/json", - "X-CSRFToken": - getCookie(process.env.NEXT_PUBLIC_CSRF_COOKIE_NAME || "csrftoken") ?? - "", - }, - credentials: "include", // TODO Remove this, should be handled by same-origin - body: JSON.stringify({ message, ...opts.extraBody }), - }) - -const RESPONSE_DELAY = 500 - -// Function to send query to the server and receive a stream of chunks as response -const makeSend = - ( - opts: EndpointOpts, - processContent: (content: string) => string = (content) => content, - ): NluxAiChatProps["send"] => - async (message, observer) => { - const response = await makeRequest(opts, message) - - if (response.status !== 200) { - observer.error(new Error("Failed to connect to the server")) - return - } - - if (!response.body) { - return - } - - // Read a stream of server-sent events - // and feed them to the observer as they are being generated - const reader = response.body.getReader() - const textDecoder = new TextDecoder() - - // eslint-disable-next-line no-constant-condition - while (true) { - const { value, done } = await reader.read() - if (done) { - /** - * Without the pause here, some messages were getting displayed completely - * empty. Unsure why. - * - * Maybe related to stream having only a single chunk? - */ - await new Promise((res) => setTimeout(res, RESPONSE_DELAY)) - break - } - - const content = textDecoder.decode(value) - if (content) { - observer.next(processContent(content)) - } - } - - observer.complete() - } - -export { makeSend } -export type { EndpointOpts } diff --git a/frontends/main/src/app-pages/ChatSyllabusPage/ChatSyllabusPage.tsx b/frontends/main/src/app-pages/ChatSyllabusPage/ChatSyllabusPage.tsx index 6a65999fea..e57eadf4ea 100644 --- a/frontends/main/src/app-pages/ChatSyllabusPage/ChatSyllabusPage.tsx +++ b/frontends/main/src/app-pages/ChatSyllabusPage/ChatSyllabusPage.tsx @@ -1,7 +1,6 @@ "use client" import React, { useState } from "react" import { styled, MenuItem, Alert } from "ol-components" - import { FeatureFlags } from "@/common/feature_flags" import { useFeatureFlagEnabled } from "posthog-js/react" import StyledContainer from "@/page-components/StyledContainer/StyledContainer" @@ -40,9 +39,6 @@ const StyledDebugPre = styled.pre({ width: "80%", whiteSpace: "pre-wrap", }) -const AiChatStyled = styled(AiChat)({ - height: "60vh", -}) const ChatSyllabusPage = () => { const botEnabled = useFeatureFlagEnabled(FeatureFlags.RecommendationBot) @@ -52,111 +48,106 @@ const ChatSyllabusPage = () => { return ( - { - // eslint-disable-next-line no-constant-condition - botEnabled ? ( - <> -
- -
- Course - - Contentfile Chunk Size - -
-
-
- { - return { - message: messages[messages.length - 1].content, - readable_id: readableId, - collection_name: collectionName, - } - }, - onFinish: (message) => { - const contentParts = message.content.split("