@mitodl/smoot-design 2.0.1 → 3.0.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/dist/cjs/components/AiChat/AiChat.js +150 -58
- package/dist/cjs/components/AiChat/AiChat.stories.js +5 -2
- package/dist/cjs/components/AiChat/AiChat.test.js +39 -7
- package/dist/cjs/components/AiChat/story-utils.js +6 -3
- package/dist/cjs/components/AiChat/types.d.ts +26 -6
- package/dist/cjs/components/AiChat/utils.js +13 -3
- package/dist/cjs/components/Button/Button.d.ts +1 -3
- package/dist/cjs/components/ImageAdapter/ImageAdapter.d.ts +23 -0
- package/dist/cjs/components/ImageAdapter/ImageAdapter.js +30 -0
- package/dist/cjs/components/LinkAdapter/LinkAdapter.d.ts +1 -1
- package/dist/cjs/components/ThemeProvider/ThemeProvider.stories.d.ts +6 -1
- package/dist/cjs/components/ThemeProvider/ThemeProvider.stories.js +6 -1
- package/dist/esm/components/AiChat/AiChat.js +151 -59
- package/dist/esm/components/AiChat/AiChat.stories.js +5 -2
- package/dist/esm/components/AiChat/AiChat.test.js +39 -7
- package/dist/esm/components/AiChat/story-utils.js +6 -3
- package/dist/esm/components/AiChat/types.d.ts +26 -6
- package/dist/esm/components/AiChat/utils.js +13 -3
- package/dist/esm/components/Button/Button.d.ts +1 -3
- package/dist/esm/components/ImageAdapter/ImageAdapter.d.ts +23 -0
- package/dist/esm/components/ImageAdapter/ImageAdapter.js +27 -0
- package/dist/esm/components/LinkAdapter/LinkAdapter.d.ts +1 -1
- package/dist/esm/components/ThemeProvider/ThemeProvider.stories.d.ts +6 -1
- package/dist/esm/components/ThemeProvider/ThemeProvider.stories.js +6 -1
- package/dist/static/images/mit_mascot_tim.png +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type-augmentation/TypescriptDocs.mdx +1 -1
- package/dist/type-augmentation/imports.d.ts +3 -0
- package/dist/type-augmentation/index.d.ts +1 -0
- package/dist/type-augmentation/theme.d.ts +1 -0
- package/package.json +4 -3
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
14
|
exports.AiChat = void 0;
|
|
4
15
|
const React = require("react");
|
|
@@ -12,32 +23,74 @@ const react_markdown_1 = require("react-markdown");
|
|
|
12
23
|
const ScrollSnap_1 = require("../ScrollSnap/ScrollSnap");
|
|
13
24
|
const classnames_1 = require("classnames");
|
|
14
25
|
const SrAnnouncer_1 = require("../SrAnnouncer/SrAnnouncer");
|
|
15
|
-
const
|
|
26
|
+
const mit_mascot_tim_png_1 = require("../../../static/images/mit_mascot_tim.png");
|
|
27
|
+
const VisuallyHidden_1 = require("../VisuallyHidden/VisuallyHidden");
|
|
28
|
+
const ImageAdapter_1 = require("../ImageAdapter/ImageAdapter");
|
|
29
|
+
const Typography_1 = require("@mui/material/Typography");
|
|
30
|
+
const classes = {
|
|
31
|
+
root: "MitAiChat--root",
|
|
32
|
+
conversationStarter: "MitAiChat--conversationStarter",
|
|
33
|
+
messagesContainer: "MitAiChat--messagesContainer",
|
|
34
|
+
messageRow: "MitAiChat--messageRow",
|
|
35
|
+
messageRowUser: "MitAiChat--messageRowUser",
|
|
36
|
+
messageRowAssistant: "MitAiChat--messageRowAssistant",
|
|
37
|
+
message: "MitAiChat--message",
|
|
38
|
+
avatar: "MitAiChat--avatar",
|
|
39
|
+
input: "MitAiChat--input",
|
|
40
|
+
};
|
|
41
|
+
const ChatContainer = styled_1.default.div({
|
|
16
42
|
width: "100%",
|
|
17
43
|
height: "100%",
|
|
18
|
-
border: `1px solid ${theme.custom.colors.silverGrayLight}`,
|
|
19
|
-
backgroundColor: theme.custom.colors.lightGray1,
|
|
20
44
|
display: "flex",
|
|
21
45
|
flexDirection: "column",
|
|
22
|
-
})
|
|
23
|
-
const MessagesContainer = (0, styled_1.default)(ScrollSnap_1.ScrollSnap)({
|
|
46
|
+
});
|
|
47
|
+
const MessagesContainer = (0, styled_1.default)(ScrollSnap_1.ScrollSnap)(({ theme }) => ({
|
|
24
48
|
display: "flex",
|
|
25
49
|
flexDirection: "column",
|
|
26
50
|
flex: 1,
|
|
27
51
|
padding: "24px",
|
|
28
|
-
paddingBottom: "
|
|
52
|
+
paddingBottom: "12px",
|
|
29
53
|
overflow: "auto",
|
|
54
|
+
gap: "24px",
|
|
55
|
+
backgroundColor: theme.custom.colors.lightGray1,
|
|
56
|
+
borderColor: theme.custom.colors.silverGrayLight,
|
|
57
|
+
borderStyle: "solid",
|
|
58
|
+
borderWidth: "0 1px",
|
|
59
|
+
}));
|
|
60
|
+
const MessageRow = styled_1.default.div({
|
|
61
|
+
display: "flex",
|
|
62
|
+
width: "100%",
|
|
63
|
+
gap: "10px",
|
|
64
|
+
[`&.${classes.messageRowUser}`]: {
|
|
65
|
+
flexDirection: "row-reverse",
|
|
66
|
+
},
|
|
67
|
+
"> *": {
|
|
68
|
+
minWidth: 0,
|
|
69
|
+
},
|
|
70
|
+
position: "relative",
|
|
30
71
|
});
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
72
|
+
const Avatar = styled_1.default.div(({ theme }) => ({
|
|
73
|
+
flexShrink: 0,
|
|
74
|
+
borderRadius: "50%",
|
|
75
|
+
backgroundColor: theme.custom.colors.white,
|
|
76
|
+
display: "flex",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
justifyContent: "center",
|
|
79
|
+
img: {
|
|
80
|
+
width: "66%",
|
|
81
|
+
},
|
|
82
|
+
width: "32px",
|
|
83
|
+
height: "32px",
|
|
84
|
+
position: "absolute",
|
|
85
|
+
top: "-16px",
|
|
86
|
+
[`.${classes.messageRowAssistant} &`]: {
|
|
87
|
+
left: "-10px",
|
|
88
|
+
},
|
|
89
|
+
[`.${classes.messageRowUser} &`]: {
|
|
90
|
+
right: "16px",
|
|
37
91
|
},
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const Message = styled_1.default.div(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white, borderRadius: "24px", padding: "4px 16px" }, theme.typography.body2), { "p:first-of-type": {
|
|
92
|
+
}));
|
|
93
|
+
const Message = styled_1.default.div(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white, padding: "12px" }, theme.typography.body2), { "p:first-of-type": {
|
|
41
94
|
marginTop: 0,
|
|
42
95
|
}, "p:last-of-type": {
|
|
43
96
|
marginBottom: 0,
|
|
@@ -47,27 +100,32 @@ const Message = styled_1.default.div(({ theme }) => (Object.assign(Object.assign
|
|
|
47
100
|
}, "a:hover": {
|
|
48
101
|
color: theme.custom.colors.red,
|
|
49
102
|
textDecoration: "underline",
|
|
103
|
+
}, borderRadius: "12px", [`.${classes.messageRowAssistant} &`]: {
|
|
104
|
+
borderRadius: "0 12px 12px 12px",
|
|
105
|
+
}, [`.${classes.messageRowUser} &`]: {
|
|
106
|
+
borderRadius: "12px 0 12px 12px",
|
|
50
107
|
} })));
|
|
51
108
|
const StarterContainer = styled_1.default.div({
|
|
52
109
|
alignSelf: "flex-end",
|
|
110
|
+
alignItems: "end",
|
|
53
111
|
display: "flex",
|
|
54
112
|
flexDirection: "column",
|
|
55
|
-
gap: "
|
|
113
|
+
gap: "12px",
|
|
56
114
|
});
|
|
57
|
-
const Starter = styled_1.default.button(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white,
|
|
115
|
+
const Starter = styled_1.default.button(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white, padding: "8px 16px" }, theme.typography.subtitle3), { cursor: "pointer", "&:hover": {
|
|
58
116
|
backgroundColor: theme.custom.colors.lightGray1,
|
|
59
|
-
} })));
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
backgroundColor: theme.custom.colors.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
117
|
+
}, borderRadius: "100vh" })));
|
|
118
|
+
const InputStyled = (0, styled_1.default)(Input_1.Input)({
|
|
119
|
+
borderRadius: "0 0 8px 8px",
|
|
120
|
+
});
|
|
121
|
+
const ActionButtonStyled = (0, styled_1.default)(ActionButton_1.ActionButton)(({ theme }) => ({
|
|
122
|
+
backgroundColor: theme.custom.colors.red,
|
|
123
|
+
flexShrink: 0,
|
|
124
|
+
marginRight: "24px",
|
|
125
|
+
marginLeft: "12px",
|
|
126
|
+
"&:hover:not(:disabled)": {
|
|
127
|
+
backgroundColor: theme.custom.colors.mitRed,
|
|
128
|
+
},
|
|
71
129
|
}));
|
|
72
130
|
const DotsContainer = styled_1.default.span(({ theme }) => ({
|
|
73
131
|
display: "inline-flex",
|
|
@@ -82,18 +140,37 @@ const Dots = () => {
|
|
|
82
140
|
React.createElement(Skeleton_1.default, { variant: "circular", width: "8px", height: "8px" }),
|
|
83
141
|
React.createElement(Skeleton_1.default, { variant: "circular", width: "8px", height: "8px" })));
|
|
84
142
|
};
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
143
|
+
const CloseButton = (0, styled_1.default)(ActionButton_1.ActionButton)(({ theme }) => ({
|
|
144
|
+
color: "inherit",
|
|
145
|
+
backgroundColor: theme.custom.colors.red,
|
|
146
|
+
"&:hover:not(:disabled)": {
|
|
147
|
+
backgroundColor: theme.custom.colors.mitRed,
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
const RobotIcon = (0, styled_1.default)(react_1.RiRobot2Line)({
|
|
151
|
+
width: "40px",
|
|
152
|
+
height: "40px",
|
|
153
|
+
});
|
|
154
|
+
const ChatTitle = (0, styled_1.default)(({ title, onClose, className }) => {
|
|
155
|
+
return (React.createElement("div", { className: className },
|
|
156
|
+
React.createElement(RobotIcon, null),
|
|
157
|
+
React.createElement(Typography_1.default, { flex: 1, variant: "h5" }, title),
|
|
158
|
+
onClose ? (React.createElement(CloseButton, { variant: "text", onClick: onClose, "aria-label": "Close chat" },
|
|
159
|
+
React.createElement(react_1.RiCloseLine, null))) : null));
|
|
160
|
+
})(({ theme }) => ({
|
|
161
|
+
backgroundColor: theme.custom.colors.red,
|
|
162
|
+
display: "flex",
|
|
163
|
+
alignItems: "center",
|
|
164
|
+
justifyContent: "space-between",
|
|
165
|
+
padding: "12px 24px",
|
|
166
|
+
gap: "16px",
|
|
167
|
+
color: theme.custom.colors.white,
|
|
168
|
+
borderRadius: "8px 8px 0 0",
|
|
169
|
+
}));
|
|
170
|
+
const AiChatInternal = function AiChat(_a) {
|
|
171
|
+
var _b;
|
|
172
|
+
var { chatId, className, conversationStarters, requestOpts, initialMessages: initMsgs, parseContent, srLoadingMessages, title, onClose, ImgComponent, placeholder = "Type a message..." } = _a, others = __rest(_a, ["chatId", "className", "conversationStarters", "requestOpts", "initialMessages", "parseContent", "srLoadingMessages", "title", "onClose", "ImgComponent", "placeholder"]) // Could contain data attributes
|
|
173
|
+
;
|
|
97
174
|
const messagesRef = React.useRef(null);
|
|
98
175
|
const initialMessages = React.useMemo(() => {
|
|
99
176
|
const prefix = Math.random().toString().slice(2);
|
|
@@ -101,6 +178,7 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
101
178
|
}, [initMsgs]);
|
|
102
179
|
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, } = (0, utils_1.useAiChat)(requestOpts, {
|
|
103
180
|
initialMessages: initialMessages,
|
|
181
|
+
id: chatId,
|
|
104
182
|
});
|
|
105
183
|
const messages = React.useMemo(() => {
|
|
106
184
|
const initial = initialMessages.map((m) => m.id);
|
|
@@ -112,7 +190,8 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
112
190
|
return m;
|
|
113
191
|
});
|
|
114
192
|
}, [parseContent, unparsed, initialMessages]);
|
|
115
|
-
const
|
|
193
|
+
const showStarters = messages.length === initialMessages.length;
|
|
194
|
+
const waiting = !showStarters && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
116
195
|
const scrollToBottom = () => {
|
|
117
196
|
var _a;
|
|
118
197
|
(_a = messagesRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({
|
|
@@ -121,30 +200,43 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
121
200
|
});
|
|
122
201
|
};
|
|
123
202
|
const lastMsg = messages[messages.length - 1];
|
|
124
|
-
return (React.createElement(ChatContainer, { className: (0, classnames_1.default)(className, classes.root) },
|
|
203
|
+
return (React.createElement(ChatContainer, Object.assign({ className: (0, classnames_1.default)(className, classes.root) }, others),
|
|
204
|
+
React.createElement(ChatTitle, { title: title, onClose: onClose }),
|
|
125
205
|
React.createElement(MessagesContainer, { className: classes.messagesContainer, ref: messagesRef },
|
|
126
|
-
messages.map((m) => (React.createElement(MessageRow, { key: m.id,
|
|
127
|
-
|
|
206
|
+
messages.map((m) => (React.createElement(MessageRow, { key: m.id, "data-chat-role": m.role, className: (0, classnames_1.default)(classes.messageRow, {
|
|
207
|
+
[classes.messageRowUser]: m.role === "user",
|
|
208
|
+
[classes.messageRowAssistant]: m.role === "assistant",
|
|
209
|
+
}) },
|
|
210
|
+
m.role === "assistant" ? (React.createElement(Avatar, { className: classes.avatar },
|
|
211
|
+
React.createElement(ImageAdapter_1.ImageAdapter, { src: mit_mascot_tim_png_1.default, alt: "", Component: ImgComponent }))) : null,
|
|
128
212
|
React.createElement(Message, { className: classes.message },
|
|
129
|
-
React.createElement(
|
|
213
|
+
React.createElement(VisuallyHidden_1.VisuallyHidden, null, m.role === "user" ? "You said: " : "Assistant said: "),
|
|
214
|
+
React.createElement(react_markdown_1.default, { skipHtml: true }, m.content))))),
|
|
130
215
|
showStarters ? (React.createElement(StarterContainer, null, conversationStarters === null || conversationStarters === void 0 ? void 0 : conversationStarters.map((m) => (React.createElement(Starter, { className: classes.conversationStarter, key: m.content, onClick: () => {
|
|
131
|
-
setShowStarters(false);
|
|
132
216
|
scrollToBottom();
|
|
133
217
|
append({ role: "user", content: m.content });
|
|
134
218
|
} }, m.content))))) : null,
|
|
135
|
-
waiting ? (React.createElement(MessageRow, { key: "loading" },
|
|
136
|
-
React.createElement(Avatar, { className: classes.avatar }
|
|
219
|
+
waiting ? (React.createElement(MessageRow, { className: (0, classnames_1.default)(classes.messageRow, classes.messageRowAssistant), key: "loading" },
|
|
220
|
+
React.createElement(Avatar, { className: classes.avatar },
|
|
221
|
+
React.createElement(ImageAdapter_1.ImageAdapter, { src: mit_mascot_tim_png_1.default, alt: "", Component: ImgComponent })),
|
|
137
222
|
React.createElement(Message, null,
|
|
138
223
|
React.createElement(Dots, null)))) : null),
|
|
139
|
-
React.createElement(
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
React.createElement(Input_1.Input, { className: classes.input, placeholder: "Type a message...", name: "message", sx: { flex: 1 }, value: input, onChange: handleInputChange }),
|
|
146
|
-
React.createElement(ActionButton_1.ActionButton, { "aria-label": "Send", type: "submit", disabled: isLoading || !input },
|
|
147
|
-
React.createElement(react_1.RiSendPlaneFill, null)))),
|
|
224
|
+
React.createElement("form", { onSubmit: (e) => {
|
|
225
|
+
scrollToBottom();
|
|
226
|
+
handleSubmit(e);
|
|
227
|
+
} },
|
|
228
|
+
React.createElement(InputStyled, { fullWidth: true, size: "hero", className: classes.input, placeholder: placeholder, name: "message", sx: { flex: 1 }, value: input, onChange: handleInputChange, endAdornment: React.createElement(ActionButtonStyled, { size: "large", "aria-label": "Send", type: "submit", disabled: isLoading || !input },
|
|
229
|
+
React.createElement(react_1.RiSendPlaneFill, null)) })),
|
|
148
230
|
React.createElement(SrAnnouncer_1.SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: lastMsg.role === "assistant" ? lastMsg.content : "" })));
|
|
149
231
|
};
|
|
232
|
+
const AiChat = (props) => (
|
|
233
|
+
/**
|
|
234
|
+
* Changing the `useChat` chatId seems to persist some state between
|
|
235
|
+
* hook calls. This can cause strange effects like loading API responses
|
|
236
|
+
* for previous chatId into new chatId.
|
|
237
|
+
*
|
|
238
|
+
* To avoid this, let's chnge the key, this will force React to make a new component
|
|
239
|
+
* not sharing any of the old state.
|
|
240
|
+
*/
|
|
241
|
+
React.createElement(AiChatInternal, Object.assign({ key: props.chatId }, props)));
|
|
150
242
|
exports.AiChat = AiChat;
|
|
@@ -5,6 +5,7 @@ const React = require("react");
|
|
|
5
5
|
const AiChat_1 = require("./AiChat");
|
|
6
6
|
const story_utils_1 = require("./story-utils");
|
|
7
7
|
const styled_1 = require("@emotion/styled");
|
|
8
|
+
const test_1 = require("@storybook/test");
|
|
8
9
|
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
9
10
|
const TEST_API_JSON = "http://localhost:4567/json";
|
|
10
11
|
const INITIAL_MESSAGES = [
|
|
@@ -20,10 +21,10 @@ const STARTERS = [
|
|
|
20
21
|
];
|
|
21
22
|
const Container = styled_1.default.div({
|
|
22
23
|
width: "100%",
|
|
23
|
-
height: "
|
|
24
|
+
height: "500px",
|
|
24
25
|
});
|
|
25
26
|
const meta = {
|
|
26
|
-
title: "smoot-design/AiChat",
|
|
27
|
+
title: "smoot-design/ai/AiChat",
|
|
27
28
|
component: AiChat_1.AiChat,
|
|
28
29
|
render: (args) => React.createElement(AiChat_1.AiChat, Object.assign({}, args)),
|
|
29
30
|
decorators: (Story) => {
|
|
@@ -34,6 +35,8 @@ const meta = {
|
|
|
34
35
|
initialMessages: INITIAL_MESSAGES,
|
|
35
36
|
requestOpts: { apiUrl: TEST_API_STREAMING },
|
|
36
37
|
conversationStarters: STARTERS,
|
|
38
|
+
title: "Chat with AI",
|
|
39
|
+
onClose: (0, test_1.fn)(),
|
|
37
40
|
},
|
|
38
41
|
argTypes: {
|
|
39
42
|
conversationStarters: {
|
|
@@ -34,6 +34,10 @@ jest.mock("react-markdown", () => {
|
|
|
34
34
|
default: ({ children }) => React.createElement("div", null, children),
|
|
35
35
|
};
|
|
36
36
|
});
|
|
37
|
+
const msg = {
|
|
38
|
+
ai: (text) => `Assistant said: ${text}`,
|
|
39
|
+
you: (text) => `You said: ${text}`,
|
|
40
|
+
};
|
|
37
41
|
const getMessages = () => {
|
|
38
42
|
return Array.from(document.querySelectorAll(".MitAiChat--message"));
|
|
39
43
|
};
|
|
@@ -56,8 +60,11 @@ describe("AiChat", () => {
|
|
|
56
60
|
{ content: en_1.faker.lorem.sentence() },
|
|
57
61
|
{ content: en_1.faker.lorem.sentence() },
|
|
58
62
|
];
|
|
59
|
-
(0, react_1.render)(React.createElement(AiChat_1.AiChat, Object.assign({ initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, props)), { wrapper: ThemeProvider_1.ThemeProvider });
|
|
60
|
-
|
|
63
|
+
const view = (0, react_1.render)(React.createElement(AiChat_1.AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, props)), { wrapper: ThemeProvider_1.ThemeProvider });
|
|
64
|
+
const rerender = (newProps) => {
|
|
65
|
+
view.rerender(React.createElement(AiChat_1.AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, newProps)));
|
|
66
|
+
};
|
|
67
|
+
return { initialMessages, conversationStarters, rerender };
|
|
61
68
|
};
|
|
62
69
|
test("Clicking conversation starters and sending chats", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
70
|
const { initialMessages, conversationStarters } = setup();
|
|
@@ -85,6 +92,21 @@ describe("AiChat", () => {
|
|
|
85
92
|
expect(afterSending[3]).toHaveTextContent("User message");
|
|
86
93
|
expect(afterSending[4]).toHaveTextContent("AI Response 1");
|
|
87
94
|
}));
|
|
95
|
+
test("Messages persist if chat has same chatId", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
96
|
+
const { rerender } = setup({ chatId: "test-123" });
|
|
97
|
+
const starterEls = getConversationStarters();
|
|
98
|
+
const chosen = en_1.faker.helpers.arrayElement([0, 1]);
|
|
99
|
+
yield user_event_1.default.click(starterEls[chosen]);
|
|
100
|
+
yield whenCount(getMessages, 3);
|
|
101
|
+
// New chat ... starters should be shown
|
|
102
|
+
rerender({ chatId: "test-345" });
|
|
103
|
+
expect(getConversationStarters().length).toBeGreaterThan(0);
|
|
104
|
+
yield whenCount(getMessages, 1);
|
|
105
|
+
// existing chat ... starters should not be shown, messages should be restored
|
|
106
|
+
rerender({ chatId: "test-123" });
|
|
107
|
+
expect(getConversationStarters().length).toBe(0);
|
|
108
|
+
yield whenCount(getMessages, 3);
|
|
109
|
+
}));
|
|
88
110
|
test("transformBody is called before sending requests", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
111
|
const fakeBody = { message: en_1.faker.lorem.sentence() };
|
|
90
112
|
const apiUrl = en_1.faker.internet.url();
|
|
@@ -120,11 +142,21 @@ describe("AiChat", () => {
|
|
|
120
142
|
yield whenCount(getMessages, initialMessages.length + 4);
|
|
121
143
|
const messagesTexts = getMessages().map((el) => el.textContent);
|
|
122
144
|
expect(messagesTexts).toEqual([
|
|
123
|
-
initialMessages[0].content,
|
|
124
|
-
conversationStarters[0].content,
|
|
125
|
-
"Parsed: AI Response 0",
|
|
126
|
-
"User message",
|
|
127
|
-
"Parsed: AI Response 1",
|
|
145
|
+
msg.ai(initialMessages[0].content),
|
|
146
|
+
msg.you(conversationStarters[0].content),
|
|
147
|
+
msg.ai("Parsed: AI Response 0"),
|
|
148
|
+
msg.you("User message"),
|
|
149
|
+
msg.ai("Parsed: AI Response 1"),
|
|
128
150
|
]);
|
|
129
151
|
}));
|
|
152
|
+
test("Passes extra attributes to root", () => {
|
|
153
|
+
const fakeBody = { message: en_1.faker.lorem.sentence() };
|
|
154
|
+
const apiUrl = en_1.faker.internet.url();
|
|
155
|
+
const transformBody = jest.fn(() => fakeBody);
|
|
156
|
+
setup({
|
|
157
|
+
requestOpts: { apiUrl, transformBody },
|
|
158
|
+
parseContent: jest.fn((content) => `Parsed: ${content}`),
|
|
159
|
+
});
|
|
160
|
+
expect(react_1.screen.getByTestId("ai-chat")).toBeInTheDocument();
|
|
161
|
+
});
|
|
130
162
|
});
|
|
@@ -16,6 +16,7 @@ const SAMPLE_RESPONSES = [
|
|
|
16
16
|
1. **[Machine Learning, Modeling, and Simulation Principles](https://xpro.mit.edu/courses/course-v1:xPRO+MLx1/)**: Offered by MIT xPRO, this course is part of the program "Machine Learning, Modeling, and Simulation: Engineering Problem-Solving in the Age of AI." It focuses on the principles of machine learning and how they can be applied to solve engineering problems, which is highly relevant for business applications of AI.
|
|
17
17
|
|
|
18
18
|
This course is not free, but it provides a certification upon completion, which can be valuable for professionals looking to apply AI in business contexts. It covers essential concepts that can help you understand how AI can be leveraged to improve business processes and decision-making.
|
|
19
|
+
<!-- Comment! -->
|
|
19
20
|
`,
|
|
20
21
|
`
|
|
21
22
|
To understand global warming, I recommend the following resources from MIT:
|
|
@@ -25,6 +26,7 @@ To understand global warming, I recommend the following resources from MIT:
|
|
|
25
26
|
2. **[Global Warming Science](https://openlearninglibrary.mit.edu/courses/course-v1:MITx+12.340x+1T2020/about)**: Another offering of the same course by MITx, available through the Open Learning Library. It provides the same in-depth exploration of the earth's climate system.
|
|
26
27
|
|
|
27
28
|
These courses are free and provide a solid foundation in understanding the scientific aspects of global warming. They are suitable for anyone interested in the topic, regardless of prior knowledge.
|
|
29
|
+
<!-- Comment! -->
|
|
28
30
|
`,
|
|
29
31
|
`
|
|
30
32
|
Here are some courses on linear algebra that you can explore:
|
|
@@ -36,6 +38,7 @@ Here are some courses on linear algebra that you can explore:
|
|
|
36
38
|
3. **[Quantum Information Science I, Part 1 (MITx)](https://openlearninglibrary.mit.edu/courses/course-v1:MITx+8.370.1x+1T2018/about)**: While primarily focused on quantum information science, this course requires some knowledge of linear algebra and is suitable for those interested in quantum mechanics. It is free and available through MITx.
|
|
37
39
|
|
|
38
40
|
These courses provide a comprehensive introduction to linear algebra and its applications across various fields.
|
|
41
|
+
<!-- Comment! -->
|
|
39
42
|
`,
|
|
40
43
|
];
|
|
41
44
|
const rand = (min, max) => {
|
|
@@ -61,7 +64,7 @@ const mockStreaming = function mockApi() {
|
|
|
61
64
|
}, []);
|
|
62
65
|
const num = chunks.length;
|
|
63
66
|
let i = 0;
|
|
64
|
-
yield new Promise((resolve) => setTimeout(resolve,
|
|
67
|
+
yield new Promise((resolve) => setTimeout(resolve, 800));
|
|
65
68
|
const body = new ReadableStream({
|
|
66
69
|
start(controller) {
|
|
67
70
|
timerId = setInterval(() => {
|
|
@@ -72,7 +75,7 @@ const mockStreaming = function mockApi() {
|
|
|
72
75
|
controller.close();
|
|
73
76
|
clearInterval(timerId);
|
|
74
77
|
}
|
|
75
|
-
},
|
|
78
|
+
}, 100);
|
|
76
79
|
},
|
|
77
80
|
cancel() {
|
|
78
81
|
if (timerId) {
|
|
@@ -90,7 +93,7 @@ const mockStreaming = function mockApi() {
|
|
|
90
93
|
exports.mockStreaming = mockStreaming;
|
|
91
94
|
const mockJson = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
95
|
const message = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)];
|
|
93
|
-
yield new Promise((res) => setTimeout(res,
|
|
96
|
+
yield new Promise((res) => setTimeout(res, 1000));
|
|
94
97
|
return Promise.resolve(new Response(JSON.stringify({ message }), {
|
|
95
98
|
headers: {
|
|
96
99
|
"Content-Type": "application/json",
|
|
@@ -15,21 +15,36 @@ type RequestOpts = {
|
|
|
15
15
|
transformBody?: (messages: ChatMessage[]) => unknown;
|
|
16
16
|
/**
|
|
17
17
|
* Extra options to pass to fetch.
|
|
18
|
-
*
|
|
19
|
-
* If headers are specified, they will override the headersOpts.
|
|
20
18
|
*/
|
|
21
19
|
fetchOpts?: RequestInit;
|
|
22
|
-
|
|
23
|
-
* Extra headers to pass to fetch.
|
|
24
|
-
*/
|
|
25
|
-
headersOpts?: HeadersInit;
|
|
20
|
+
onFinish?: (message: ChatMessage) => void;
|
|
26
21
|
};
|
|
27
22
|
type AiChatProps = {
|
|
23
|
+
/**
|
|
24
|
+
* Changing the `chatId` will reset the chat. Changing the `chatId` to a
|
|
25
|
+
* previously used value will restore the session state.
|
|
26
|
+
*/
|
|
27
|
+
chatId?: string;
|
|
28
|
+
/**
|
|
29
|
+
* If provided, renders a title bar.
|
|
30
|
+
*/
|
|
31
|
+
title?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Plaeholder message for chat input
|
|
34
|
+
*/
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Fired when "Close" button within title bar is clicked.
|
|
38
|
+
*/
|
|
39
|
+
onClose?: () => void;
|
|
28
40
|
className?: string;
|
|
29
41
|
initialMessages: Omit<ChatMessage, "id">[];
|
|
30
42
|
conversationStarters?: {
|
|
31
43
|
content: string;
|
|
32
44
|
}[];
|
|
45
|
+
/**
|
|
46
|
+
* Options for making requests to the AI service.
|
|
47
|
+
*/
|
|
33
48
|
requestOpts: RequestOpts;
|
|
34
49
|
parseContent?: (content: unknown) => string;
|
|
35
50
|
/**
|
|
@@ -41,5 +56,10 @@ type AiChatProps = {
|
|
|
41
56
|
delay: number;
|
|
42
57
|
text: string;
|
|
43
58
|
}[];
|
|
59
|
+
/**
|
|
60
|
+
* If provided, element to use for rendering avatar images.
|
|
61
|
+
* By default, the theme's ImageAdater is used.
|
|
62
|
+
*/
|
|
63
|
+
ImgComponent?: React.ElementType;
|
|
44
64
|
};
|
|
45
65
|
export type { RequestOpts, AiChatProps, ChatMessage };
|
|
@@ -14,18 +14,28 @@ const react_1 = require("ai/react");
|
|
|
14
14
|
const react_2 = require("react");
|
|
15
15
|
const identity = (x) => x;
|
|
16
16
|
const getFetcher = (requestOpts) => (url, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
17
|
-
var _a;
|
|
17
|
+
var _a, _b;
|
|
18
18
|
if (typeof (opts === null || opts === void 0 ? void 0 : opts.body) !== "string") {
|
|
19
19
|
console.error("Unexpected body type.");
|
|
20
20
|
return window.fetch(url, opts);
|
|
21
21
|
}
|
|
22
22
|
const messages = JSON.parse(opts === null || opts === void 0 ? void 0 : opts.body).messages;
|
|
23
23
|
const transformBody = (_a = requestOpts.transformBody) !== null && _a !== void 0 ? _a : identity;
|
|
24
|
-
const options = Object.assign(Object.assign(Object.assign({}, opts), { body: JSON.stringify(transformBody(messages)), headers: Object.assign(Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts.headers), { "Content-Type": "application/json" }), requestOpts.
|
|
24
|
+
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) });
|
|
25
25
|
return fetch(url, options);
|
|
26
26
|
});
|
|
27
27
|
const useAiChat = (requestOpts, opts) => {
|
|
28
28
|
const fetcher = (0, react_2.useMemo)(() => getFetcher(requestOpts), [requestOpts]);
|
|
29
|
-
return (0, react_1.useChat)(Object.assign({ api: requestOpts.apiUrl, streamProtocol: "text", fetch: fetcher
|
|
29
|
+
return (0, react_1.useChat)(Object.assign({ api: requestOpts.apiUrl, streamProtocol: "text", fetch: fetcher, onFinish: (message) => {
|
|
30
|
+
var _a;
|
|
31
|
+
if (!requestOpts.onFinish)
|
|
32
|
+
return;
|
|
33
|
+
if (message.role === "assistant" || message.role === "user") {
|
|
34
|
+
(_a = requestOpts.onFinish) === null || _a === void 0 ? void 0 : _a.call(requestOpts, message);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.info("Unexpected message role.", message);
|
|
38
|
+
}
|
|
39
|
+
} }, opts));
|
|
30
40
|
};
|
|
31
41
|
exports.useAiChat = useAiChat;
|
|
@@ -31,9 +31,7 @@ declare const ButtonRoot: import("@emotion/styled").StyledComponent<{
|
|
|
31
31
|
theme?: import("@emotion/react").Theme;
|
|
32
32
|
as?: React.ElementType;
|
|
33
33
|
} & ButtonStyleProps, React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, {}>;
|
|
34
|
-
declare const ButtonLinkRoot: import("@emotion/styled").StyledComponent<Omit<
|
|
35
|
-
Component?: React.ElementType;
|
|
36
|
-
} & LinkAdapterPropsOverrides, "ref"> & React.RefAttributes<HTMLAnchorElement> & {
|
|
34
|
+
declare const ButtonLinkRoot: import("@emotion/styled").StyledComponent<Omit<import("../LinkAdapter/LinkAdapter").LinkAdapterProps, "ref"> & React.RefAttributes<HTMLAnchorElement> & {
|
|
37
35
|
theme?: import("@emotion/react").Theme;
|
|
38
36
|
} & ButtonStyleProps, {}, {}>;
|
|
39
37
|
type ButtonProps = ButtonStyleProps & React.ComponentProps<"button">;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* ImageAdapterPropsOverrides can be used with module augmentation to provide
|
|
4
|
+
* extra props to ButtonLink.
|
|
5
|
+
*
|
|
6
|
+
* For example, in a NextJS App, you might set `next/image` as your default
|
|
7
|
+
* image implementation, and use ImageAdapterPropsOverrides to provide
|
|
8
|
+
* `next/image`-specific props.
|
|
9
|
+
*/
|
|
10
|
+
interface ImageAdapterPropsOverrides {
|
|
11
|
+
}
|
|
12
|
+
type ImageAdapterProps = React.ComponentProps<"img"> & {
|
|
13
|
+
Component?: React.ElementType;
|
|
14
|
+
} & ImageAdapterPropsOverrides;
|
|
15
|
+
/**
|
|
16
|
+
* Overrideable Image component.
|
|
17
|
+
* - If `Component` is provided, renders as `Component`
|
|
18
|
+
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
19
|
+
* - else, renders as `img` tag
|
|
20
|
+
*/
|
|
21
|
+
declare const ImageAdapter: React.ForwardRefExoticComponent<Omit<ImageAdapterProps, "ref"> & React.RefAttributes<HTMLImageElement>>;
|
|
22
|
+
export { ImageAdapter };
|
|
23
|
+
export type { ImageAdapterPropsOverrides, ImageAdapterProps };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.ImageAdapter = void 0;
|
|
15
|
+
const React = require("react");
|
|
16
|
+
const react_1 = require("@emotion/react");
|
|
17
|
+
/**
|
|
18
|
+
* Overrideable Image component.
|
|
19
|
+
* - If `Component` is provided, renders as `Component`
|
|
20
|
+
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
21
|
+
* - else, renders as `img` tag
|
|
22
|
+
*/
|
|
23
|
+
const ImageAdapter = React.forwardRef(function ImageAdapter(_a, ref) {
|
|
24
|
+
var _b;
|
|
25
|
+
var { Component } = _a, props = __rest(_a, ["Component"]);
|
|
26
|
+
const theme = (0, react_1.useTheme)();
|
|
27
|
+
const ImgComponent = (_b = Component !== null && Component !== void 0 ? Component : theme.custom.ImgAdapter) !== null && _b !== void 0 ? _b : "img";
|
|
28
|
+
return React.createElement(ImgComponent, Object.assign({ ref: ref }, props));
|
|
29
|
+
});
|
|
30
|
+
exports.ImageAdapter = ImageAdapter;
|
|
@@ -20,4 +20,4 @@ type LinkAdapterProps = React.ComponentProps<"a"> & {
|
|
|
20
20
|
*/
|
|
21
21
|
declare const LinkAdapter: React.ForwardRefExoticComponent<Omit<LinkAdapterProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
22
22
|
export { LinkAdapter };
|
|
23
|
-
export type { LinkAdapterPropsOverrides };
|
|
23
|
+
export type { LinkAdapterPropsOverrides, LinkAdapterProps };
|
|
@@ -22,7 +22,6 @@ type Story = StoryObj<typeof ThemeProvider>;
|
|
|
22
22
|
* </ThemeProvider>
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
25
|
* ### Custom Link Adapter
|
|
27
26
|
* One particularly notable property is `theme.custom.LinkAdapter`. Some `smoot-design`
|
|
28
27
|
* components render links. These links are native anchor tags by default. In
|
|
@@ -53,6 +52,12 @@ type Story = StoryObj<typeof ThemeProvider>;
|
|
|
53
52
|
* }
|
|
54
53
|
* }
|
|
55
54
|
* ```
|
|
55
|
+
*
|
|
56
|
+
* ### ImageAdapter
|
|
57
|
+
* Similarly, `theme.custom.ImageAdapter` can be used to customize the image
|
|
58
|
+
* component used by `smoot-design`. By default, `ImageAdapter` uses a simple `img`
|
|
59
|
+
* tag. Interface `ImageAdapterPropsOverrides` is similarly available for
|
|
60
|
+
* augmentation.
|
|
56
61
|
*/
|
|
57
62
|
export declare const LinkAdapterOverride: Story;
|
|
58
63
|
export default meta;
|
|
@@ -53,7 +53,6 @@ const meta = {
|
|
|
53
53
|
* </ThemeProvider>
|
|
54
54
|
* ```
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
56
|
* ### Custom Link Adapter
|
|
58
57
|
* One particularly notable property is `theme.custom.LinkAdapter`. Some `smoot-design`
|
|
59
58
|
* components render links. These links are native anchor tags by default. In
|
|
@@ -84,6 +83,12 @@ const meta = {
|
|
|
84
83
|
* }
|
|
85
84
|
* }
|
|
86
85
|
* ```
|
|
86
|
+
*
|
|
87
|
+
* ### ImageAdapter
|
|
88
|
+
* Similarly, `theme.custom.ImageAdapter` can be used to customize the image
|
|
89
|
+
* component used by `smoot-design`. By default, `ImageAdapter` uses a simple `img`
|
|
90
|
+
* tag. Interface `ImageAdapterPropsOverrides` is similarly available for
|
|
91
|
+
* augmentation.
|
|
87
92
|
*/
|
|
88
93
|
exports.LinkAdapterOverride = {
|
|
89
94
|
args: {
|