Skip to content

Commit 7239c23

Browse files
Tie chatbots to URL parameters (#2076)
* make LinkAdapter use shallow * tie AskTim to drawer query param * display syllabus chat based on query param
1 parent aa02630 commit 7239c23

File tree

19 files changed

+347
-111
lines changed

19 files changed

+347
-111
lines changed

frontends/main/src/common/metadata.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
1+
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
22
import { learningResourcesApi } from "api/clients"
33
import type { Metadata } from "next"
44
import handleNotFound from "./handleNotFound"
@@ -28,7 +28,9 @@ export const getMetadataAsync = async ({
2828
...otherMeta
2929
}: MetadataAsyncProps) => {
3030
// The learning resource drawer is open
31-
const learningResourceId = (await searchParams)?.[RESOURCE_DRAWER_QUERY_PARAM]
31+
const learningResourceId = (await searchParams)?.[
32+
RESOURCE_DRAWER_PARAMS.resource
33+
]
3234
if (learningResourceId) {
3335
const { data } = await handleNotFound(
3436
learningResourcesApi.learningResourcesRetrieve({

frontends/main/src/common/urls.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ export const UNITS = "/units"
112112

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

115-
export const RESOURCE_DRAWER_QUERY_PARAM = "resource"
115+
export const RECOMMENDER_QUERY_PARAM = "recommender"
116+
117+
export const RESOURCE_DRAWER_PARAMS = {
118+
resource: "resource",
119+
syllabus: "syllabus",
120+
} as const
116121

117122
export const querifiedSearchUrl = (
118123
params:

frontends/main/src/page-components/AiChat/AiChatWithEntryScreen.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ const AiChatWithEntryScreen = ({
170170
return (
171171
<Container className={className}>
172172
{showEntryScreen ? (
173-
<EntryScreen>
173+
<EntryScreen data-testid="ai-chat-entry-screen">
174174
<TimLogoBox>
175175
<RiSparkling2Line />
176176
<TimLogo src={timLogo.src} alt="" width={40} height={40} />

frontends/main/src/page-components/AiChat/AiRecommendationBotDrawer.tsx

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from "react"
2-
import { styled, Drawer } from "ol-components"
2+
import { styled, RoutedDrawer } from "ol-components"
33
import { RiCloseLine } from "@remixicon/react"
44
import { ActionButton } from "@mitodl/smoot-design"
55
import type { AiChatProps } from "@mitodl/smoot-design/ai"
66
import AiChatWithEntryScreen from "./AiChatWithEntryScreen"
77
import { getCsrfToken } from "@/common/utils"
8+
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"
89

910
const CloseButton = styled(ActionButton)(({ theme }) => ({
1011
position: "absolute",
@@ -51,37 +52,15 @@ const STARTERS = [
5152
},
5253
]
5354

54-
const AiRecommendationBotDrawer = ({
55-
open,
56-
setOpen,
57-
}: {
58-
open: boolean
59-
setOpen: (open: boolean) => void
60-
}) => {
61-
const closeDrawer = () => {
62-
setOpen(false)
63-
// setShowEntryScreen(true)
64-
}
65-
55+
const DrawerContent: React.FC<{
56+
onClose?: () => void
57+
}> = ({ onClose }) => {
6658
return (
67-
<Drawer
68-
open={open}
69-
anchor="right"
70-
onClose={closeDrawer}
71-
PaperProps={{
72-
sx: {
73-
minWidth: (theme) => ({
74-
[theme.breakpoints.down("md")]: {
75-
width: "100%",
76-
},
77-
}),
78-
},
79-
}}
80-
>
59+
<>
8160
<CloseButton
61+
onClick={onClose}
8262
variant="text"
8363
size="medium"
84-
onClick={closeDrawer}
8564
aria-label="Close"
8665
>
8766
<RiCloseLine />
@@ -104,7 +83,30 @@ const AiRecommendationBotDrawer = ({
10483
}),
10584
}}
10685
/>
107-
</Drawer>
86+
</>
87+
)
88+
}
89+
90+
const DRAWER_REQUIRED_PARAMS = [RECOMMENDER_QUERY_PARAM] as const
91+
const AiRecommendationBotDrawer = () => {
92+
return (
93+
<RoutedDrawer
94+
hideCloseButton
95+
requiredParams={DRAWER_REQUIRED_PARAMS}
96+
aria-label="What do you want to learn about?"
97+
anchor="right"
98+
PaperProps={{
99+
sx: {
100+
minWidth: (theme) => ({
101+
[theme.breakpoints.down("md")]: {
102+
width: "100%",
103+
},
104+
}),
105+
},
106+
}}
107+
>
108+
{({ closeDrawer }) => <DrawerContent onClose={closeDrawer} />}
109+
</RoutedDrawer>
108110
)
109111
}
110112

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react"
2+
import { renderWithProviders, screen, user, waitFor } from "@/test-utils"
3+
import AskTIMButton from "./AskTimDrawerButton"
4+
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"
5+
6+
describe("AskTIMButton", () => {
7+
it.each([
8+
{ url: "", open: false },
9+
{ url: `?${RECOMMENDER_QUERY_PARAM}`, open: true },
10+
])("Opens drawer based on URL param", async ({ url, open }) => {
11+
renderWithProviders(<AskTIMButton />, {
12+
url,
13+
})
14+
15+
const aiChat = screen.queryByTestId("ai-chat-entry-screen")
16+
expect(!!aiChat).toBe(open)
17+
})
18+
19+
test("Clicking button opens / closes drawer", async () => {
20+
const { location } = renderWithProviders(<AskTIMButton />)
21+
22+
expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
23+
false,
24+
)
25+
26+
const askTim = screen.getByRole("link", { name: /ask tim/i })
27+
28+
await user.click(askTim)
29+
30+
expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
31+
true,
32+
)
33+
34+
await user.click(screen.getByRole("button", { name: "Close" }))
35+
36+
await waitFor(() => {
37+
expect(location.current.searchParams.has(RECOMMENDER_QUERY_PARAM)).toBe(
38+
false,
39+
)
40+
})
41+
})
42+
})

frontends/main/src/page-components/AiChat/AskTimDrawerButton.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React, { useState } from "react"
1+
import React from "react"
22
import { Typography, styled } from "ol-components"
3-
import { Button } from "@mitodl/smoot-design"
3+
import { ButtonLink } from "@mitodl/smoot-design"
44
import { RiSparkling2Line } from "@remixicon/react"
55
import AiRecommendationBotDrawer from "./AiRecommendationBotDrawer"
6+
import { RECOMMENDER_QUERY_PARAM } from "@/common/urls"
67

7-
const StyledButton = styled(Button)(({ theme }) => ({
8+
const StyledButton = styled(ButtonLink)(({ theme }) => ({
89
display: "flex",
910
flexDirection: "row",
1011
gap: "8px",
@@ -30,21 +31,20 @@ const StyledButton = styled(Button)(({ theme }) => ({
3031
}))
3132

3233
const AskTIMButton = () => {
33-
const [open, setOpen] = useState(false)
34-
3534
return (
3635
<>
3736
<StyledButton
37+
shallow
3838
variant="bordered"
3939
edge="rounded"
40-
onClick={() => setOpen(true)}
40+
href={`?${RECOMMENDER_QUERY_PARAM}`}
4141
>
4242
<RiSparkling2Line />
4343
<Typography variant="body1">
4444
Ask<strong>TIM</strong>
4545
</Typography>
4646
</StyledButton>
47-
<AiRecommendationBotDrawer open={open} setOpen={setOpen} />
47+
<AiRecommendationBotDrawer />
4848
</>
4949
)
5050
}

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
import React from "react"
22
import {
3+
expectLastProps,
34
expectProps,
45
renderWithProviders,
56
screen,
7+
user,
68
waitFor,
79
within,
810
} from "@/test-utils"
911
import LearningResourceDrawer from "./LearningResourceDrawer"
1012
import { urls, factories, setMockResponse } from "api/test-utils"
1113
import { LearningResourceExpanded } from "../LearningResourceExpanded/LearningResourceExpanded"
12-
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
14+
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
1315
import { LearningResource, ResourceTypeEnum } from "api"
1416
import { makeUserSettings } from "@/test-utils/factories"
1517
import type { User } from "api/hooks/user"
16-
import { usePostHog } from "posthog-js/react"
18+
import { useFeatureFlagEnabled, usePostHog } from "posthog-js/react"
1719

1820
jest.mock("../LearningResourceExpanded/LearningResourceExpanded", () => {
1921
const actual = jest.requireActual(
@@ -31,6 +33,9 @@ jest.mocked(usePostHog).mockReturnValue(
3133
// @ts-expect-error Not mocking all of posthog
3234
{ capture: mockedPostHogCapture },
3335
)
36+
const mockedUseFeatureFlagEnabled = jest
37+
.mocked(useFeatureFlagEnabled)
38+
.mockImplementation(() => false)
3439

3540
describe("LearningResourceDrawer", () => {
3641
const setupApis = (
@@ -94,7 +99,7 @@ describe("LearningResourceDrawer", () => {
9499
: ""
95100

96101
renderWithProviders(<LearningResourceDrawer />, {
97-
url: `?dog=woof&${RESOURCE_DRAWER_QUERY_PARAM}=${resource.id}`,
102+
url: `?dog=woof&${RESOURCE_DRAWER_PARAMS.resource}=${resource.id}`,
98103
})
99104
expect(LearningResourceExpanded).toHaveBeenCalled()
100105
await waitFor(() => {
@@ -220,4 +225,80 @@ describe("LearningResourceDrawer", () => {
220225
similarResources.some((r) => text.includes(r.title)),
221226
)
222227
})
228+
229+
it.each([
230+
{ extraQueryParams: "", expectChat: false },
231+
{
232+
extraQueryParams: `&${RESOURCE_DRAWER_PARAMS.syllabus}`,
233+
expectChat: true,
234+
},
235+
])(
236+
"Renders drawer with chatExpanded based on URL",
237+
async ({ extraQueryParams, expectChat }) => {
238+
mockedUseFeatureFlagEnabled.mockReturnValue(true)
239+
const { resource } = setupApis({
240+
resource: {
241+
// Chat is only enabled for courses
242+
resource_type: ResourceTypeEnum.Course,
243+
},
244+
})
245+
renderWithProviders(<LearningResourceDrawer />, {
246+
url: `?resource=${resource.id}${extraQueryParams}`,
247+
})
248+
249+
await screen.findByText(resource.title)
250+
251+
await waitFor(() => {
252+
expectLastProps(LearningResourceExpanded, {
253+
resource,
254+
chatExpanded: expectChat,
255+
})
256+
})
257+
},
258+
)
259+
260+
test("If chat is not supported, 'syllabus' param removed from URL", async () => {
261+
mockedUseFeatureFlagEnabled.mockReturnValue(true)
262+
const { resource } = setupApis({
263+
resource: {
264+
// Chat is only enabled for courses; NOT enabled here
265+
resource_type: ResourceTypeEnum.Program,
266+
},
267+
})
268+
const { location } = renderWithProviders(<LearningResourceDrawer />, {
269+
url: `?resource=${resource.id}&syllabus`,
270+
})
271+
272+
expect(location.current.searchParams.has("syllabus")).toBe(true)
273+
274+
await waitFor(() => {
275+
expectLastProps(LearningResourceExpanded, {
276+
resource,
277+
chatExpanded: false,
278+
})
279+
})
280+
expect(location.current.searchParams.has("syllabus")).toBe(false)
281+
})
282+
283+
test("Clicking 'Ask Tim' toggles chat query param", async () => {
284+
mockedUseFeatureFlagEnabled.mockReturnValue(true)
285+
const { resource } = setupApis({
286+
resource: {
287+
// Chat is only enabled for courses
288+
resource_type: ResourceTypeEnum.Course,
289+
},
290+
})
291+
const { location } = renderWithProviders(<LearningResourceDrawer />, {
292+
url: `?resource=${resource.id}`,
293+
})
294+
295+
const askTimButton = await screen.findByRole("button", { name: /Ask\sTIM/ })
296+
expect(askTimButton).toBeInTheDocument()
297+
298+
expect(location.current.searchParams.has("syllabus")).toBe(false)
299+
await user.click(askTimButton)
300+
expect(location.current.searchParams.has("syllabus")).toBe(true)
301+
await user.click(askTimButton)
302+
expect(location.current.searchParams.has("syllabus")).toBe(false)
303+
})
223304
})

frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from "ol-components"
88
import { useLearningResourcesDetail } from "api/hooks/learningResources"
99

10-
import { RESOURCE_DRAWER_QUERY_PARAM } from "@/common/urls"
10+
import { RESOURCE_DRAWER_PARAMS } from "@/common/urls"
1111
import { useUserMe } from "api/hooks/user"
1212
import NiceModal from "@ebay/nice-modal-react"
1313
import {
@@ -23,7 +23,11 @@ import { TopicCarouselConfig } from "@/common/carousels"
2323
import { ResourceTypeEnum } from "api"
2424
import { PostHogEvents } from "@/common/constants"
2525

26-
const RESOURCE_DRAWER_PARAMS = [RESOURCE_DRAWER_QUERY_PARAM] as const
26+
const REQUIRED_PARAMS = [RESOURCE_DRAWER_PARAMS.resource] as const
27+
const ALL_PARAMS = [
28+
RESOURCE_DRAWER_PARAMS.resource,
29+
RESOURCE_DRAWER_PARAMS.syllabus,
30+
] as const
2731

2832
const useCapturePageView = (resourceId: number) => {
2933
const { data, isSuccess } = useLearningResourcesDetail(Number(resourceId))
@@ -54,7 +58,8 @@ const DrawerContent: React.FC<{
5458
resourceId: number
5559
titleId: string
5660
closeDrawer: () => void
57-
}> = ({ resourceId, closeDrawer, titleId }) => {
61+
chatExpanded: boolean
62+
}> = ({ resourceId, closeDrawer, titleId, chatExpanded }) => {
5863
/**
5964
* Ideally the resource data should already exist in the query cache, e.g., by:
6065
* - a server-side prefetch
@@ -207,8 +212,9 @@ const DrawerContent: React.FC<{
207212
resource={resource.data}
208213
topCarousels={topCarousels}
209214
bottomCarousels={bottomCarousels}
215+
chatExpanded={chatExpanded}
210216
user={user}
211-
shareUrl={`${window.location.origin}/search?${RESOURCE_DRAWER_QUERY_PARAM}=${resourceId}`}
217+
shareUrl={`${window.location.origin}/search?${RESOURCE_DRAWER_PARAMS.resource}=${resourceId}`}
212218
inLearningPath={inLearningPath}
213219
inUserList={inUserList}
214220
onAddToLearningPathClick={handleAddToLearningPathClick}
@@ -244,16 +250,18 @@ const LearningResourceDrawer = () => {
244250
<Suspense>
245251
<RoutedDrawer
246252
anchor="right"
247-
requiredParams={RESOURCE_DRAWER_PARAMS}
253+
requiredParams={REQUIRED_PARAMS}
254+
params={ALL_PARAMS}
248255
PaperProps={PAPER_PROPS}
249256
hideCloseButton={true}
250257
aria-labelledby={id}
251258
>
252259
{({ params, closeDrawer }) => {
253260
return (
254261
<DrawerContent
262+
chatExpanded={params[RESOURCE_DRAWER_PARAMS.syllabus] !== null}
255263
titleId={id}
256-
resourceId={Number(params.resource)}
264+
resourceId={Number(params[RESOURCE_DRAWER_PARAMS.resource])}
257265
closeDrawer={closeDrawer}
258266
/>
259267
)

0 commit comments

Comments
 (0)