Skip to content

Commit d8b2e66

Browse files
feat: Add aichat bundle (#142)
1 parent 54669a1 commit d8b2e66

File tree

13 files changed

+167
-9
lines changed

13 files changed

+167
-9
lines changed

bundle-preview/bundle-ai-chat.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
</head>
7+
<body>
8+
<div id="ai-chat-container"></div>
9+
<script type="module">
10+
import * as aiChat from "./static/bundles/aiChat.es.js"
11+
12+
const requestOpts = {
13+
apiUrl: "https://api.rc.learn.mit.edu/ai/http/syllabus_agent/",
14+
transformBody: (messages) => {
15+
const body = {
16+
collection_name: "content_files",
17+
message: messages[messages.length - 1].content,
18+
course_id: "course-v1:xPRO+PCDEx",
19+
}
20+
return body
21+
},
22+
}
23+
24+
aiChat.init(
25+
{
26+
requestOpts,
27+
entryScreenEnabled: false,
28+
askTimTitle: "About this Course",
29+
initialMessages: [
30+
{
31+
role: "assistant",
32+
content: "How can I help you today?",
33+
},
34+
],
35+
},
36+
{
37+
container: document.getElementById("ai-chat-container"),
38+
},
39+
)
40+
</script>
41+
</body>
42+
</html>

bundle-preview/bundle-demo.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
<head>
44
<meta charset="utf-8" />
55
<title>Home</title>
6-
<script src="./static/bundles/aiDrawerManager.umd.js"></script>
76
<style>
87
.app-frame {
98
width: 600px;
@@ -12,11 +11,11 @@
1211
}
1312
</style>
1413
</head>
15-
1614
<body>
1715
<iframe class="app-frame" src="./iframe.html"></iframe>
1816
</body>
19-
<script>
17+
<script type="module">
18+
import * as aiDrawerManager from "./static/bundles/aiDrawerManager.es.js"
2019
/**
2120
* Accepts options of https://mitodl.github.io/smoot-design/?path=/docs/smoot-design-ai-aidrawermanager--docs
2221
*/

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
"typecheck": "tsc --noEmit",
1818
"build:esm": "tsc",
1919
"build:cjs": "tsc --module commonjs --outDir dist/cjs",
20-
"build:bundles": "vite build --outDir dist/bundles",
21-
"build:bundles:legacy": "vite build --config vite.config.legacy.mts --outDir dist/bundles",
20+
"build:bundles": "vite build",
21+
"build:bundles:legacy": "vite build --config vite.config.legacy.mts",
22+
"build:bundles:aiChat": "vite build --config vite.config.aiChat.mts",
2223
"build:type-augmentation": "cp -r src/type-augmentation dist/type-augmentation",
2324
"build": "./scripts/build.sh",
2425
"lint-check": "eslint src/ .storybook/",

scripts/build.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ rm -rf dist &&
88
npm run build:cjs &&
99
npm run build:type-augmentation &&
1010
npm run build:bundles &&
11-
npm run build:bundles:legacy
11+
npm run build:bundles:legacy &&
12+
npm run build:bundles:aiChat

src/bundles/aiChat.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as React from "react"
2+
import { createRoot } from "react-dom/client"
3+
import { AiChat } from "../components/AiChat/AiChat"
4+
import { ThemeProvider } from "../components/ThemeProvider/ThemeProvider"
5+
import type { AiChatProps } from "../ai"
6+
import styled from "@emotion/styled"
7+
8+
const createAndAppend = () => {
9+
const newContainer = document.createElement("div")
10+
document.body.appendChild(newContainer)
11+
return newContainer
12+
}
13+
14+
type InitOptions = {
15+
container?: HTMLElement
16+
}
17+
18+
const StyledAiChat = styled(AiChat)({
19+
/**
20+
* Unset some openedx styles that conflict with our chat styling.
21+
* There are probably other conflicts not handled here.
22+
*/
23+
"& input[type=text], & input[type=text]:focus": {
24+
border: "unset",
25+
boxShadow: "unset",
26+
},
27+
})
28+
29+
const init = (props: AiChatProps, { container }: InitOptions) => {
30+
const rootEl = container ?? createAndAppend()
31+
const root = createRoot(rootEl)
32+
root.render(
33+
<ThemeProvider>
34+
<StyledAiChat {...props} />
35+
</ThemeProvider>,
36+
)
37+
}
38+
39+
export { init }

src/components/AiChat/AiChat.test.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ describe("AiChat", () => {
6161
global.ResizeObserver = jest
6262
.fn()
6363
.mockImplementation(() => MockObserverInstance)
64+
65+
document.cookie = ""
6466
})
6567

6668
const setup = (props: Partial<AiChatProps> = {}) => {
@@ -283,6 +285,30 @@ describe("AiChat", () => {
283285
const messages = getMessages()
284286
expect(messages[0]).toHaveTextContent("Starter 1")
285287
})
288+
289+
test("csrfCookieName and csrfHeaderName are used to set CSRF token if provided", async () => {
290+
const csrfCookieName = "my-csrf-cookie"
291+
const csrfHeaderName = "My-Csrf-Header"
292+
document.cookie = `${csrfCookieName}=test-csrf-token`
293+
setup({
294+
requestOpts: {
295+
apiUrl: API_URL,
296+
csrfCookieName,
297+
csrfHeaderName,
298+
},
299+
})
300+
301+
await user.click(getConversationStarters()[0])
302+
303+
expect(window.fetch).toHaveBeenCalledWith(
304+
API_URL,
305+
expect.objectContaining({
306+
headers: expect.objectContaining({
307+
[csrfHeaderName]: "test-csrf-token",
308+
}),
309+
}),
310+
)
311+
})
286312
})
287313

288314
test("replaceMathjax replaces unsupported MathJax syntax", () => {

src/components/AiChat/AiChatContext.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useChat, UseChatHelpers } from "@ai-sdk/react"
33
import type { RequestOpts, AiChatMessage, AiChatContextProps } from "./types"
44
import { useMemo, createContext } from "react"
55
import retryingFetch from "../../utils/retryingFetch"
6+
import { getCookie } from "../../utils/getCookie"
67

78
const identity = <T,>(x: T): T => x
89

@@ -25,6 +26,14 @@ const getFetcher: (requestOpts: RequestOpts) => typeof fetch =
2526
...requestOpts.fetchOpts?.headers,
2627
},
2728
}
29+
30+
const { csrfCookieName, csrfHeaderName } = requestOpts
31+
if (csrfCookieName && csrfHeaderName) {
32+
options.headers = {
33+
...options.headers,
34+
[csrfHeaderName]: getCookie(csrfCookieName) ?? "",
35+
}
36+
}
2837
return retryingFetch(url, options)
2938
}
3039

src/components/AiChat/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ type RequestOpts = {
2424
*/
2525
fetchOpts?: RequestInit
2626
onFinish?: (message: AiChatMessage) => void
27+
/**
28+
* Cookie name from which to read CSRF token.
29+
*/
30+
csrfCookieName?: string
31+
/**
32+
* Header name to which to write CSRF token.
33+
*/
34+
csrfHeaderName?: string
2735
}
2836

2937
type AiChatContextProps = {

src/components/Input/Input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ const AdornmentButtonStyled = styled.button(({ theme, disabled }) => ({
237237
fill: theme.custom.colors.silverGray,
238238
},
239239
},
240-
":hover": {
240+
":hover:not(:disabled)": {
241241
background: disabled ? "inherit" : "rgba(0, 0, 0, 0.06)",
242242
},
243243
color: theme.custom.colors.silverGray,

src/utils/getCookie.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const getCookie = (name: string): string | undefined => {
2+
const value = `; ${document.cookie}`
3+
// value looks like `; name1=value1; name2=value2; name3=value3` etc
4+
const parts = value.split(`; ${name}=`)
5+
// Splitting at 2, parts look like
6+
// ["; name1=value1", "value2; name3=value3"]
7+
if (parts.length === 2) {
8+
// parts[1] looks like "value2; name3=value3"
9+
return parts[1]?.split(";")[0]
10+
}
11+
return undefined
12+
}
13+
14+
export { getCookie }

0 commit comments

Comments
 (0)