Skip to content

Commit cf541fb

Browse files
refactor AiChat to Provider + Display
1 parent a299ba5 commit cf541fb

File tree

7 files changed

+275
-78
lines changed

7 files changed

+275
-78
lines changed

src/ai.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1-
export { AiChat } from "./components/AiChat/AiChat"
2-
export type { AiChatProps } from "./components/AiChat/AiChat"
3-
export type { AiChatMessage } from "./components/AiChat/types"
1+
export { AiChat, AiChatDisplay } from "./components/AiChat/AiChat"
2+
export { AiChatProvider, useAiChat } from "./components/AiChat/AiChatContext"
3+
export type {
4+
AiChatMessage,
5+
AiChatContextProps,
6+
AiChatDisplayProps,
7+
AiChatProps,
8+
} from "./components/AiChat/types"

src/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const IFrame = ({ payload }: { payload: InitPayload }) => {
7474

7575
const meta: Meta<typeof RemoteTutorDrawer> = {
7676
title: "smoot-design/AI/RemoteTutorDrawer",
77+
component: RemoteTutorDrawer,
7778
render: ({ target }, { parameters: { payload } }) => (
7879
<>
7980
<IFrame payload={payload} />

src/bundles/RemoteTutorDrawer/RemoteTutorDrawer.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ import Typography from "@mui/material/Typography"
1414
import TabContext from "@mui/lab/TabContext"
1515
import TabPanel from "@mui/lab/TabPanel"
1616
import { AiChat } from "../../components/AiChat/AiChat"
17-
import { AiChatMessage } from "../../components/AiChat/types"
18-
import type { AiChatProps } from "../../components/AiChat/AiChat"
17+
import type { AiChatProps, AiChatMessage } from "../../components/AiChat/types"
1918
import { ActionButton } from "../../components/Button/ActionButton"
2019
import { FlashcardsScreen } from "./FlashcardsScreen"
2120
import type { Flashcard } from "./FlashcardsScreen"

src/components/AiChat/AiChat.tsx

Lines changed: 32 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,21 @@
11
import * as React from "react"
2-
import { useEffect, useRef, useState, useMemo } from "react"
2+
import { useEffect, useRef, useState } from "react"
33
import type { FC } from "react"
44
import styled from "@emotion/styled"
55
import Typography from "@mui/material/Typography"
66
import classNames from "classnames"
77
import { RiSendPlaneFill, RiStopFill, RiMoreFill } from "@remixicon/react"
88
import { Input, AdornmentButton } from "../Input/Input"
9-
import type { AiChatMessage, AiChatProps } from "./types"
10-
import { EntryScreen } from "./EntryScreen"
9+
import type { AiChatDisplayProps, AiChatProps } from "./types"
1110
import Markdown from "react-markdown"
1211
import { ScrollSnap } from "../ScrollSnap/ScrollSnap"
1312
import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer"
1413
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden"
1514
import { Alert } from "../Alert/Alert"
1615
import { ChatTitle } from "./ChatTitle"
17-
import { useAiChat } from "./utils"
16+
import { AiChatProvider, useAiChat } from "./AiChatContext"
1817
import { useScrollSnap } from "../ScrollSnap/useScrollSnap"
19-
import type { Message } from "@ai-sdk/react"
18+
import { EntryScreen } from "./EntryScreen"
2019

2120
const classes = {
2221
root: "MitAiChat--root",
@@ -179,78 +178,48 @@ const Disclaimer = styled(Typography)(({ theme }) => ({
179178
textAlign: "center",
180179
}))
181180

182-
const AiChat: FC<AiChatProps> = ({
183-
entryScreenTitle,
184-
entryScreenEnabled = true,
181+
const AiChatDisplay: FC<AiChatDisplayProps> = ({
185182
conversationStarters,
186-
initialMessages: _initialMessages,
187183
askTimTitle,
188-
requestOpts,
189-
parseContent,
184+
entryScreenEnabled = true,
185+
entryScreenTitle,
190186
srLoadingMessages,
191187
placeholder = "",
192188
className,
193189
scrollElement,
194-
chatId,
195190
ref,
196191
...others // Could contain data attributes
197192
}) => {
198193
const containerRef = useRef<HTMLDivElement>(null)
199194
const messagesContainerRef = useRef<HTMLDivElement>(null)
200-
const [showEntryScreen, setShowEntryScreen] = useState(entryScreenEnabled)
201195
const chatScreenRef = useRef<HTMLDivElement>(null)
202-
const [initialMessages, setInitialMessages] = useState<AiChatMessage[]>()
203196
const promptInputRef = useRef<HTMLDivElement>(null)
204197

205198
const {
206-
messages: unparsed,
199+
messages,
207200
input,
208201
handleInputChange,
209202
handleSubmit,
210203
append,
211204
isLoading,
212205
stop,
213206
error,
214-
} = useAiChat(requestOpts, {
215207
initialMessages,
216-
id: chatId,
217-
})
208+
} = useAiChat()
218209

219210
useScrollSnap({
220211
scrollElement: scrollElement || messagesContainerRef.current,
221212
contentElement: scrollElement ? messagesContainerRef.current : null,
222213
threshold: 200,
223214
})
224215

225-
useEffect(() => {
226-
if (_initialMessages) {
227-
const prefix = Math.random().toString().slice(2)
228-
setInitialMessages(
229-
_initialMessages.map((m, i) => ({
230-
...m,
231-
id: `initial-${prefix}-${i}`,
232-
})),
233-
)
234-
}
235-
}, [_initialMessages])
236-
216+
const [showEntryScreen, setShowEntryScreen] = useState(entryScreenEnabled)
237217
useEffect(() => {
238218
if (!showEntryScreen) {
239219
promptInputRef.current?.querySelector("input")?.focus()
240220
}
241221
}, [showEntryScreen])
242222

243-
const messages = useMemo(() => {
244-
const initial = initialMessages?.map((m) => m.id)
245-
return unparsed.map((m: Message) => {
246-
if (m.role === "assistant" && !initial?.includes(m.id)) {
247-
const content = parseContent ? parseContent(m.content) : m.content
248-
return { ...m, content }
249-
}
250-
return m
251-
})
252-
}, [parseContent, unparsed, initialMessages])
253-
254223
const showStarters = messages.length === (initialMessages?.length || 0)
255224

256225
const waiting =
@@ -270,19 +239,7 @@ const AiChat: FC<AiChatProps> = ({
270239
const externalScroll = !!scrollElement
271240

272241
return (
273-
<Container
274-
className={className}
275-
ref={containerRef}
276-
/**
277-
* Changing the `useChat` chatId seems to persist some state between
278-
* hook calls. This can cause strange effects like loading API responses
279-
* for previous chatId into new chatId.
280-
*
281-
* To avoid this, let's change the key, this will force React to make a new component
282-
* not sharing any of the old state.
283-
*/
284-
key={chatId}
285-
>
242+
<Container className={className} ref={containerRef}>
286243
{showEntryScreen ? (
287244
<EntryScreen
288245
className={classes.entryScreenContainer}
@@ -318,7 +275,7 @@ const AiChat: FC<AiChatProps> = ({
318275
externalScroll={externalScroll}
319276
ref={messagesContainerRef}
320277
>
321-
{messages.map((m: Message) => (
278+
{messages.map((m) => (
322279
<MessageRow
323280
key={m.id}
324281
data-chat-role={m.role}
@@ -442,5 +399,23 @@ const AiChat: FC<AiChatProps> = ({
442399
)
443400
}
444401

445-
export { AiChat }
446-
export type { AiChatProps }
402+
const AiChat: FC<AiChatProps> = ({
403+
requestOpts,
404+
initialMessages,
405+
chatId,
406+
parseContent,
407+
...displayProps
408+
}) => {
409+
return (
410+
<AiChatProvider
411+
requestOpts={requestOpts}
412+
chatId={chatId}
413+
initialMessages={initialMessages}
414+
parseContent={parseContent}
415+
>
416+
<AiChatDisplay {...displayProps} />
417+
</AiChatProvider>
418+
)
419+
}
420+
421+
export { AiChatDisplay, AiChat }
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as React from "react"
2+
import type { Meta, StoryObj } from "@storybook/react"
3+
import { AiChatDisplay } from "./AiChat"
4+
import { AiChatProvider, useAiChat } from "./AiChatContext"
5+
import type { AiChatProps } from "./types"
6+
import styled from "@emotion/styled"
7+
import { handlers } from "./test-utils/api"
8+
import Typography from "@mui/material/Typography"
9+
10+
const TEST_API_STREAMING = "http://localhost:4567/streaming"
11+
12+
const INITIAL_MESSAGES: AiChatProps["initialMessages"] = [
13+
{
14+
content: "Hi! What are you interested in learning about?",
15+
role: "assistant",
16+
},
17+
]
18+
19+
const STARTERS = [
20+
{ content: "I'm interested in quantum computing" },
21+
{ content: "I want to understand global warming. " },
22+
{ content: "I am curious about AI applications for business" },
23+
]
24+
25+
const Container = styled.div({
26+
width: "100%",
27+
height: "400px",
28+
position: "relative",
29+
})
30+
31+
const MessageCounter = () => {
32+
const { messages } = useAiChat()
33+
34+
return (
35+
<Typography variant="subtitle1">
36+
Message count: {messages.length} (Provided by <code>AiChatContext</code>)
37+
</Typography>
38+
)
39+
}
40+
41+
/**
42+
* AiChatProvider provides state and functions for managing chat. The higher-level
43+
* `AiChat` component is a wrapper around this provider and the `AiChatDisplay`,
44+
* roughly.
45+
*
46+
* If you need to access chat state outside of the chat display, you can use
47+
* `AiChatProvider` directly.
48+
*/
49+
const meta: Meta<typeof AiChatProvider> = {
50+
title: "smoot-design/AI/AiChatContext",
51+
component: AiChatProvider,
52+
parameters: {
53+
msw: { handlers },
54+
},
55+
render: (args) => {
56+
return (
57+
<AiChatProvider {...args}>
58+
<MessageCounter />
59+
<Container>
60+
<AiChatDisplay
61+
entryScreenEnabled={false}
62+
conversationStarters={STARTERS}
63+
placeholder="Type your message here"
64+
askTimTitle="Ask TIM"
65+
/>
66+
</Container>
67+
</AiChatProvider>
68+
)
69+
},
70+
decorators: (Story) => {
71+
return (
72+
<Container>
73+
<Story />
74+
</Container>
75+
)
76+
},
77+
args: {
78+
requestOpts: { apiUrl: TEST_API_STREAMING },
79+
initialMessages: INITIAL_MESSAGES,
80+
},
81+
argTypes: {
82+
initialMessages: {
83+
control: { type: "object", disable: true },
84+
},
85+
requestOpts: {
86+
control: { type: "object", disable: true },
87+
table: { readonly: true }, // See above
88+
},
89+
},
90+
}
91+
92+
export default meta
93+
94+
type Story = StoryObj<typeof AiChatProvider>
95+
96+
export const StreamingResponses: Story = {}

0 commit comments

Comments
 (0)