Skip to content

Commit 29a9bf3

Browse files
committed
Assignment list / problem set control
1 parent 74039e5 commit 29a9bf3

File tree

10 files changed

+415
-20
lines changed

10 files changed

+415
-20
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,10 @@
9292
"@storybook/addon-links": "^9.0.13",
9393
"@storybook/addon-onboarding": "^9.0.13",
9494
"@storybook/addon-webpack5-compiler-swc": "^3.0.0",
95+
"@storybook/blocks": "^8.6.14",
9596
"@storybook/nextjs": "^9.0.13",
9697
"@storybook/react-webpack5": "^9.0.13",
98+
"@storybook/test": "^8.6.14",
9799
"@swc/jest": "^0.2.37",
98100
"@testing-library/dom": "^10.4.0",
99101
"@testing-library/jest-dom": "^6.6.3",

src/bundles/AiDrawer/AiDrawerManager.stories.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,39 @@ const meta: Meta<typeof AiDrawerManager> = {
123123
},
124124
}}
125125
/>
126+
<h3>Tutor Bot with assignment selection</h3>
127+
<IFrame
128+
payload={{
129+
blockType: "problem",
130+
blockUsageKey: "problem-frame-assignment",
131+
trackingUrl: TRACKING_EVENTS_ENDPOINT,
132+
chat: {
133+
apiUrl: TEST_API_STREAMING,
134+
initialMessages: INITIAL_MESSAGES,
135+
conversationStarters: STARTERS,
136+
entryScreenEnabled: false,
137+
entryScreenTitle: "AskTIM about this problem",
138+
assignmentList: [
139+
{
140+
displayName: "Assignment 1",
141+
usageKey: "assignment-1",
142+
},
143+
{
144+
displayName: "Assignment 2",
145+
usageKey: "assignment-2",
146+
},
147+
{
148+
displayName: "Assignment 3",
149+
usageKey: "assignment-3",
150+
},
151+
{
152+
displayName: "Assignment 4",
153+
usageKey: "assignment-4",
154+
},
155+
],
156+
},
157+
}}
158+
/>
126159
<h3>Video Drawer</h3>
127160
<p>
128161
The chat entry screen is shown by default for the video blocks Tutor
@@ -185,7 +218,6 @@ export const AiDrawerManagerStory: Story = {
185218
}),
186219
http.post(TRACKING_EVENTS_ENDPOINT, async ({ request }) => {
187220
const body = await request.json()
188-
console.log("TrackingEvent", body)
189221
return HttpResponse.json({ success: true })
190222
}),
191223
...handlers,

src/bundles/AiDrawer/AiDrawerManager.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ const trackingEvent = jest.fn()
6767
const assertTrackingEvent = (...data: unknown[]) => {
6868
expect(trackingEvent).toHaveBeenCalledTimes(data.length)
6969
data.forEach((eventData) => {
70-
console.log(eventData)
7170
expect(trackingEvent).toHaveBeenCalledWith(eventData)
7271
})
7372
trackingEvent.mockClear()

src/components/AiChat/AiChat.stories.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from "react"
22
import type { Meta, StoryObj } from "@storybook/nextjs"
3+
import { http, HttpResponse } from "msw"
34
import { AiChat } from "./AiChat"
45
import type { AiChatProps } from "./types"
56
import styled from "@emotion/styled"
@@ -9,6 +10,7 @@ import { MathJaxContext } from "better-react-mathjax"
910

1011
const TEST_API_STREAMING = "http://localhost:4567/streaming"
1112
const TEST_API_JSON = "http://localhost:4567/json"
13+
const TEST_API_PROBLEM_SET_LIST = "http://localhost:4567/problem_set_list"
1214

1315
const INITIAL_MESSAGES: AiChatProps["initialMessages"] = [
1416
{
@@ -41,7 +43,21 @@ const meta: Meta<typeof AiChat> = {
4143
title: "smoot-design/AI/AiChat",
4244
component: AiChat,
4345
parameters: {
44-
msw: { handlers },
46+
msw: {
47+
handlers: [
48+
http.get(TEST_API_PROBLEM_SET_LIST, () => {
49+
return HttpResponse.json({
50+
problem_set_titles: [
51+
"Assignment 1",
52+
"Assignment 2",
53+
"Assignment 3",
54+
"Assignment 4",
55+
],
56+
})
57+
}),
58+
...handlers,
59+
],
60+
},
4561
},
4662
render: (args) => <AiChat {...args} />,
4763
decorators: (Story, context) => {
@@ -94,6 +110,49 @@ export const JsonResponses: Story = {
94110
},
95111
}
96112

113+
export const Temp: Story = {
114+
args: {
115+
requestOpts: {
116+
apiUrl: TEST_API_STREAMING,
117+
transformBody: (messages, body) => ({
118+
message: messages[messages.length - 1].content,
119+
problem_set_title: body?.problem_set_title,
120+
}),
121+
},
122+
initialMessages: [
123+
{
124+
content:
125+
"Hi! Please select an assignment from the dropdown menu to begin.",
126+
role: "assistant",
127+
},
128+
],
129+
conversationStarters: STARTERS,
130+
entryScreenEnabled: false,
131+
},
132+
}
133+
134+
export const AssignmentSelection: Story = {
135+
args: {
136+
requestOpts: {
137+
apiUrl: TEST_API_STREAMING,
138+
transformBody: (messages, body) => ({
139+
message: messages[messages.length - 1].content,
140+
problem_set_title: body?.problem_set_title,
141+
}),
142+
},
143+
initialMessages: [
144+
{
145+
content:
146+
"Hi! Please select an assignment from the dropdown menu to begin.",
147+
role: "assistant",
148+
},
149+
],
150+
conversationStarters: [],
151+
entryScreenEnabled: false,
152+
problemSetListUrl: TEST_API_PROBLEM_SET_LIST,
153+
},
154+
}
155+
97156
const ScrollComponent: FC<AiChatProps> = (args) => {
98157
const ref = useRef<HTMLDivElement>(null)
99158
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(

src/components/AiChat/AiChat.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import { useScrollSnap } from "../ScrollSnap/useScrollSnap"
1818
import type { Message } from "@ai-sdk/react"
1919
import Markdown from "./Markdown"
2020
import EllipsisIcon from "./EllipsisIcon"
21+
import { SimpleSelectField } from "../SimpleSelect/SimpleSelect"
22+
import { useFetch } from "./utils"
23+
import { SelectChangeEvent } from "@mui/material/Select"
2124

2225
const classes = {
2326
root: "MitAiChat--root",
@@ -69,6 +72,13 @@ const ChatContainer = styled.div<{ externalScroll: boolean }>(
6972
}),
7073
)
7174

75+
const AssignmentSelect = styled(SimpleSelectField)(({ theme }) => ({
76+
width: "295px",
77+
"> div": {
78+
width: "inherit",
79+
},
80+
}))
81+
7282
const MessagesContainer = styled(ScrollSnap)<{ externalScroll: boolean }>(
7383
({ externalScroll }) => ({
7484
display: "flex",
@@ -224,12 +234,16 @@ const AiChatDisplay: FC<AiChatDisplayProps> = ({
224234
ref,
225235
useMathJax = false,
226236
onSubmit,
237+
problemSetListUrl,
227238
...others // Could contain data attributes
228239
}) => {
229240
const containerRef = useRef<HTMLDivElement>(null)
230241
const messagesContainerRef = useRef<HTMLDivElement>(null)
231242
const chatScreenRef = useRef<HTMLDivElement>(null)
232243
const promptInputRef = useRef<HTMLDivElement>(null)
244+
const { response: problemSetListResponse } = useFetch<{
245+
problem_set_titles: string[]
246+
}>(problemSetListUrl)
233247

234248
const {
235249
messages,
@@ -242,6 +256,8 @@ const AiChatDisplay: FC<AiChatDisplayProps> = ({
242256
error,
243257
initialMessages,
244258
status,
259+
additionalBody,
260+
setAdditionalBody,
245261
} = useAiChat()
246262

247263
useScrollSnap({
@@ -281,6 +297,10 @@ const AiChatDisplay: FC<AiChatDisplayProps> = ({
281297
})
282298
}
283299

300+
const onProblemSetChange = (event: SelectChangeEvent<string | string[]>) => {
301+
setAdditionalBody?.({ problem_set_title: event.target.value as string })
302+
}
303+
284304
const lastMsg = messages[messages.length - 1]
285305

286306
const externalScroll = !!scrollElement
@@ -317,7 +337,30 @@ const AiChatDisplay: FC<AiChatDisplayProps> = ({
317337
askTimTitle={askTimTitle}
318338
externalScroll={externalScroll}
319339
className={classNames(className, classes.title)}
340+
control={
341+
problemSetListResponse?.problem_set_titles?.length ? (
342+
<AssignmentSelect
343+
label="Assignments"
344+
options={[
345+
{
346+
value: "",
347+
label: "Select an assignment",
348+
disabled: true,
349+
},
350+
...problemSetListResponse.problem_set_titles.map(
351+
(title) => ({
352+
value: title,
353+
label: title,
354+
}),
355+
),
356+
]}
357+
value={additionalBody?.problem_set_title ?? ""}
358+
onChange={onProblemSetChange}
359+
/>
360+
) : null
361+
}
320362
/>
363+
321364
<MessagesContainer
322365
className={classes.messagesContainer}
323366
externalScroll={externalScroll}

src/components/AiChat/AiChatContext.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
import * as React from "react"
22
import { useChat, UseChatHelpers } from "@ai-sdk/react"
33
import type { RequestOpts, AiChatMessage, AiChatContextProps } from "./types"
4-
import { useMemo, createContext } from "react"
4+
import { useMemo, createContext, useState } from "react"
55
import retryingFetch from "../../utils/retryingFetch"
66
import { getCookie } from "../../utils/getCookie"
77

88
const identity = <T,>(x: T): T => x
99

10-
const getFetcher: (requestOpts: RequestOpts) => typeof fetch =
11-
(requestOpts: RequestOpts) => async (url, opts) => {
10+
const getFetcher: (
11+
requestOpts: RequestOpts,
12+
additionalBody: Record<string, string>,
13+
) => typeof fetch =
14+
(requestOpts: RequestOpts, additionalBody: Record<string, string> = {}) =>
15+
async (url, opts) => {
1216
if (typeof opts?.body !== "string") {
1317
console.error("Unexpected body type.")
1418
return retryingFetch(url, opts)
1519
}
16-
const messages: AiChatMessage[] = JSON.parse(opts?.body).messages
20+
const parsedBody = JSON.parse(opts?.body)
21+
const messages: AiChatMessage[] = parsedBody.messages
1722
const transformBody: RequestOpts["transformBody"] =
1823
requestOpts.transformBody ?? identity
1924
const options: RequestInit = {
2025
...opts,
21-
body: JSON.stringify(transformBody(messages)),
26+
body: JSON.stringify(transformBody(messages, additionalBody)),
2227
...requestOpts.fetchOpts,
2328
headers: {
2429
...opts?.headers,
@@ -43,6 +48,8 @@ const getFetcher: (requestOpts: RequestOpts) => typeof fetch =
4348
*/
4449
type AiChatContextResult = UseChatHelpers & {
4550
initialMessages: AiChatMessage[] | null
51+
additionalBody?: Record<string, string>
52+
setAdditionalBody?: (body: Record<string, string>) => void
4653
}
4754
const AiChatContext = createContext<AiChatContextResult | null>(null)
4855

@@ -66,8 +73,19 @@ const AiChatProvider: React.FC<AiChatContextProps> = ({
6673
)
6774
}, [_initialMessages])
6875

69-
const fetcher = useMemo(() => getFetcher(requestOpts), [requestOpts])
70-
const { messages: unparsed, ...others } = useChat({
76+
const [additionalBody, setAdditionalBody] = useState<Record<string, string>>(
77+
{},
78+
)
79+
80+
const fetcher = useMemo(
81+
() => getFetcher(requestOpts, additionalBody),
82+
[requestOpts, additionalBody],
83+
)
84+
const {
85+
messages: unparsed,
86+
setMessages,
87+
...others
88+
} = useChat({
7189
api: requestOpts.apiUrl,
7290
streamProtocol: "text",
7391
fetch: fetcher,
@@ -94,13 +112,31 @@ const AiChatProvider: React.FC<AiChatContextProps> = ({
94112
})
95113
}, [parseContent, unparsed, initialMessages])
96114

115+
const _setAdditionalBody = (body: Record<string, string>) => {
116+
setMessages([
117+
{
118+
role: "assistant",
119+
content: "Which question are you working on?",
120+
id: "initial-0",
121+
},
122+
])
123+
setAdditionalBody(body)
124+
}
125+
97126
return (
98127
<AiChatContext.Provider
99128
/**
100129
* Ensure that child state is reset when chatId changes.
101130
*/
102131
key={chatId}
103-
value={{ initialMessages, messages, ...others }}
132+
value={{
133+
initialMessages,
134+
messages,
135+
additionalBody,
136+
setAdditionalBody: _setAdditionalBody,
137+
setMessages,
138+
...others,
139+
}}
104140
>
105141
{children}
106142
</AiChatContext.Provider>

src/components/AiChat/ChatTitle.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,22 @@ const AskTimTitle = styled.div(({ theme }) => ({
3737
width: "24px",
3838
height: "24px",
3939
},
40+
"&& p": {
41+
margin: 0,
42+
},
4043
}))
4144

4245
type ChatTitleProps = {
4346
askTimTitle?: string
4447
externalScroll?: boolean
48+
control?: React.ReactNode
4549
className?: string
4650
}
4751

4852
const ChatTitle = ({
4953
askTimTitle,
5054
externalScroll,
55+
control,
5156
className,
5257
}: ChatTitleProps) => {
5358
if (!askTimTitle) return null
@@ -60,6 +65,7 @@ const ChatTitle = ({
6065
{askTimTitle}
6166
</Typography>
6267
</AskTimTitle>
68+
{control}
6369
</Container>
6470
)
6571
}

0 commit comments

Comments
 (0)