@mitodl/smoot-design 6.2.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/bundles/remoteTutorDrawer.es.js +15505 -16010
- package/dist/bundles/remoteTutorDrawer.umd.js +79 -52
- package/dist/cjs/ai.d.ts +3 -3
- package/dist/cjs/ai.js +5 -1
- package/dist/cjs/bundles/RemoteTutorDrawer/FlashcardsScreen.js +1 -1
- package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts +1 -2
- package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +12 -6
- package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.d.ts +1 -0
- package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +18 -1
- package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.test.js +6 -0
- package/dist/cjs/components/AiChat/AiChat.d.ts +3 -3
- package/dist/cjs/components/AiChat/AiChat.js +20 -41
- package/dist/cjs/components/AiChat/AiChatContext.d.ts +26 -0
- package/dist/cjs/components/AiChat/AiChatContext.js +105 -0
- package/dist/cjs/components/AiChat/AiChatContext.stories.d.ts +14 -0
- package/dist/cjs/components/AiChat/AiChatContext.stories.js +75 -0
- package/dist/cjs/components/AiChat/types.d.ts +15 -11
- package/dist/cjs/components/AiChat/types.js +1 -1
- package/dist/cjs/components/AiChat/utils.d.ts +1 -1
- package/dist/cjs/components/AiChat/utils.js +1 -1
- package/dist/cjs/components/LinkAdapter/LinkAdapter.js +1 -1
- package/dist/cjs/components/TabButtons/TabButtonList.js +1 -1
- package/dist/esm/ai.d.ts +3 -3
- package/dist/esm/ai.js +2 -1
- package/dist/esm/bundles/RemoteTutorDrawer/FlashcardsScreen.js +1 -1
- package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts +1 -2
- package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +12 -6
- package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.d.ts +1 -0
- package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +17 -0
- package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.test.js +6 -0
- package/dist/esm/components/AiChat/AiChat.d.ts +3 -3
- package/dist/esm/components/AiChat/AiChat.js +20 -42
- package/dist/esm/components/AiChat/AiChatContext.d.ts +26 -0
- package/dist/esm/components/AiChat/AiChatContext.js +101 -0
- package/dist/esm/components/AiChat/AiChatContext.stories.d.ts +14 -0
- package/dist/esm/components/AiChat/AiChatContext.stories.js +72 -0
- package/dist/esm/components/AiChat/types.d.ts +15 -11
- package/dist/esm/components/AiChat/types.js +1 -1
- package/dist/esm/components/AiChat/utils.d.ts +1 -1
- package/dist/esm/components/AiChat/utils.js +1 -1
- package/dist/esm/components/LinkAdapter/LinkAdapter.js +1 -1
- package/dist/esm/components/TabButtons/TabButtonList.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -7
package/dist/esm/ai.js
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export { AiChat } from "./components/AiChat/AiChat";
|
|
1
|
+
export { AiChat, AiChatDisplay } from "./components/AiChat/AiChat";
|
|
2
|
+
export { AiChatProvider, useAiChat } from "./components/AiChat/AiChatContext";
|
|
@@ -35,7 +35,7 @@ const Flashcard = React.forwardRef(({ content, "aria-label": ariaLabel }, ref) =
|
|
|
35
35
|
setScreen(screen === 0 ? 1 : 0);
|
|
36
36
|
}
|
|
37
37
|
};
|
|
38
|
-
return (React.createElement(FlashcardContainer, { ref: ref, onClick: () => setScreen(screen === 0 ? 1 : 0), onKeyDown: handleKeyDown, tabIndex: 0, "aria-label": ariaLabel },
|
|
38
|
+
return (React.createElement(FlashcardContainer, { ref: ref, onClick: () => setScreen(screen === 0 ? 1 : 0), onKeyDown: handleKeyDown, tabIndex: 0, "aria-label": ariaLabel, role: "button" },
|
|
39
39
|
React.createElement(Typography, { variant: "h5" }, screen === 0 ? `Q: ${content.question}` : `Answer: ${content.answer}`)));
|
|
40
40
|
});
|
|
41
41
|
Flashcard.displayName = "Flashcard";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { FC } from "react";
|
|
2
|
-
import { AiChatMessage } from "../../components/AiChat/types";
|
|
3
|
-
import type { AiChatProps } from "../../components/AiChat/AiChat";
|
|
2
|
+
import type { AiChatProps, AiChatMessage } from "../../components/AiChat/types";
|
|
4
3
|
type RemoteTutorDrawerInitMessage = {
|
|
5
4
|
type: "smoot-design::tutor-drawer-open";
|
|
6
5
|
payload: {
|
|
@@ -149,6 +149,12 @@ const useContentFetch = (contentUrl) => {
|
|
|
149
149
|
}, [contentUrl]);
|
|
150
150
|
return { response, loading };
|
|
151
151
|
};
|
|
152
|
+
const DEFAULT_PROBLEM_INITIAL_MESSAGES = [
|
|
153
|
+
{
|
|
154
|
+
role: "assistant",
|
|
155
|
+
content: "Let's try to work on this problem together. It would be great to hear how you're thinking about solving it. Can you walk me through the approach you're considering?",
|
|
156
|
+
},
|
|
157
|
+
];
|
|
152
158
|
const DEFAULT_VIDEO_ENTRY_SCREEN_TITLE = "What do you want to know about this video?";
|
|
153
159
|
const DEFAULT_VIDEO_STARTERS = [
|
|
154
160
|
{ content: "What are the most important concepts introduced in the video?" },
|
|
@@ -157,10 +163,10 @@ const DEFAULT_VIDEO_STARTERS = [
|
|
|
157
163
|
},
|
|
158
164
|
{ content: "What are the key terms introduced in this video?" },
|
|
159
165
|
];
|
|
160
|
-
const ChatComponent = ({ payload, transformBody, fetchOpts, scrollElement, entryScreenEnabled, entryScreenTitle, conversationStarters, hasTabs, }) => {
|
|
166
|
+
const ChatComponent = ({ payload, transformBody, fetchOpts, scrollElement, entryScreenEnabled, entryScreenTitle, conversationStarters, initialMessages, hasTabs, }) => {
|
|
161
167
|
if (!payload)
|
|
162
168
|
return null;
|
|
163
|
-
return (React.createElement(StyledAiChat, { chatId: payload.chatId, conversationStarters: conversationStarters, initialMessages:
|
|
169
|
+
return (React.createElement(StyledAiChat, { chatId: payload.chatId, conversationStarters: conversationStarters, initialMessages: initialMessages, scrollElement: scrollElement, entryScreenEnabled: entryScreenEnabled, entryScreenTitle: entryScreenTitle, requestOpts: {
|
|
164
170
|
transformBody: (messages) => (Object.assign(Object.assign({}, payload.requestBody), transformBody === null || transformBody === void 0 ? void 0 : transformBody(messages))),
|
|
165
171
|
apiUrl: payload.apiUrl,
|
|
166
172
|
fetchOpts: Object.assign(Object.assign({}, DEFAULT_FETCH_OPTS), fetchOpts),
|
|
@@ -245,24 +251,24 @@ const RemoteTutorDrawer = ({ messageOrigin, transformBody = identity, className,
|
|
|
245
251
|
md: "0 32px",
|
|
246
252
|
},
|
|
247
253
|
},
|
|
248
|
-
}, anchor: "right", open: open, onClose: () => setOpen(false) },
|
|
254
|
+
}, anchor: "right", open: open, onClose: () => setOpen(false), role: "dialog", "aria-modal": "true" },
|
|
249
255
|
React.createElement(Header, null,
|
|
250
256
|
React.createElement(Title, null,
|
|
251
257
|
payload.title ? React.createElement(RiSparkling2Line, null) : null,
|
|
252
|
-
React.createElement(Typography, { variant: "body1", component: "
|
|
258
|
+
React.createElement(Typography, { variant: "body1", component: "h1" }, ((_b = payload.title) === null || _b === void 0 ? void 0 : _b.includes("AskTIM")) ? (React.createElement(React.Fragment, null,
|
|
253
259
|
"Ask",
|
|
254
260
|
React.createElement("strong", null, "TIM"),
|
|
255
261
|
payload.title.replace("AskTIM", ""))) : (payload.title))),
|
|
256
262
|
React.createElement(CloseButton, { variant: "text", size: "medium", onClick: () => setOpen(false), "aria-label": "Close" },
|
|
257
263
|
React.createElement(RiCloseLine, null))),
|
|
258
|
-
blockType === "problem" ? (React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, entryScreenEnabled: (_d = (_c = payload.chat) === null || _c === void 0 ? void 0 : _c.entryScreenEnabled) !== null && _d !== void 0 ? _d : false, entryScreenTitle: payload.chat.entryScreenTitle, hasTabs: hasTabs })) : null,
|
|
264
|
+
blockType === "problem" ? (React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, entryScreenEnabled: (_d = (_c = payload.chat) === null || _c === void 0 ? void 0 : _c.entryScreenEnabled) !== null && _d !== void 0 ? _d : false, entryScreenTitle: payload.chat.entryScreenTitle, initialMessages: payload.chat.initialMessages || DEFAULT_PROBLEM_INITIAL_MESSAGES, hasTabs: hasTabs })) : null,
|
|
259
265
|
blockType === "video" ? (React.createElement(TabContext, { value: tab },
|
|
260
266
|
React.createElement(StyledTabButtonList, { styleVariant: "chat", onChange: (e, tab) => setTab(tab) },
|
|
261
267
|
React.createElement(TabButton, { value: "chat", label: "Chat" }),
|
|
262
268
|
((_e = response === null || response === void 0 ? void 0 : response.flashcards) === null || _e === void 0 ? void 0 : _e.length) ? (React.createElement(TabButton, { value: "flashcards", label: "Flashcards", onMouseDown: handleMouseDown, onFocus: handleFocus })) : null,
|
|
263
269
|
React.createElement(TabButton, { value: "summary", label: "Summary" })),
|
|
264
270
|
React.createElement(StyledTabPanel, { value: "chat", keepMounted: true },
|
|
265
|
-
React.createElement(ChatComponent, { payload: Object.assign({}, chat), transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, entryScreenEnabled: (_g = (_f = payload.chat) === null || _f === void 0 ? void 0 : _f.entryScreenEnabled) !== null && _g !== void 0 ? _g : true, entryScreenTitle: (_h = payload.chat.entryScreenTitle) !== null && _h !== void 0 ? _h : DEFAULT_VIDEO_ENTRY_SCREEN_TITLE, conversationStarters: conversationStarters, hasTabs: hasTabs })),
|
|
271
|
+
React.createElement(ChatComponent, { payload: Object.assign({}, chat), transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, entryScreenEnabled: (_g = (_f = payload.chat) === null || _f === void 0 ? void 0 : _f.entryScreenEnabled) !== null && _g !== void 0 ? _g : true, entryScreenTitle: (_h = payload.chat.entryScreenTitle) !== null && _h !== void 0 ? _h : DEFAULT_VIDEO_ENTRY_SCREEN_TITLE, conversationStarters: conversationStarters, initialMessages: payload.chat.initialMessages, hasTabs: hasTabs })),
|
|
266
272
|
((_j = response === null || response === void 0 ? void 0 : response.flashcards) === null || _j === void 0 ? void 0 : _j.length) ? (React.createElement(StyledTabPanel, { value: "flashcards" },
|
|
267
273
|
React.createElement(FlashcardsScreen, { flashcards: response === null || response === void 0 ? void 0 : response.flashcards, wasKeyboardFocus: _wasKeyboardFocus }))) : null,
|
|
268
274
|
React.createElement(StyledTabPanel, { value: "summary" },
|
|
@@ -4,6 +4,7 @@ declare const meta: Meta<typeof RemoteTutorDrawer>;
|
|
|
4
4
|
export default meta;
|
|
5
5
|
type Story = StoryObj<typeof RemoteTutorDrawer>;
|
|
6
6
|
export declare const ProblemStory: Story;
|
|
7
|
+
export declare const ProblemDefaultInitialMessagesStory: Story;
|
|
7
8
|
export declare const EntryScreenStory: Story;
|
|
8
9
|
/**
|
|
9
10
|
* The chat entry screen is shown by default for the video blocks Tutor drawer.
|
|
@@ -51,6 +51,7 @@ const IFrame = ({ payload }) => {
|
|
|
51
51
|
};
|
|
52
52
|
const meta = {
|
|
53
53
|
title: "smoot-design/AI/RemoteTutorDrawer",
|
|
54
|
+
component: RemoteTutorDrawer,
|
|
54
55
|
render: ({ target }, { parameters: { payload } }) => (React.createElement(React.Fragment, null,
|
|
55
56
|
React.createElement(IFrame, { payload: payload }),
|
|
56
57
|
React.createElement(RemoteTutorDrawer, { target: target, messageOrigin: "http://localhost:6006" }))),
|
|
@@ -73,6 +74,22 @@ export const ProblemStory = {
|
|
|
73
74
|
},
|
|
74
75
|
},
|
|
75
76
|
};
|
|
77
|
+
export const ProblemDefaultInitialMessagesStory = {
|
|
78
|
+
args: {
|
|
79
|
+
target: "problem-frame-default-initial-messages",
|
|
80
|
+
},
|
|
81
|
+
parameters: {
|
|
82
|
+
payload: {
|
|
83
|
+
blockType: "problem",
|
|
84
|
+
target: "problem-frame-default-initial-messages",
|
|
85
|
+
title: "AskTIM for help with Problem: Derivatives 1.1",
|
|
86
|
+
chat: {
|
|
87
|
+
apiUrl: TEST_API_STREAMING,
|
|
88
|
+
conversationStarters: STARTERS,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
76
93
|
export const EntryScreenStory = {
|
|
77
94
|
args: {
|
|
78
95
|
target: "entry-screen-frame",
|
|
@@ -14,6 +14,12 @@ import { ThemeProvider } from "../../components/ThemeProvider/ThemeProvider";
|
|
|
14
14
|
import * as React from "react";
|
|
15
15
|
import { http, HttpResponse } from "msw";
|
|
16
16
|
import { setupServer } from "msw/node";
|
|
17
|
+
jest.mock("react-markdown", () => {
|
|
18
|
+
return {
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: ({ children }) => React.createElement("div", null, children),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
17
23
|
const TEST_API_STREAMING = "http://localhost:4567/test";
|
|
18
24
|
const CONTENT_FILE_URL = "http://localhost:4567/api/v1/contentfiles/1";
|
|
19
25
|
const CONTENT_RESPONSE = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FC } from "react";
|
|
2
|
-
import type { AiChatProps } from "./types";
|
|
2
|
+
import type { AiChatDisplayProps, AiChatProps } from "./types";
|
|
3
|
+
declare const AiChatDisplay: FC<AiChatDisplayProps>;
|
|
3
4
|
declare const AiChat: FC<AiChatProps>;
|
|
4
|
-
export { AiChat };
|
|
5
|
-
export type { AiChatProps };
|
|
5
|
+
export { AiChatDisplay, AiChat };
|
|
@@ -10,21 +10,21 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import * as React from "react";
|
|
13
|
-
import { useEffect, useRef, useState
|
|
13
|
+
import { useEffect, useRef, useState } from "react";
|
|
14
14
|
import styled from "@emotion/styled";
|
|
15
15
|
import Typography from "@mui/material/Typography";
|
|
16
16
|
import classNames from "classnames";
|
|
17
17
|
import { RiSendPlaneFill, RiStopFill, RiMoreFill } from "@remixicon/react";
|
|
18
18
|
import { Input, AdornmentButton } from "../Input/Input";
|
|
19
|
-
import { EntryScreen } from "./EntryScreen";
|
|
20
19
|
import Markdown from "react-markdown";
|
|
21
20
|
import { ScrollSnap } from "../ScrollSnap/ScrollSnap";
|
|
22
21
|
import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer";
|
|
23
22
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden";
|
|
24
23
|
import { Alert } from "../Alert/Alert";
|
|
25
24
|
import { ChatTitle } from "./ChatTitle";
|
|
26
|
-
import { useAiChat } from "./
|
|
25
|
+
import { AiChatProvider, useAiChat } from "./AiChatContext";
|
|
27
26
|
import { useScrollSnap } from "../ScrollSnap/useScrollSnap";
|
|
27
|
+
import { EntryScreen } from "./EntryScreen";
|
|
28
28
|
const classes = {
|
|
29
29
|
root: "MitAiChat--root",
|
|
30
30
|
title: "MitAiChat--title",
|
|
@@ -62,6 +62,9 @@ const MessagesContainer = styled(ScrollSnap)(({ externalScroll }) => ({
|
|
|
62
62
|
padding: "14px 0",
|
|
63
63
|
overflow: externalScroll ? "visible" : "auto",
|
|
64
64
|
gap: "16px",
|
|
65
|
+
[`> .${classes.messageRowAssistant}:first-child`]: {
|
|
66
|
+
marginTop: "16px",
|
|
67
|
+
},
|
|
65
68
|
}));
|
|
66
69
|
const MessageRow = styled.div({
|
|
67
70
|
display: "flex",
|
|
@@ -75,7 +78,7 @@ const MessageRow = styled.div({
|
|
|
75
78
|
},
|
|
76
79
|
position: "relative",
|
|
77
80
|
});
|
|
78
|
-
const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ color: theme.custom.colors.darkGray2, backgroundColor: theme.custom.colors.white
|
|
81
|
+
const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ color: theme.custom.colors.darkGray2, backgroundColor: theme.custom.colors.white }, theme.typography.body2), { "p:first-of-type": {
|
|
79
82
|
marginTop: 0,
|
|
80
83
|
}, "p:last-of-type": {
|
|
81
84
|
marginBottom: 0,
|
|
@@ -91,9 +94,8 @@ const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ color:
|
|
|
91
94
|
}, a: {
|
|
92
95
|
color: theme.custom.colors.red,
|
|
93
96
|
fontWeight: "normal",
|
|
94
|
-
}, borderRadius: "12px", [`.${classes.
|
|
95
|
-
padding: "12px 16px
|
|
96
|
-
}, [`.${classes.messageRowUser} &`]: {
|
|
97
|
+
}, borderRadius: "12px", [`.${classes.messageRowUser} &`]: {
|
|
98
|
+
padding: "12px 16px",
|
|
97
99
|
borderRadius: "8px 0px 8px 8px",
|
|
98
100
|
backgroundColor: theme.custom.colors.lightGray1,
|
|
99
101
|
} })));
|
|
@@ -128,47 +130,27 @@ const Disclaimer = styled(Typography)(({ theme }) => ({
|
|
|
128
130
|
marginTop: "16px",
|
|
129
131
|
textAlign: "center",
|
|
130
132
|
}));
|
|
131
|
-
const
|
|
133
|
+
const AiChatDisplay = (_a) => {
|
|
132
134
|
var _b, _c;
|
|
133
|
-
var {
|
|
135
|
+
var { conversationStarters, askTimTitle, entryScreenEnabled = true, entryScreenTitle, srLoadingMessages, placeholder = "", className, scrollElement, ref } = _a, others = __rest(_a, ["conversationStarters", "askTimTitle", "entryScreenEnabled", "entryScreenTitle", "srLoadingMessages", "placeholder", "className", "scrollElement", "ref"]) // Could contain data attributes
|
|
134
136
|
;
|
|
135
137
|
const containerRef = useRef(null);
|
|
136
138
|
const messagesContainerRef = useRef(null);
|
|
137
|
-
const [showEntryScreen, setShowEntryScreen] = useState(entryScreenEnabled);
|
|
138
139
|
const chatScreenRef = useRef(null);
|
|
139
|
-
const [initialMessages, setInitialMessages] = useState();
|
|
140
140
|
const promptInputRef = useRef(null);
|
|
141
|
-
const { messages
|
|
142
|
-
initialMessages,
|
|
143
|
-
id: chatId,
|
|
144
|
-
});
|
|
141
|
+
const { messages, input, handleInputChange, handleSubmit, append, isLoading, stop, error, initialMessages, } = useAiChat();
|
|
145
142
|
useScrollSnap({
|
|
146
143
|
scrollElement: scrollElement || messagesContainerRef.current,
|
|
147
144
|
contentElement: scrollElement ? messagesContainerRef.current : null,
|
|
148
145
|
threshold: 200,
|
|
149
146
|
});
|
|
150
|
-
|
|
151
|
-
if (_initialMessages) {
|
|
152
|
-
const prefix = Math.random().toString().slice(2);
|
|
153
|
-
setInitialMessages(_initialMessages.map((m, i) => (Object.assign(Object.assign({}, m), { id: `initial-${prefix}-${i}` }))));
|
|
154
|
-
}
|
|
155
|
-
}, [_initialMessages]);
|
|
147
|
+
const [showEntryScreen, setShowEntryScreen] = useState(entryScreenEnabled);
|
|
156
148
|
useEffect(() => {
|
|
157
149
|
var _a, _b;
|
|
158
150
|
if (!showEntryScreen) {
|
|
159
151
|
(_b = (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.querySelector("input")) === null || _b === void 0 ? void 0 : _b.focus();
|
|
160
152
|
}
|
|
161
153
|
}, [showEntryScreen]);
|
|
162
|
-
const messages = useMemo(() => {
|
|
163
|
-
const initial = initialMessages === null || initialMessages === void 0 ? void 0 : initialMessages.map((m) => m.id);
|
|
164
|
-
return unparsed.map((m) => {
|
|
165
|
-
if (m.role === "assistant" && !(initial === null || initial === void 0 ? void 0 : initial.includes(m.id))) {
|
|
166
|
-
const content = parseContent ? parseContent(m.content) : m.content;
|
|
167
|
-
return Object.assign(Object.assign({}, m), { content });
|
|
168
|
-
}
|
|
169
|
-
return m;
|
|
170
|
-
});
|
|
171
|
-
}, [parseContent, unparsed, initialMessages]);
|
|
172
154
|
const showStarters = messages.length === ((initialMessages === null || initialMessages === void 0 ? void 0 : initialMessages.length) || 0);
|
|
173
155
|
const waiting = !showStarters && !error && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
174
156
|
const stoppable = isLoading && ((_c = messages[messages.length - 1]) === null || _c === void 0 ? void 0 : _c.role) !== "user";
|
|
@@ -181,16 +163,7 @@ const AiChat = (_a) => {
|
|
|
181
163
|
};
|
|
182
164
|
const lastMsg = messages[messages.length - 1];
|
|
183
165
|
const externalScroll = !!scrollElement;
|
|
184
|
-
return (React.createElement(Container, { className: className, ref: containerRef,
|
|
185
|
-
/**
|
|
186
|
-
* Changing the `useChat` chatId seems to persist some state between
|
|
187
|
-
* hook calls. This can cause strange effects like loading API responses
|
|
188
|
-
* for previous chatId into new chatId.
|
|
189
|
-
*
|
|
190
|
-
* To avoid this, let's change the key, this will force React to make a new component
|
|
191
|
-
* not sharing any of the old state.
|
|
192
|
-
*/
|
|
193
|
-
key: chatId }, showEntryScreen ? (React.createElement(EntryScreen, { className: classes.entryScreenContainer, title: entryScreenTitle, conversationStarters: conversationStarters, onPromptSubmit: (prompt) => {
|
|
166
|
+
return (React.createElement(Container, { className: className, ref: containerRef }, showEntryScreen ? (React.createElement(EntryScreen, { className: classes.entryScreenContainer, title: entryScreenTitle, conversationStarters: conversationStarters, onPromptSubmit: (prompt) => {
|
|
194
167
|
if (prompt.trim() === "") {
|
|
195
168
|
return;
|
|
196
169
|
}
|
|
@@ -241,4 +214,9 @@ const AiChat = (_a) => {
|
|
|
241
214
|
React.createElement(Disclaimer, { variant: "body3" }, "AI-generated content may be incorrect.")),
|
|
242
215
|
React.createElement(SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: (lastMsg === null || lastMsg === void 0 ? void 0 : lastMsg.role) === "assistant" ? lastMsg === null || lastMsg === void 0 ? void 0 : lastMsg.content : "" }))))));
|
|
243
216
|
};
|
|
244
|
-
|
|
217
|
+
const AiChat = (_a) => {
|
|
218
|
+
var { requestOpts, initialMessages, chatId, parseContent } = _a, displayProps = __rest(_a, ["requestOpts", "initialMessages", "chatId", "parseContent"]);
|
|
219
|
+
return (React.createElement(AiChatProvider, { requestOpts: requestOpts, chatId: chatId, initialMessages: initialMessages, parseContent: parseContent },
|
|
220
|
+
React.createElement(AiChatDisplay, Object.assign({}, displayProps))));
|
|
221
|
+
};
|
|
222
|
+
export { AiChatDisplay, AiChat };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { UseChatHelpers } from "@ai-sdk/react";
|
|
3
|
+
import type { AiChatMessage, AiChatContextProps } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* All of `@ai-sdk/react`'s [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat)
|
|
6
|
+
* results, plus the initial messages.
|
|
7
|
+
*/
|
|
8
|
+
type AiChatContextResult = UseChatHelpers & {
|
|
9
|
+
initialMessages: AiChatMessage[] | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Provides AiChatContext to its children. Within this provider, you can consume
|
|
13
|
+
* the AiChatContext using the `useAiChat` hook.
|
|
14
|
+
*/
|
|
15
|
+
declare const AiChatProvider: React.FC<AiChatContextProps>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the AiChatContext, which includes all results from `@ai-sdk/react`'s
|
|
18
|
+
* [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) hook as
|
|
19
|
+
* well as the initial messages.
|
|
20
|
+
*
|
|
21
|
+
* In addition to customizing the fetcher, using a context allows us to avoid
|
|
22
|
+
* this issue https://github.com/vercel/ai/issues/3266 since the caller no
|
|
23
|
+
* longer needs to provide the initial messages.
|
|
24
|
+
*/
|
|
25
|
+
declare const useAiChat: () => AiChatContextResult;
|
|
26
|
+
export { useAiChat, AiChatProvider };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
11
|
+
var t = {};
|
|
12
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
13
|
+
t[p] = s[p];
|
|
14
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
15
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
16
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
17
|
+
t[p[i]] = s[p[i]];
|
|
18
|
+
}
|
|
19
|
+
return t;
|
|
20
|
+
};
|
|
21
|
+
import * as React from "react";
|
|
22
|
+
import { useChat } from "@ai-sdk/react";
|
|
23
|
+
import { useMemo, createContext } from "react";
|
|
24
|
+
const identity = (x) => x;
|
|
25
|
+
const getFetcher = (requestOpts) => (url, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
26
|
+
var _a, _b;
|
|
27
|
+
if (typeof (opts === null || opts === void 0 ? void 0 : opts.body) !== "string") {
|
|
28
|
+
console.error("Unexpected body type.");
|
|
29
|
+
return window.fetch(url, opts);
|
|
30
|
+
}
|
|
31
|
+
const messages = JSON.parse(opts === null || opts === void 0 ? void 0 : opts.body).messages;
|
|
32
|
+
const transformBody = (_a = requestOpts.transformBody) !== null && _a !== void 0 ? _a : identity;
|
|
33
|
+
const options = Object.assign(Object.assign(Object.assign(Object.assign({}, opts), { body: JSON.stringify(transformBody(messages)) }), requestOpts.fetchOpts), { headers: Object.assign(Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts.headers), { "Content-Type": "application/json" }), (_b = requestOpts.fetchOpts) === null || _b === void 0 ? void 0 : _b.headers) });
|
|
34
|
+
return fetch(url, options);
|
|
35
|
+
});
|
|
36
|
+
const AiChatContext = createContext(null);
|
|
37
|
+
/**
|
|
38
|
+
* Provides AiChatContext to its children. Within this provider, you can consume
|
|
39
|
+
* the AiChatContext using the `useAiChat` hook.
|
|
40
|
+
*/
|
|
41
|
+
const AiChatProvider = ({ initialMessages: _initialMessages, requestOpts, chatId, parseContent, children, }) => {
|
|
42
|
+
const initialMessages = useMemo(() => {
|
|
43
|
+
var _a;
|
|
44
|
+
return ((_a = _initialMessages === null || _initialMessages === void 0 ? void 0 : _initialMessages.map((message, i) => (Object.assign(Object.assign({}, message), { id: `initial-${i}` })))) !== null && _a !== void 0 ? _a : []);
|
|
45
|
+
}, [_initialMessages]);
|
|
46
|
+
const fetcher = useMemo(() => getFetcher(requestOpts), [requestOpts]);
|
|
47
|
+
const _a = useChat({
|
|
48
|
+
api: requestOpts.apiUrl,
|
|
49
|
+
streamProtocol: "text",
|
|
50
|
+
fetch: fetcher,
|
|
51
|
+
onFinish: (message) => {
|
|
52
|
+
var _a;
|
|
53
|
+
if (!requestOpts.onFinish)
|
|
54
|
+
return;
|
|
55
|
+
if (message.role === "assistant" || message.role === "user") {
|
|
56
|
+
(_a = requestOpts.onFinish) === null || _a === void 0 ? void 0 : _a.call(requestOpts, message);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.info("Unexpected message role.", message);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
initialMessages,
|
|
63
|
+
id: chatId,
|
|
64
|
+
}), { messages: unparsed } = _a, others = __rest(_a, ["messages"]);
|
|
65
|
+
const messages = useMemo(() => {
|
|
66
|
+
const initial = initialMessages === null || initialMessages === void 0 ? void 0 : initialMessages.map((m) => m.id);
|
|
67
|
+
return unparsed.map((m) => {
|
|
68
|
+
if (m.role === "assistant" && !(initial === null || initial === void 0 ? void 0 : initial.includes(m.id))) {
|
|
69
|
+
const content = parseContent ? parseContent(m.content) : m.content;
|
|
70
|
+
return Object.assign(Object.assign({}, m), { content });
|
|
71
|
+
}
|
|
72
|
+
return m;
|
|
73
|
+
});
|
|
74
|
+
}, [parseContent, unparsed, initialMessages]);
|
|
75
|
+
return (React.createElement(AiChatContext.Provider
|
|
76
|
+
/**
|
|
77
|
+
* Ensure that child state is reset when chatId changes.
|
|
78
|
+
*/
|
|
79
|
+
, {
|
|
80
|
+
/**
|
|
81
|
+
* Ensure that child state is reset when chatId changes.
|
|
82
|
+
*/
|
|
83
|
+
key: chatId, value: Object.assign({ initialMessages, messages }, others) }, children));
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Returns the AiChatContext, which includes all results from `@ai-sdk/react`'s
|
|
87
|
+
* [`useChat`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) hook as
|
|
88
|
+
* well as the initial messages.
|
|
89
|
+
*
|
|
90
|
+
* In addition to customizing the fetcher, using a context allows us to avoid
|
|
91
|
+
* this issue https://github.com/vercel/ai/issues/3266 since the caller no
|
|
92
|
+
* longer needs to provide the initial messages.
|
|
93
|
+
*/
|
|
94
|
+
const useAiChat = () => {
|
|
95
|
+
const context = React.useContext(AiChatContext);
|
|
96
|
+
if (!context) {
|
|
97
|
+
throw new Error("useAiChatContext must be used within an AiChatProvider");
|
|
98
|
+
}
|
|
99
|
+
return context;
|
|
100
|
+
};
|
|
101
|
+
export { useAiChat, AiChatProvider };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { AiChatProvider } from "./AiChatContext";
|
|
3
|
+
/**
|
|
4
|
+
* AiChatProvider provides state and functions for managing chat. The higher-level
|
|
5
|
+
* `AiChat` component is a wrapper around this provider and the `AiChatDisplay`,
|
|
6
|
+
* roughly.
|
|
7
|
+
*
|
|
8
|
+
* If you need to access chat state outside of the chat display, you can use
|
|
9
|
+
* `AiChatProvider` directly.
|
|
10
|
+
*/
|
|
11
|
+
declare const meta: Meta<typeof AiChatProvider>;
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof AiChatProvider>;
|
|
14
|
+
export declare const StreamingResponses: Story;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { AiChatDisplay } from "./AiChat";
|
|
3
|
+
import { AiChatProvider, useAiChat } from "./AiChatContext";
|
|
4
|
+
import styled from "@emotion/styled";
|
|
5
|
+
import { handlers } from "./test-utils/api";
|
|
6
|
+
import Typography from "@mui/material/Typography";
|
|
7
|
+
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
8
|
+
const INITIAL_MESSAGES = [
|
|
9
|
+
{
|
|
10
|
+
content: "Hi! What are you interested in learning about?",
|
|
11
|
+
role: "assistant",
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
const STARTERS = [
|
|
15
|
+
{ content: "I'm interested in quantum computing" },
|
|
16
|
+
{ content: "I want to understand global warming. " },
|
|
17
|
+
{ content: "I am curious about AI applications for business" },
|
|
18
|
+
];
|
|
19
|
+
const Container = styled.div({
|
|
20
|
+
width: "100%",
|
|
21
|
+
height: "400px",
|
|
22
|
+
position: "relative",
|
|
23
|
+
});
|
|
24
|
+
const MessageCounter = () => {
|
|
25
|
+
const { messages } = useAiChat();
|
|
26
|
+
return (React.createElement(Typography, { variant: "subtitle1" },
|
|
27
|
+
"Message count: ",
|
|
28
|
+
messages.length,
|
|
29
|
+
" (Provided by ",
|
|
30
|
+
React.createElement("code", null, "AiChatContext"),
|
|
31
|
+
")"));
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* AiChatProvider provides state and functions for managing chat. The higher-level
|
|
35
|
+
* `AiChat` component is a wrapper around this provider and the `AiChatDisplay`,
|
|
36
|
+
* roughly.
|
|
37
|
+
*
|
|
38
|
+
* If you need to access chat state outside of the chat display, you can use
|
|
39
|
+
* `AiChatProvider` directly.
|
|
40
|
+
*/
|
|
41
|
+
const meta = {
|
|
42
|
+
title: "smoot-design/AI/AiChatContext",
|
|
43
|
+
component: AiChatProvider,
|
|
44
|
+
parameters: {
|
|
45
|
+
msw: { handlers },
|
|
46
|
+
},
|
|
47
|
+
render: (args) => {
|
|
48
|
+
return (React.createElement(AiChatProvider, Object.assign({}, args),
|
|
49
|
+
React.createElement(MessageCounter, null),
|
|
50
|
+
React.createElement(Container, null,
|
|
51
|
+
React.createElement(AiChatDisplay, { entryScreenEnabled: false, conversationStarters: STARTERS, placeholder: "Type your message here", askTimTitle: "Ask TIM" }))));
|
|
52
|
+
},
|
|
53
|
+
decorators: (Story) => {
|
|
54
|
+
return (React.createElement(Container, null,
|
|
55
|
+
React.createElement(Story, null)));
|
|
56
|
+
},
|
|
57
|
+
args: {
|
|
58
|
+
requestOpts: { apiUrl: TEST_API_STREAMING },
|
|
59
|
+
initialMessages: INITIAL_MESSAGES,
|
|
60
|
+
},
|
|
61
|
+
argTypes: {
|
|
62
|
+
initialMessages: {
|
|
63
|
+
control: { type: "object", disable: true },
|
|
64
|
+
},
|
|
65
|
+
requestOpts: {
|
|
66
|
+
control: { type: "object", disable: true },
|
|
67
|
+
table: { readonly: true }, // See above
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
export default meta;
|
|
72
|
+
export const StreamingResponses = {};
|
|
@@ -20,12 +20,24 @@ type RequestOpts = {
|
|
|
20
20
|
fetchOpts?: RequestInit;
|
|
21
21
|
onFinish?: (message: AiChatMessage) => void;
|
|
22
22
|
};
|
|
23
|
-
type
|
|
23
|
+
type AiChatContextProps = {
|
|
24
24
|
/**
|
|
25
25
|
* Changing the `chatId` will reset the chat. Changing the `chatId` to a
|
|
26
26
|
* previously used value will restore the session state.
|
|
27
27
|
*/
|
|
28
28
|
chatId?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Options for making requests to the AI service.
|
|
31
|
+
*/
|
|
32
|
+
requestOpts: RequestOpts;
|
|
33
|
+
parseContent?: (content: unknown) => string;
|
|
34
|
+
/**
|
|
35
|
+
* Initial messages to display on the chat. If not provided, the entry screen title will be used as the initial message.
|
|
36
|
+
*/
|
|
37
|
+
initialMessages?: Omit<AiChatMessage, "id">[];
|
|
38
|
+
children?: React.ReactNode;
|
|
39
|
+
};
|
|
40
|
+
type AiChatDisplayProps = {
|
|
29
41
|
/**
|
|
30
42
|
* If provided, renders the "AskTIM" title motif followed by the text.
|
|
31
43
|
*/
|
|
@@ -44,21 +56,12 @@ type AiChatProps = {
|
|
|
44
56
|
* Title to display on the entry screen, also the initial assistant message if not overridden by `initialMessages`.
|
|
45
57
|
*/
|
|
46
58
|
entryScreenTitle?: string;
|
|
47
|
-
/**
|
|
48
|
-
* Initial messages to display on the chat. If not provided, the entry screen title will be used as the initial message.
|
|
49
|
-
*/
|
|
50
|
-
initialMessages?: Omit<AiChatMessage, "id">[];
|
|
51
59
|
/**
|
|
52
60
|
* Prompt suggestions for the user, clickable on the entry screen or in the chat if the entry screen is not enabled.
|
|
53
61
|
*/
|
|
54
62
|
conversationStarters?: {
|
|
55
63
|
content: string;
|
|
56
64
|
}[];
|
|
57
|
-
/**
|
|
58
|
-
* Options for making requests to the AI service.
|
|
59
|
-
*/
|
|
60
|
-
requestOpts: RequestOpts;
|
|
61
|
-
parseContent?: (content: unknown) => string;
|
|
62
65
|
/**
|
|
63
66
|
* A message to display while the component is in a loading state.
|
|
64
67
|
*
|
|
@@ -79,4 +82,5 @@ type AiChatProps = {
|
|
|
79
82
|
*/
|
|
80
83
|
scrollElement?: HTMLElement | null;
|
|
81
84
|
} & RefAttributes<HTMLDivElement>;
|
|
82
|
-
|
|
85
|
+
type AiChatProps = AiChatContextProps & AiChatDisplayProps;
|
|
86
|
+
export type { RequestOpts, AiChatMessage, AiChatContextProps, AiChatDisplayProps, AiChatProps, };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// Some of these are based on (compatible, but simplified / restricted) versions of ai/react types.
|
|
1
|
+
// Some of these are based on (compatible, but simplified / restricted) versions of @ai-sdk/react types.
|
|
2
2
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UseChatOptions } from "ai/react";
|
|
1
|
+
import { UseChatOptions } from "@ai-sdk/react";
|
|
2
2
|
import type { RequestOpts } from "./types";
|
|
3
3
|
declare const useAiChat: (requestOpts: RequestOpts, opts: UseChatOptions) => import("@ai-sdk/react").UseChatHelpers & {
|
|
4
4
|
addToolResult: ({ toolCallId, result, }: {
|
|
@@ -7,7 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { useChat } from "ai/react";
|
|
10
|
+
import { useChat } from "@ai-sdk/react";
|
|
11
11
|
import { useMemo } from "react";
|
|
12
12
|
const identity = (x) => x;
|
|
13
13
|
const getFetcher = (requestOpts) => (url, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
@@ -26,6 +26,6 @@ const LinkAdapter = React.forwardRef(function LinkAdapter(_a, ref) {
|
|
|
26
26
|
var { Component } = _a, props = __rest(_a, ["Component"]);
|
|
27
27
|
const theme = useTheme();
|
|
28
28
|
const LinkComponent = Component !== null && Component !== void 0 ? Component : theme.custom.LinkAdapter;
|
|
29
|
-
return React.createElement(PlainLink, Object.assign({ as: LinkComponent, ref: ref }, props));
|
|
29
|
+
return (React.createElement(PlainLink, Object.assign({ as: LinkComponent, ref: ref }, props), props.children));
|
|
30
30
|
});
|
|
31
31
|
export { LinkAdapter };
|
|
@@ -73,7 +73,7 @@ const TabButtonInner = React.forwardRef(
|
|
|
73
73
|
TabButtonInner.displayName = "TabButtonInner";
|
|
74
74
|
const TabLinkInner = React.forwardRef((props, ref) => {
|
|
75
75
|
const { className } = props, others = __rest(props, ["className"]);
|
|
76
|
-
return React.createElement(TabLinkStyled, Object.assign({}, defaultTabButtonProps, others, { ref: ref }));
|
|
76
|
+
return (React.createElement(TabLinkStyled, Object.assign({}, defaultTabButtonProps, others, { ref: ref }), props.children));
|
|
77
77
|
});
|
|
78
78
|
TabLinkInner.displayName = "TabLinkInner";
|
|
79
79
|
/**
|