@mitodl/smoot-design 3.0.1 → 3.2.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/bundles/aiChat.es.js +24870 -0
- package/dist/bundles/aiChat.umd.js +125 -0
- package/dist/cjs/ai.d.ts +1 -0
- package/dist/cjs/bundles/aiChat.d.ts +6 -0
- package/dist/cjs/bundles/aiChat.js +13 -0
- package/dist/cjs/components/AiChat/AiChat.js +83 -99
- package/dist/cjs/components/AiChat/AiChat.stories.js +11 -2
- package/dist/cjs/components/AiChat/AiChat.test.js +1 -1
- package/dist/cjs/components/AiChat/types.d.ts +17 -7
- package/dist/cjs/components/ImageAdapter/ImageAdapter.d.ts +1 -1
- package/dist/cjs/components/ImageAdapter/ImageAdapter.js +1 -1
- package/dist/cjs/components/Input/Input.d.ts +2 -1
- package/dist/cjs/components/Input/Input.js +27 -4
- package/dist/cjs/components/Input/Input.stories.js +1 -0
- package/dist/cjs/components/ScrollSnap/ScrollSnap.js +1 -1
- package/dist/cjs/components/TextField/TextField.stories.js +1 -0
- package/dist/cjs/utils/composeRefs.test.js +2 -2
- package/dist/cjs/utils/useInterval.js +1 -1
- package/dist/esm/ai.d.ts +1 -0
- package/dist/esm/bundles/aiChat.d.ts +6 -0
- package/dist/esm/bundles/aiChat.js +10 -0
- package/dist/esm/components/AiChat/AiChat.js +85 -101
- package/dist/esm/components/AiChat/AiChat.stories.js +11 -2
- package/dist/esm/components/AiChat/AiChat.test.js +1 -1
- package/dist/esm/components/AiChat/types.d.ts +17 -7
- package/dist/esm/components/ImageAdapter/ImageAdapter.d.ts +1 -1
- package/dist/esm/components/ImageAdapter/ImageAdapter.js +1 -1
- package/dist/esm/components/Input/Input.d.ts +2 -1
- package/dist/esm/components/Input/Input.js +27 -4
- package/dist/esm/components/Input/Input.stories.js +1 -0
- package/dist/esm/components/ScrollSnap/ScrollSnap.js +1 -1
- package/dist/esm/components/TextField/TextField.stories.js +1 -0
- package/dist/esm/utils/composeRefs.test.js +2 -2
- package/dist/esm/utils/useInterval.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type-augmentation/theme.d.ts +1 -0
- package/package.json +6 -3
- package/dist/static/images/mit_mascot_tim.png +0 -0
|
@@ -11,28 +11,25 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
};
|
|
12
12
|
import * as React from "react";
|
|
13
13
|
import styled from "@emotion/styled";
|
|
14
|
-
import
|
|
15
|
-
import { Input } from "../Input/Input";
|
|
14
|
+
import { Input, AdornmentButton } from "../Input/Input";
|
|
16
15
|
import { ActionButton } from "../Button/ActionButton";
|
|
17
|
-
import { RiCloseLine, RiRobot2Line, RiSendPlaneFill } from "@remixicon/react";
|
|
16
|
+
import { RiCloseLine, RiRobot2Line, RiSendPlaneFill, RiStopFill, RiSparkling2Line, RiMoreFill, } from "@remixicon/react";
|
|
18
17
|
import { useAiChat } from "./utils";
|
|
19
18
|
import Markdown from "react-markdown";
|
|
20
19
|
import { ScrollSnap } from "../ScrollSnap/ScrollSnap";
|
|
21
20
|
import classNames from "classnames";
|
|
22
21
|
import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer";
|
|
23
|
-
import mascot from "../../../static/images/mit_mascot_tim.png";
|
|
24
22
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden";
|
|
25
|
-
import { ImageAdapter } from "../ImageAdapter/ImageAdapter";
|
|
26
23
|
import Typography from "@mui/material/Typography";
|
|
27
24
|
const classes = {
|
|
28
25
|
root: "MitAiChat--root",
|
|
26
|
+
title: "MitAiChat--title",
|
|
29
27
|
conversationStarter: "MitAiChat--conversationStarter",
|
|
30
28
|
messagesContainer: "MitAiChat--messagesContainer",
|
|
31
29
|
messageRow: "MitAiChat--messageRow",
|
|
32
30
|
messageRowUser: "MitAiChat--messageRowUser",
|
|
33
31
|
messageRowAssistant: "MitAiChat--messageRowAssistant",
|
|
34
32
|
message: "MitAiChat--message",
|
|
35
|
-
avatar: "MitAiChat--avatar",
|
|
36
33
|
input: "MitAiChat--input",
|
|
37
34
|
};
|
|
38
35
|
const ChatContainer = styled.div({
|
|
@@ -41,19 +38,30 @@ const ChatContainer = styled.div({
|
|
|
41
38
|
display: "flex",
|
|
42
39
|
flexDirection: "column",
|
|
43
40
|
});
|
|
44
|
-
const
|
|
41
|
+
const AskTimTitle = styled.div(({ theme }) => ({
|
|
42
|
+
display: "flex",
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
gap: "8px",
|
|
45
|
+
color: theme.custom.colors.darkGray2,
|
|
46
|
+
img: {
|
|
47
|
+
width: "24px",
|
|
48
|
+
height: "24px",
|
|
49
|
+
},
|
|
50
|
+
svg: {
|
|
51
|
+
fill: theme.custom.colors.red,
|
|
52
|
+
width: "24px",
|
|
53
|
+
height: "24px",
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
const MessagesContainer = styled(ScrollSnap)({
|
|
45
57
|
display: "flex",
|
|
46
58
|
flexDirection: "column",
|
|
47
59
|
flex: 1,
|
|
48
|
-
|
|
49
|
-
paddingBottom: "
|
|
60
|
+
paddingTop: "14px",
|
|
61
|
+
paddingBottom: "24px",
|
|
50
62
|
overflow: "auto",
|
|
51
63
|
gap: "24px",
|
|
52
|
-
|
|
53
|
-
borderColor: theme.custom.colors.silverGrayLight,
|
|
54
|
-
borderStyle: "solid",
|
|
55
|
-
borderWidth: "0 1px",
|
|
56
|
-
}));
|
|
64
|
+
});
|
|
57
65
|
const MessageRow = styled.div({
|
|
58
66
|
display: "flex",
|
|
59
67
|
width: "100%",
|
|
@@ -66,45 +74,25 @@ const MessageRow = styled.div({
|
|
|
66
74
|
},
|
|
67
75
|
position: "relative",
|
|
68
76
|
});
|
|
69
|
-
const
|
|
70
|
-
flexShrink: 0,
|
|
71
|
-
borderRadius: "50%",
|
|
72
|
-
backgroundColor: theme.custom.colors.white,
|
|
73
|
-
display: "flex",
|
|
74
|
-
alignItems: "center",
|
|
75
|
-
justifyContent: "center",
|
|
76
|
-
img: {
|
|
77
|
-
width: "66%",
|
|
78
|
-
// This is the default, but NextJS adds a height attribute to images
|
|
79
|
-
// The attr is useful for aspect ratio, but we want the actual CSS size to
|
|
80
|
-
// be auto.
|
|
81
|
-
height: "auto",
|
|
82
|
-
},
|
|
83
|
-
width: "32px",
|
|
84
|
-
height: "32px",
|
|
85
|
-
position: "absolute",
|
|
86
|
-
top: "-16px",
|
|
87
|
-
[`.${classes.messageRowAssistant} &`]: {
|
|
88
|
-
left: "-10px",
|
|
89
|
-
},
|
|
90
|
-
[`.${classes.messageRowUser} &`]: {
|
|
91
|
-
right: "16px",
|
|
92
|
-
},
|
|
93
|
-
}));
|
|
94
|
-
const Message = styled.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": {
|
|
77
|
+
const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ backgroundColor: theme.custom.colors.white, padding: "12px 16px" }, theme.typography.body2), { "p:first-of-type": {
|
|
95
78
|
marginTop: 0,
|
|
96
79
|
}, "p:last-of-type": {
|
|
97
80
|
marginBottom: 0,
|
|
98
81
|
}, a: {
|
|
99
|
-
color: theme.custom.colors.mitRed,
|
|
100
|
-
textDecoration: "none",
|
|
101
|
-
}, "a:hover": {
|
|
102
82
|
color: theme.custom.colors.red,
|
|
103
|
-
|
|
83
|
+
fontWeight: "normal",
|
|
104
84
|
}, borderRadius: "12px", [`.${classes.messageRowAssistant} &`]: {
|
|
105
|
-
|
|
85
|
+
border: `1px solid ${theme.custom.colors.lightGray2}`,
|
|
86
|
+
color: theme.custom.colors.darkGray2,
|
|
87
|
+
borderRadius: "0px 8px 8px 8px",
|
|
88
|
+
svg: {
|
|
89
|
+
fill: theme.custom.colors.silverGrayDark,
|
|
90
|
+
display: "block",
|
|
91
|
+
},
|
|
106
92
|
}, [`.${classes.messageRowUser} &`]: {
|
|
107
|
-
borderRadius: "
|
|
93
|
+
borderRadius: "8px 0px 8px 8px",
|
|
94
|
+
color: theme.custom.colors.white,
|
|
95
|
+
backgroundColor: theme.custom.colors.silverGrayDark,
|
|
108
96
|
} })));
|
|
109
97
|
const StarterContainer = styled.div({
|
|
110
98
|
alignSelf: "flex-end",
|
|
@@ -113,74 +101,63 @@ const StarterContainer = styled.div({
|
|
|
113
101
|
flexDirection: "column",
|
|
114
102
|
gap: "12px",
|
|
115
103
|
});
|
|
116
|
-
const Starter = styled.button(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
borderRadius: "
|
|
121
|
-
});
|
|
122
|
-
const ActionButtonStyled = styled(ActionButton)(({ theme }) => ({
|
|
123
|
-
backgroundColor: theme.custom.colors.red,
|
|
124
|
-
flexShrink: 0,
|
|
125
|
-
marginRight: "24px",
|
|
126
|
-
marginLeft: "12px",
|
|
127
|
-
"&:hover:not(:disabled)": {
|
|
128
|
-
backgroundColor: theme.custom.colors.mitRed,
|
|
129
|
-
},
|
|
130
|
-
}));
|
|
131
|
-
const DotsContainer = styled.span(({ theme }) => ({
|
|
132
|
-
display: "inline-flex",
|
|
133
|
-
gap: "4px",
|
|
134
|
-
".MuiSkeleton-root": {
|
|
135
|
-
backgroundColor: theme.custom.colors.silverGray,
|
|
136
|
-
},
|
|
137
|
-
}));
|
|
138
|
-
const Dots = () => {
|
|
139
|
-
return (React.createElement(DotsContainer, null,
|
|
140
|
-
React.createElement(Skeleton, { variant: "circular", width: "8px", height: "8px" }),
|
|
141
|
-
React.createElement(Skeleton, { variant: "circular", width: "8px", height: "8px" }),
|
|
142
|
-
React.createElement(Skeleton, { variant: "circular", width: "8px", height: "8px" })));
|
|
143
|
-
};
|
|
144
|
-
const CloseButton = styled(ActionButton)(({ theme }) => ({
|
|
145
|
-
color: "inherit",
|
|
146
|
-
backgroundColor: theme.custom.colors.red,
|
|
147
|
-
"&:hover:not(:disabled)": {
|
|
148
|
-
backgroundColor: theme.custom.colors.mitRed,
|
|
149
|
-
},
|
|
150
|
-
}));
|
|
104
|
+
const Starter = styled.button(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.lightGray2}`, backgroundColor: theme.custom.colors.white, padding: "8px 16px" }, theme.typography.body3), { cursor: "pointer", boxSizing: "border-box", "&:hover": {
|
|
105
|
+
color: theme.custom.colors.white,
|
|
106
|
+
backgroundColor: theme.custom.colors.silverGrayDark,
|
|
107
|
+
borderColor: "transparent",
|
|
108
|
+
}, borderRadius: "8px" })));
|
|
151
109
|
const RobotIcon = styled(RiRobot2Line)({
|
|
152
110
|
width: "40px",
|
|
153
111
|
height: "40px",
|
|
154
112
|
});
|
|
155
|
-
const
|
|
113
|
+
const StyledInput = styled(Input)(({ theme }) => ({
|
|
114
|
+
backgroundColor: theme.custom.colors.lightGray1,
|
|
115
|
+
borderRadius: "8px",
|
|
116
|
+
border: `1px solid ${theme.custom.colors.lightGray2}`,
|
|
117
|
+
}));
|
|
118
|
+
const StyledSendButton = styled(RiSendPlaneFill)(({ theme }) => ({
|
|
119
|
+
fill: theme.custom.colors.red,
|
|
120
|
+
}));
|
|
121
|
+
const StyledStopButton = styled(RiStopFill)(({ theme }) => ({
|
|
122
|
+
fill: theme.custom.colors.red,
|
|
123
|
+
}));
|
|
124
|
+
const ChatTitle = styled(({ title, askTimTitle, onClose, className }) => {
|
|
156
125
|
return (React.createElement("div", { className: className },
|
|
157
|
-
React.createElement(
|
|
158
|
-
|
|
159
|
-
|
|
126
|
+
askTimTitle ? (React.createElement(AskTimTitle, null,
|
|
127
|
+
React.createElement(RiSparkling2Line, null),
|
|
128
|
+
React.createElement(Typography, { variant: "body1" },
|
|
129
|
+
"Ask",
|
|
130
|
+
React.createElement("strong", null, "TIM"),
|
|
131
|
+
"\u00A0",
|
|
132
|
+
askTimTitle))) : null,
|
|
133
|
+
title ? (React.createElement(React.Fragment, null,
|
|
134
|
+
React.createElement(RobotIcon, null),
|
|
135
|
+
React.createElement(Typography, { flex: 1, variant: "h5" }, title))) : null,
|
|
136
|
+
onClose ? (React.createElement(ActionButton, { variant: "text", edge: "none", onClick: onClose, "aria-label": "Close chat" },
|
|
160
137
|
React.createElement(RiCloseLine, null))) : null));
|
|
161
138
|
})(({ theme }) => ({
|
|
162
|
-
backgroundColor: theme.custom.colors.red,
|
|
163
139
|
display: "flex",
|
|
164
140
|
alignItems: "center",
|
|
165
141
|
justifyContent: "space-between",
|
|
166
|
-
padding: "12px
|
|
142
|
+
padding: "12px 0",
|
|
167
143
|
gap: "16px",
|
|
168
144
|
color: theme.custom.colors.white,
|
|
169
145
|
borderRadius: "8px 8px 0 0",
|
|
170
146
|
}));
|
|
171
147
|
const AiChatInternal = function AiChat(_a) {
|
|
172
|
-
var _b;
|
|
173
|
-
var { chatId, className, conversationStarters, requestOpts, initialMessages: initMsgs, parseContent, srLoadingMessages, title, onClose, ImgComponent, placeholder = "
|
|
148
|
+
var _b, _c;
|
|
149
|
+
var { chatId, className, conversationStarters, requestOpts, initialMessages: initMsgs, parseContent, srLoadingMessages, title, askTimTitle, onClose, ImgComponent, placeholder = "", ref } = _a, others = __rest(_a, ["chatId", "className", "conversationStarters", "requestOpts", "initialMessages", "parseContent", "srLoadingMessages", "title", "askTimTitle", "onClose", "ImgComponent", "placeholder", "ref"]) // Could contain data attributes
|
|
174
150
|
;
|
|
175
151
|
const messagesRef = React.useRef(null);
|
|
176
152
|
const initialMessages = React.useMemo(() => {
|
|
177
153
|
const prefix = Math.random().toString().slice(2);
|
|
178
154
|
return initMsgs.map((m, i) => (Object.assign(Object.assign({}, m), { id: `initial-${prefix}-${i}` })));
|
|
179
155
|
}, [initMsgs]);
|
|
180
|
-
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, } = useAiChat(requestOpts, {
|
|
156
|
+
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, stop, } = useAiChat(requestOpts, {
|
|
181
157
|
initialMessages: initialMessages,
|
|
182
158
|
id: chatId,
|
|
183
159
|
});
|
|
160
|
+
React.useImperativeHandle(ref, () => ({ append }), [append]);
|
|
184
161
|
const messages = React.useMemo(() => {
|
|
185
162
|
const initial = initialMessages.map((m) => m.id);
|
|
186
163
|
return unparsed.map((m) => {
|
|
@@ -193,6 +170,7 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
193
170
|
}, [parseContent, unparsed, initialMessages]);
|
|
194
171
|
const showStarters = messages.length === initialMessages.length;
|
|
195
172
|
const waiting = !showStarters && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
173
|
+
const stoppable = isLoading && ((_c = messages[messages.length - 1]) === null || _c === void 0 ? void 0 : _c.role) !== "user";
|
|
196
174
|
const scrollToBottom = () => {
|
|
197
175
|
var _a;
|
|
198
176
|
(_a = messagesRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({
|
|
@@ -202,14 +180,12 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
202
180
|
};
|
|
203
181
|
const lastMsg = messages[messages.length - 1];
|
|
204
182
|
return (React.createElement(ChatContainer, Object.assign({ className: classNames(className, classes.root) }, others),
|
|
205
|
-
React.createElement(ChatTitle, { title: title, onClose: onClose }),
|
|
183
|
+
React.createElement(ChatTitle, { title: title, askTimTitle: askTimTitle, onClose: onClose, className: classNames(className, classes.title) }),
|
|
206
184
|
React.createElement(MessagesContainer, { className: classes.messagesContainer, ref: messagesRef },
|
|
207
185
|
messages.map((m) => (React.createElement(MessageRow, { key: m.id, "data-chat-role": m.role, className: classNames(classes.messageRow, {
|
|
208
186
|
[classes.messageRowUser]: m.role === "user",
|
|
209
187
|
[classes.messageRowAssistant]: m.role === "assistant",
|
|
210
188
|
}) },
|
|
211
|
-
m.role === "assistant" ? (React.createElement(Avatar, { className: classes.avatar },
|
|
212
|
-
React.createElement(ImageAdapter, { src: mascot, alt: "", Component: ImgComponent }))) : null,
|
|
213
189
|
React.createElement(Message, { className: classes.message },
|
|
214
190
|
React.createElement(VisuallyHidden, null, m.role === "user" ? "You said: " : "Assistant said: "),
|
|
215
191
|
React.createElement(Markdown, { skipHtml: true }, m.content))))),
|
|
@@ -218,16 +194,24 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
218
194
|
append({ role: "user", content: m.content });
|
|
219
195
|
} }, m.content))))) : null,
|
|
220
196
|
waiting ? (React.createElement(MessageRow, { className: classNames(classes.messageRow, classes.messageRowAssistant), key: "loading" },
|
|
221
|
-
React.createElement(Avatar, { className: classes.avatar },
|
|
222
|
-
React.createElement(ImageAdapter, { src: mascot, alt: "", Component: ImgComponent })),
|
|
223
197
|
React.createElement(Message, null,
|
|
224
|
-
React.createElement(
|
|
198
|
+
React.createElement(RiMoreFill, null)))) : null),
|
|
225
199
|
React.createElement("form", { onSubmit: (e) => {
|
|
226
|
-
|
|
227
|
-
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
if (isLoading && stoppable) {
|
|
202
|
+
stop();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
scrollToBottom();
|
|
206
|
+
handleSubmit(e);
|
|
207
|
+
}
|
|
228
208
|
} },
|
|
229
|
-
React.createElement(
|
|
230
|
-
React.createElement(
|
|
209
|
+
React.createElement(StyledInput, { fullWidth: true, size: "chat", className: classes.input, placeholder: placeholder, name: "message", sx: { flex: 1 }, value: input, onChange: handleInputChange, endAdornment: isLoading ? (React.createElement(AdornmentButton, { "aria-label": "Stop", onClick: stop, disabled: !stoppable },
|
|
210
|
+
React.createElement(StyledStopButton, null))) : (React.createElement(AdornmentButton, { "aria-label": "Send", type: "submit", disabled: !input, onClick: (e) => {
|
|
211
|
+
scrollToBottom();
|
|
212
|
+
handleSubmit(e);
|
|
213
|
+
} },
|
|
214
|
+
React.createElement(StyledSendButton, null))) })),
|
|
231
215
|
React.createElement(SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: lastMsg.role === "assistant" ? lastMsg.content : "" })));
|
|
232
216
|
};
|
|
233
217
|
const AiChat = (props) => (
|
|
@@ -236,7 +220,7 @@ const AiChat = (props) => (
|
|
|
236
220
|
* hook calls. This can cause strange effects like loading API responses
|
|
237
221
|
* for previous chatId into new chatId.
|
|
238
222
|
*
|
|
239
|
-
* To avoid this, let's
|
|
223
|
+
* To avoid this, let's change the key, this will force React to make a new component
|
|
240
224
|
* not sharing any of the old state.
|
|
241
225
|
*/
|
|
242
226
|
React.createElement(AiChatInternal, Object.assign({ key: props.chatId }, props)));
|
|
@@ -10,6 +10,14 @@ const INITIAL_MESSAGES = [
|
|
|
10
10
|
content: "Hi! What are you interested in learning about?",
|
|
11
11
|
role: "assistant",
|
|
12
12
|
},
|
|
13
|
+
{
|
|
14
|
+
content: "I need to brush up on my Calculus",
|
|
15
|
+
role: "user",
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
content: "Great! Do you want to start with the basics, like limits and derivatives, or jump into more advanced topics like integrals and series? Let me know how I can help!",
|
|
19
|
+
role: "assistant",
|
|
20
|
+
},
|
|
13
21
|
];
|
|
14
22
|
const STARTERS = [
|
|
15
23
|
{ content: "I'm interested in quantum computing" },
|
|
@@ -18,7 +26,7 @@ const STARTERS = [
|
|
|
18
26
|
];
|
|
19
27
|
const Container = styled.div({
|
|
20
28
|
width: "100%",
|
|
21
|
-
height: "
|
|
29
|
+
height: "800px",
|
|
22
30
|
});
|
|
23
31
|
const meta = {
|
|
24
32
|
title: "smoot-design/ai/AiChat",
|
|
@@ -32,7 +40,7 @@ const meta = {
|
|
|
32
40
|
initialMessages: INITIAL_MESSAGES,
|
|
33
41
|
requestOpts: { apiUrl: TEST_API_STREAMING },
|
|
34
42
|
conversationStarters: STARTERS,
|
|
35
|
-
|
|
43
|
+
askTimTitle: "to recommend a course",
|
|
36
44
|
onClose: fn(),
|
|
37
45
|
},
|
|
38
46
|
argTypes: {
|
|
@@ -68,6 +76,7 @@ export const StreamingResponses = {};
|
|
|
68
76
|
*/
|
|
69
77
|
export const JsonResponses = {
|
|
70
78
|
args: {
|
|
79
|
+
title: "Chat with AI",
|
|
71
80
|
requestOpts: { apiUrl: TEST_API_JSON },
|
|
72
81
|
parseContent: (content) => {
|
|
73
82
|
return JSON.parse(content).message;
|
|
@@ -58,7 +58,7 @@ describe("AiChat", () => {
|
|
|
58
58
|
{ content: faker.lorem.sentence() },
|
|
59
59
|
{ content: faker.lorem.sentence() },
|
|
60
60
|
];
|
|
61
|
-
const view = render(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, props)), { wrapper: ThemeProvider });
|
|
61
|
+
const view = render(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" }, placeholder: "Type a message..." }, props)), { wrapper: ThemeProvider });
|
|
62
62
|
const rerender = (newProps) => {
|
|
63
63
|
view.rerender(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, newProps)));
|
|
64
64
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
type Role = "assistant" | "user";
|
|
2
|
-
type
|
|
2
|
+
type AiChatMessage = {
|
|
3
3
|
id: string;
|
|
4
4
|
content: string;
|
|
5
5
|
role: Role;
|
|
@@ -12,12 +12,12 @@ type RequestOpts = {
|
|
|
12
12
|
*
|
|
13
13
|
* JSON.stringify is applied to the return value.
|
|
14
14
|
*/
|
|
15
|
-
transformBody?: (messages:
|
|
15
|
+
transformBody?: (messages: AiChatMessage[]) => unknown;
|
|
16
16
|
/**
|
|
17
17
|
* Extra options to pass to fetch.
|
|
18
18
|
*/
|
|
19
19
|
fetchOpts?: RequestInit;
|
|
20
|
-
onFinish?: (message:
|
|
20
|
+
onFinish?: (message: AiChatMessage) => void;
|
|
21
21
|
};
|
|
22
22
|
type AiChatProps = {
|
|
23
23
|
/**
|
|
@@ -30,15 +30,19 @@ type AiChatProps = {
|
|
|
30
30
|
*/
|
|
31
31
|
title?: string;
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* If provided, renders the "AskTIM" title motif followed by the text.
|
|
34
|
+
*/
|
|
35
|
+
askTimTitle?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Placeholder message for chat input
|
|
34
38
|
*/
|
|
35
39
|
placeholder?: string;
|
|
36
40
|
/**
|
|
37
|
-
*
|
|
41
|
+
* Sends an initial user prompt on first load
|
|
38
42
|
*/
|
|
39
43
|
onClose?: () => void;
|
|
40
44
|
className?: string;
|
|
41
|
-
initialMessages: Omit<
|
|
45
|
+
initialMessages: Omit<AiChatMessage, "id">[];
|
|
42
46
|
conversationStarters?: {
|
|
43
47
|
content: string;
|
|
44
48
|
}[];
|
|
@@ -61,5 +65,11 @@ type AiChatProps = {
|
|
|
61
65
|
* By default, the theme's ImageAdater is used.
|
|
62
66
|
*/
|
|
63
67
|
ImgComponent?: React.ElementType;
|
|
68
|
+
/**
|
|
69
|
+
* Provide a ref to the chat component to access the `append` method.
|
|
70
|
+
*/
|
|
71
|
+
ref?: React.Ref<{
|
|
72
|
+
append: (message: Omit<AiChatMessage, "id">) => void;
|
|
73
|
+
}>;
|
|
64
74
|
};
|
|
65
|
-
export type { RequestOpts, AiChatProps,
|
|
75
|
+
export type { RequestOpts, AiChatProps, AiChatMessage };
|
|
@@ -13,7 +13,7 @@ type ImageAdapterProps = React.ComponentProps<"img"> & {
|
|
|
13
13
|
Component?: React.ElementType;
|
|
14
14
|
} & ImageAdapterPropsOverrides;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Overridable Image component.
|
|
17
17
|
* - If `Component` is provided, renders as `Component`
|
|
18
18
|
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
19
19
|
* - else, renders as `img` tag
|
|
@@ -12,7 +12,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
12
12
|
import * as React from "react";
|
|
13
13
|
import { useTheme } from "@emotion/react";
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
15
|
+
* Overridable Image component.
|
|
16
16
|
* - If `Component` is provided, renders as `Component`
|
|
17
17
|
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
18
18
|
* - else, renders as `img` tag
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { InputBaseProps } from "@mui/material/InputBase";
|
|
3
3
|
import type { Theme } from "@mui/material/styles";
|
|
4
|
-
type Size = "small" | "medium" | "large" | "hero";
|
|
4
|
+
type Size = "small" | "medium" | "large" | "chat" | "hero";
|
|
5
5
|
type CustomInputProps = {
|
|
6
6
|
/**
|
|
7
7
|
* If true, the input will display one size smaller at mobile breakpoint.
|
|
@@ -34,6 +34,7 @@ declare const baseInputStyles: (theme: Theme) => {
|
|
|
34
34
|
borderWidth: string;
|
|
35
35
|
borderStyle: string;
|
|
36
36
|
borderRadius: string;
|
|
37
|
+
overflow: string;
|
|
37
38
|
"&.Mui-disabled": {
|
|
38
39
|
backgroundColor: string;
|
|
39
40
|
};
|
|
@@ -22,11 +22,12 @@ const responsiveSize = {
|
|
|
22
22
|
small: "small",
|
|
23
23
|
medium: "small",
|
|
24
24
|
large: "medium",
|
|
25
|
+
chat: "medium",
|
|
25
26
|
hero: "large",
|
|
26
27
|
};
|
|
27
28
|
const sizeStyles = ({ size, theme, multiline }) => css([
|
|
28
29
|
(size === "small" || size === "medium") && Object.assign({}, theme.typography.body2),
|
|
29
|
-
(size === "large" || size === "hero") && Object.assign({ ".remixicon": {
|
|
30
|
+
(size === "large" || size === "chat" || size === "hero") && Object.assign({ ".remixicon": {
|
|
30
31
|
width: "24px",
|
|
31
32
|
height: "24px",
|
|
32
33
|
} }, theme.typography.body1),
|
|
@@ -46,6 +47,10 @@ const sizeStyles = ({ size, theme, multiline }) => css([
|
|
|
46
47
|
!multiline && {
|
|
47
48
|
height: "48px",
|
|
48
49
|
},
|
|
50
|
+
size === "chat" &&
|
|
51
|
+
!multiline && {
|
|
52
|
+
height: "56px",
|
|
53
|
+
},
|
|
49
54
|
size === "hero" &&
|
|
50
55
|
!multiline && {
|
|
51
56
|
height: "72px",
|
|
@@ -76,6 +81,20 @@ const sizeStyles = ({ size, theme, multiline }) => css([
|
|
|
76
81
|
width: "48px",
|
|
77
82
|
},
|
|
78
83
|
},
|
|
84
|
+
size === "chat" && {
|
|
85
|
+
padding: "0 16px",
|
|
86
|
+
borderRadius: "8px",
|
|
87
|
+
"&:hover:not(.Mui-disabled):not(.Mui-focused)": {
|
|
88
|
+
borderColor: theme.custom.colors.silverGrayLight,
|
|
89
|
+
},
|
|
90
|
+
"&.Mui-focused": {
|
|
91
|
+
borderColor: theme.custom.colors.silverGrayLight,
|
|
92
|
+
outline: "none",
|
|
93
|
+
},
|
|
94
|
+
".Mit-AdornmentButton": {
|
|
95
|
+
padding: "0 16px",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
79
98
|
size === "hero" && {
|
|
80
99
|
padding: "0 24px",
|
|
81
100
|
".Mit-AdornmentButton": {
|
|
@@ -93,6 +112,7 @@ const baseInputStyles = (theme) => ({
|
|
|
93
112
|
borderWidth: "1px",
|
|
94
113
|
borderStyle: "solid",
|
|
95
114
|
borderRadius: "4px",
|
|
115
|
+
overflow: "hidden",
|
|
96
116
|
"&.Mui-disabled": {
|
|
97
117
|
backgroundColor: theme.custom.colors.lightGray1,
|
|
98
118
|
},
|
|
@@ -166,16 +186,19 @@ const Input = styled(InputBase, {
|
|
|
166
186
|
}),
|
|
167
187
|
},
|
|
168
188
|
]);
|
|
169
|
-
const AdornmentButtonStyled = styled.button(({ theme }) => (Object.assign(Object.assign({}, theme.typography.button), {
|
|
189
|
+
const AdornmentButtonStyled = styled.button(({ theme, disabled }) => (Object.assign(Object.assign({}, theme.typography.button), {
|
|
170
190
|
// display
|
|
171
191
|
display: "flex", flexShrink: 0, justifyContent: "center", alignItems: "center",
|
|
172
192
|
// background and border
|
|
173
193
|
border: "none", background: "transparent", transition: `background ${theme.transitions.duration.short}ms`,
|
|
174
194
|
// cursor
|
|
175
|
-
cursor: "pointer", ":disabled": {
|
|
195
|
+
cursor: disabled ? "default" : "pointer", ":disabled": {
|
|
176
196
|
cursor: "default",
|
|
197
|
+
svg: {
|
|
198
|
+
fill: theme.custom.colors.silverGray,
|
|
199
|
+
},
|
|
177
200
|
}, ":hover": {
|
|
178
|
-
background: "rgba(0, 0, 0, 0.06)",
|
|
201
|
+
background: disabled ? "inherit" : "rgba(0, 0, 0, 0.06)",
|
|
179
202
|
}, color: theme.custom.colors.silverGray, ".MuiInputBase-root:hover &": {
|
|
180
203
|
color: "inherit",
|
|
181
204
|
}, ".MuiInputBase-root.Mui-focused &": {
|
|
@@ -21,7 +21,7 @@ const Scroller = styled.div({
|
|
|
21
21
|
* content is added, unless the user has scrolled up.
|
|
22
22
|
*/
|
|
23
23
|
const ScrollSnap = React.forwardRef(function ScrollSnap({ children, threshold = 2, className }, ref) {
|
|
24
|
-
const el = React.useRef();
|
|
24
|
+
const el = React.useRef(null);
|
|
25
25
|
// `content` a delayed version of children to allow measuring scroll position
|
|
26
26
|
// using the old children.
|
|
27
27
|
const [content, setContent] = React.useState(children);
|
|
@@ -3,8 +3,8 @@ import { composeRefs } from "./composeRefs";
|
|
|
3
3
|
import { render, screen } from "@testing-library/react";
|
|
4
4
|
describe("composeRefs", () => {
|
|
5
5
|
test("Composing object + fn ref", () => {
|
|
6
|
-
const objRef1 =
|
|
7
|
-
const objRef2 =
|
|
6
|
+
const objRef1 = React.createRef();
|
|
7
|
+
const objRef2 = React.createRef();
|
|
8
8
|
const fnRef1 = jest.fn();
|
|
9
9
|
const fnRef2 = jest.fn();
|
|
10
10
|
render(React.createElement("div", { "data-testid": "my-div", ref: composeRefs(objRef1, objRef2, fnRef1, fnRef2) }));
|
|
@@ -5,7 +5,7 @@ import { useRef, useEffect } from "react";
|
|
|
5
5
|
* Based on https://overreacted.io/making-setinterval-declarative-with-react-hooks/
|
|
6
6
|
*/
|
|
7
7
|
const useInterval = (callback, delay) => {
|
|
8
|
-
const savedCallback = useRef();
|
|
8
|
+
const savedCallback = useRef(null);
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
savedCallback.current = callback;
|
|
11
11
|
}, [callback]);
|