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