Skip to content

Commit

Permalink
Tie chatbots to URL parameters (#2076)
Browse files Browse the repository at this point in the history
* make LinkAdapter use shallow

* tie AskTim to drawer query param

* display syllabus chat based on query param
  • Loading branch information
ChristopherChudzicki authored Feb 28, 2025
1 parent aa02630 commit 7239c23
Show file tree
Hide file tree
Showing 19 changed files with 347 additions and 111 deletions.
6 changes: 4 additions & 2 deletions frontends/main/src/common/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
import { learningResourcesApi } from "api/clients"
import type { Metadata } from "next"
import handleNotFound from "./handleNotFound"
Expand Down Expand Up @@ -28,7 +28,9 @@ export const getMetadataAsync = async ({
...otherMeta
}: MetadataAsyncProps) => {
// The learning resource drawer is open
const learningResourceId = (await searchParams)?.[RESOURCE_DRAWER_QUERY_PARAM]
const learningResourceId = (await searchParams)?.[
RESOURCE_DRAWER_PARAMS.resource
]
if (learningResourceId) {
const { data } = await handleNotFound(
learningResourcesApi.learningResourcesRetrieve({
Expand Down
7 changes: 6 additions & 1 deletion frontends/main/src/common/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ export const UNITS = "/units"

export const CONTACT = "mailto:[email protected]"

export const RESOURCE_DRAWER_QUERY_PARAM = "resource"
export const RECOMMENDER_QUERY_PARAM = "recommender"

export const RESOURCE_DRAWER_PARAMS = {
resource: "resource",
syllabus: "syllabus",
} as const

export const querifiedSearchUrl = (
params:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const AiChatWithEntryScreen = ({
return (
<Container className={className}>
{showEntryScreen ? (
<EntryScreen>
<EntryScreen data-testid="ai-chat-entry-screen">
<TimLogoBox>
<RiSparkling2Line />
<TimLogo src={timLogo.src} alt="" width={40} height={40} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react"
import { styled, Drawer } from "ol-components"
import { styled, RoutedDrawer } from "ol-components"
import { RiCloseLine } from "@remixicon/react"
import { ActionButton } from "@mitodl/smoot-design"
import type { AiChatProps } from "@mitodl/smoot-design/ai"
import AiChatWithEntryScreen from "./AiChatWithEntryScreen"
import { getCsrfToken } from "@/common/utils"
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"

const CloseButton = styled(ActionButton)(({ theme }) => ({
position: "absolute",
Expand Down Expand Up @@ -51,37 +52,15 @@ const STARTERS = [
},
]

const AiRecommendationBotDrawer = ({
open,
setOpen,
}: {
open: boolean
setOpen: (open: boolean) => void
}) => {
const closeDrawer = () => {
setOpen(false)
// setShowEntryScreen(true)
}

const DrawerContent: React.FC<{
onClose?: () => void
}> = ({ onClose }) => {
return (
<Drawer
open={open}
anchor="right"
onClose={closeDrawer}
PaperProps={{
sx: {
minWidth: (theme) => ({
[theme.breakpoints.down("md")]: {
width: "100%",
},
}),
},
}}
>
<>
<CloseButton
onClick={onClose}
variant="text"
size="medium"
onClick={closeDrawer}
aria-label="Close"
>
<RiCloseLine />
Expand All @@ -104,7 +83,30 @@ const AiRecommendationBotDrawer = ({
}),
}}
/>
</Drawer>
</>
)
}

const DRAWER_REQUIRED_PARAMS = [RECOMMENDER_QUERY_PARAM] as const
const AiRecommendationBotDrawer = () => {
return (
<RoutedDrawer
hideCloseButton
requiredParams={DRAWER_REQUIRED_PARAMS}
aria-label="What do you want to learn about?"
anchor="right"
PaperProps={{
sx: {
minWidth: (theme) => ({
[theme.breakpoints.down("md")]: {
width: "100%",
},
}),
},
}}
>
{({ closeDrawer }) => <DrawerContent onClose={closeDrawer} />}
</RoutedDrawer>
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react"
import { renderWithProviders, screen, user, waitFor } from "@/test-utils"
import AskTIMButton from "./AskTimDrawerButton"
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"

describe("AskTIMButton", () => {
it.each([
{ url: "", open: false },
{ url: `?${RECOMMENDER_QUERY_PARAM}`, open: true },
])("Opens drawer based on URL param", async ({ url, open }) => {
renderWithProviders(<AskTIMButton />, {
url,
})

const aiChat = screen.queryByTestId("ai-chat-entry-screen")
expect(!!aiChat).toBe(open)
})

test("Clicking button opens / closes drawer", async () => {
const { location } = renderWithProviders(<AskTIMButton />)

expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
false,
)

const askTim = screen.getByRole("link", { name: /ask tim/i })

await user.click(askTim)

expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
true,
)

await user.click(screen.getByRole("button", { name: "Close" }))

await waitFor(() => {
expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
false,
)
})
})
})
14 changes: 7 additions & 7 deletions frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState } from "react"
import React from "react"
import { Typography, styled } from "ol-components"
import { Button } from "@mitodl/smoot-design"
import { ButtonLink } from "@mitodl/smoot-design"
import { RiSparkling2Line } from "@remixicon/react"
import AiRecommendationBotDrawer from "./AiRecommendationBotDrawer"
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"

const StyledButton = styled(Button)(({ theme }) => ({
const StyledButton = styled(ButtonLink)(({ theme }) => ({
display: "flex",
flexDirection: "row",
gap: "8px",
Expand All @@ -30,21 +31,20 @@ const StyledButton = styled(Button)(({ theme }) => ({
}))

const AskTIMButton = () => {
const [open, setOpen] = useState(false)

return (
<>
<StyledButton
shallow
variant="bordered"
edge="rounded"
onClick={() => setOpen(true)}
href={`?${RECOMMENDER_QUERY_PARAM}`}
>
<RiSparkling2Line />
<Typography variant="body1">
Ask<strong>TIM</strong>
</Typography>
</StyledButton>
<AiRecommendationBotDrawer open={open} setOpen={setOpen} />
<AiRecommendationBotDrawer />
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import React from "react"
import {
expectLastProps,
expectProps,
renderWithProviders,
screen,
user,
waitFor,
within,
} from "@/test-utils"
import LearningResourceDrawer from "./LearningResourceDrawer"
import { urls, factories, setMockResponse } from "api/test-utils"
import { LearningResourceExpanded } from "../LearningResourceExpanded/LearningResourceExpanded"
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
import { LearningResource, ResourceTypeEnum } from "api"
import { makeUserSettings } from "@/test-utils/factories"
import type { User } from "api/hooks/user"
import { usePostHog } from "posthog-js/react"
import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"

jest.mock("../LearningResourceExpanded/LearningResourceExpanded", () => {
const actual = jest.requireActual(
Expand All @@ -31,6 +33,9 @@ jest.mocked(usePostHog).mockReturnValue(
// @ts-expect-error Not mocking all of posthog
{ capture: mockedPostHogCapture },
)
const mockedUseFeatureFlagEnabled = jest
.mocked(useFeatureFlagEnabled)
.mockImplementation(() => false)

describe("LearningResourceDrawer", () => {
const setupApis = (
Expand Down Expand Up @@ -94,7 +99,7 @@ describe("LearningResourceDrawer", () => {
: ""

renderWithProviders(<LearningResourceDrawer />, {
url: `?dog=woof&${RESOURCE_DRAWER_QUERY_PARAM}=${resource.id}`,
url: `?dog=woof&${RESOURCE_DRAWER_PARAMS.resource}=${resource.id}`,
})
expect(LearningResourceExpanded).toHaveBeenCalled()
await waitFor(() => {
Expand Down Expand Up @@ -220,4 +225,80 @@ describe("LearningResourceDrawer", () => {
similarResources.some((r) => text.includes(r.title)),
)
})

it.each([
{ extraQueryParams: "", expectChat: false },
{
extraQueryParams: `&${RESOURCE_DRAWER_PARAMS.syllabus}`,
expectChat: true,
},
])(
"Renders drawer with chatExpanded based on URL",
async ({ extraQueryParams, expectChat }) => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
const { resource } = setupApis({
resource: {
// Chat is only enabled for courses
resource_type: ResourceTypeEnum.Course,
},
})
renderWithProviders(<LearningResourceDrawer />, {
url: `?resource=${resource.id}${extraQueryParams}`,
})

await screen.findByText(resource.title)

await waitFor(() => {
expectLastProps(LearningResourceExpanded, {
resource,
chatExpanded: expectChat,
})
})
},
)

test("If chat is not supported, 'syllabus' param removed from URL", async () => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
const { resource } = setupApis({
resource: {
// Chat is only enabled for courses; NOT enabled here
resource_type: ResourceTypeEnum.Program,
},
})
const { location } = renderWithProviders(<LearningResourceDrawer />, {
url: `?resource=${resource.id}&syllabus`,
})

expect(location.current.searchParams.has("syllabus")).toBe(true)

await waitFor(() => {
expectLastProps(LearningResourceExpanded, {
resource,
chatExpanded: false,
})
})
expect(location.current.searchParams.has("syllabus")).toBe(false)
})

test("Clicking 'Ask Tim' toggles chat query param", async () => {
mockedUseFeatureFlagEnabled.mockReturnValue(true)
const { resource } = setupApis({
resource: {
// Chat is only enabled for courses
resource_type: ResourceTypeEnum.Course,
},
})
const { location } = renderWithProviders(<LearningResourceDrawer />, {
url: `?resource=${resource.id}`,
})

const askTimButton = await screen.findByRole("button", { name: /Ask\sTIM/ })
expect(askTimButton).toBeInTheDocument()

expect(location.current.searchParams.has("syllabus")).toBe(false)
await user.click(askTimButton)
expect(location.current.searchParams.has("syllabus")).toBe(true)
await user.click(askTimButton)
expect(location.current.searchParams.has("syllabus")).toBe(false)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
} from "ol-components"
import { useLearningResourcesDetail } from "api/hooks/learningResources"

import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
import { useUserMe } from "api/hooks/user"
import NiceModal from "@ebay/nice-modal-react"
import {
Expand All @@ -23,7 +23,11 @@ import { TopicCarouselConfig } from "@/common/carousels"
import { ResourceTypeEnum } from "api"
import { PostHogEvents } from "@/common/constants"

const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const
const REQUIRED_PARAMS = [RESOURCE_DRAWER_PARAMS.resource] as const
const ALL_PARAMS = [
RESOURCE_DRAWER_PARAMS.resource,
RESOURCE_DRAWER_PARAMS.syllabus,
] as const

const useCapturePageView = (resourceId: number) => {
const { data, isSuccess } = useLearningResourcesDetail(Number(resourceId))
Expand Down Expand Up @@ -54,7 +58,8 @@ const DrawerContent: React.FC<{
resourceId: number
titleId: string
closeDrawer: () => void
}> = ({ resourceId, closeDrawer, titleId }) => {
chatExpanded: boolean
}> = ({ resourceId, closeDrawer, titleId, chatExpanded }) => {
/**
* Ideally the resource data should already exist in the query cache, e.g., by:
* - a server-side prefetch
Expand Down Expand Up @@ -207,8 +212,9 @@ const DrawerContent: React.FC<{
resource={resource.data}
topCarousels={topCarousels}
bottomCarousels={bottomCarousels}
chatExpanded={chatExpanded}
user={user}
shareUrl={`${window.location.origin}/search?${RESOURCE_DRAWER_QUERY_PARAM}=${resourceId}`}
shareUrl={`${window.location.origin}/search?${RESOURCE_DRAWER_PARAMS.resource}=${resourceId}`}
inLearningPath={inLearningPath}
inUserList={inUserList}
onAddToLearningPathClick={handleAddToLearningPathClick}
Expand Down Expand Up @@ -244,16 +250,18 @@ const LearningResourceDrawer = () => {
<Suspense>
<RoutedDrawer
anchor="right"
requiredParams={RESOURCE_DRAWER_PARAMS}
requiredParams={REQUIRED_PARAMS}
params={ALL_PARAMS}
PaperProps={PAPER_PROPS}
hideCloseButton={true}
aria-labelledby={id}
>
{({ params, closeDrawer }) => {
return (
<DrawerContent
chatExpanded={params[RESOURCE_DRAWER_PARAMS.syllabus] !== null}
titleId={id}
resourceId={Number(params.resource)}
resourceId={Number(params[RESOURCE_DRAWER_PARAMS.resource])}
closeDrawer={closeDrawer}
/>
)
Expand Down
Loading

0 comments on commit 7239c23

Please sign in to comment.