@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.
Files changed (38) hide show
  1. package/dist/bundles/aiChat.es.js +24870 -0
  2. package/dist/bundles/aiChat.umd.js +125 -0
  3. package/dist/cjs/ai.d.ts +1 -0
  4. package/dist/cjs/bundles/aiChat.d.ts +6 -0
  5. package/dist/cjs/bundles/aiChat.js +13 -0
  6. package/dist/cjs/components/AiChat/AiChat.js +83 -99
  7. package/dist/cjs/components/AiChat/AiChat.stories.js +11 -2
  8. package/dist/cjs/components/AiChat/AiChat.test.js +1 -1
  9. package/dist/cjs/components/AiChat/types.d.ts +17 -7
  10. package/dist/cjs/components/ImageAdapter/ImageAdapter.d.ts +1 -1
  11. package/dist/cjs/components/ImageAdapter/ImageAdapter.js +1 -1
  12. package/dist/cjs/components/Input/Input.d.ts +2 -1
  13. package/dist/cjs/components/Input/Input.js +27 -4
  14. package/dist/cjs/components/Input/Input.stories.js +1 -0
  15. package/dist/cjs/components/ScrollSnap/ScrollSnap.js +1 -1
  16. package/dist/cjs/components/TextField/TextField.stories.js +1 -0
  17. package/dist/cjs/utils/composeRefs.test.js +2 -2
  18. package/dist/cjs/utils/useInterval.js +1 -1
  19. package/dist/esm/ai.d.ts +1 -0
  20. package/dist/esm/bundles/aiChat.d.ts +6 -0
  21. package/dist/esm/bundles/aiChat.js +10 -0
  22. package/dist/esm/components/AiChat/AiChat.js +85 -101
  23. package/dist/esm/components/AiChat/AiChat.stories.js +11 -2
  24. package/dist/esm/components/AiChat/AiChat.test.js +1 -1
  25. package/dist/esm/components/AiChat/types.d.ts +17 -7
  26. package/dist/esm/components/ImageAdapter/ImageAdapter.d.ts +1 -1
  27. package/dist/esm/components/ImageAdapter/ImageAdapter.js +1 -1
  28. package/dist/esm/components/Input/Input.d.ts +2 -1
  29. package/dist/esm/components/Input/Input.js +27 -4
  30. package/dist/esm/components/Input/Input.stories.js +1 -0
  31. package/dist/esm/components/ScrollSnap/ScrollSnap.js +1 -1
  32. package/dist/esm/components/TextField/TextField.stories.js +1 -0
  33. package/dist/esm/utils/composeRefs.test.js +2 -2
  34. package/dist/esm/utils/useInterval.js +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/dist/type-augmentation/theme.d.ts +1 -0
  37. package/package.json +6 -3
  38. 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 Skeleton from "@mui/material/Skeleton";
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 MessagesContainer = styled(ScrollSnap)(({ theme }) => ({
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
- padding: "24px",
49
- paddingBottom: "12px",
60
+ paddingTop: "14px",
61
+ paddingBottom: "24px",
50
62
  overflow: "auto",
51
63
  gap: "24px",
52
- backgroundColor: theme.custom.colors.lightGray1,
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 Avatar = styled.div(({ theme }) => ({
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
- textDecoration: "underline",
83
+ fontWeight: "normal",
104
84
  }, borderRadius: "12px", [`.${classes.messageRowAssistant} &`]: {
105
- borderRadius: "0 12px 12px 12px",
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: "12px 0 12px 12px",
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.silverGrayLight}`, backgroundColor: theme.custom.colors.white, padding: "8px 16px" }, theme.typography.subtitle3), { cursor: "pointer", "&:hover": {
117
- backgroundColor: theme.custom.colors.lightGray1,
118
- }, borderRadius: "100vh" })));
119
- const InputStyled = styled(Input)({
120
- borderRadius: "0 0 8px 8px",
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 ChatTitle = styled(({ title, onClose, className }) => {
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(RobotIcon, null),
158
- React.createElement(Typography, { flex: 1, variant: "h5" }, title),
159
- onClose ? (React.createElement(CloseButton, { variant: "text", onClick: onClose, "aria-label": "Close chat" },
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 24px",
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 = "Type a message..." } = _a, others = __rest(_a, ["chatId", "className", "conversationStarters", "requestOpts", "initialMessages", "parseContent", "srLoadingMessages", "title", "onClose", "ImgComponent", "placeholder"]) // Could contain data attributes
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(Dots, null)))) : null),
198
+ React.createElement(RiMoreFill, null)))) : null),
225
199
  React.createElement("form", { onSubmit: (e) => {
226
- scrollToBottom();
227
- handleSubmit(e);
200
+ e.preventDefault();
201
+ if (isLoading && stoppable) {
202
+ stop();
203
+ }
204
+ else {
205
+ scrollToBottom();
206
+ handleSubmit(e);
207
+ }
228
208
  } },
229
- 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 },
230
- React.createElement(RiSendPlaneFill, null)) })),
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 chnge the key, this will force React to make a new component
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: "500px",
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
- title: "Chat with AI",
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 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
@@ -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
- * Overrideable Image component.
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 &": {
@@ -10,6 +10,7 @@ const SIZES = enumValues({
10
10
  small: true,
11
11
  medium: true,
12
12
  large: true,
13
+ chat: true,
13
14
  hero: true,
14
15
  });
15
16
  const ADORNMENTS = {
@@ -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);
@@ -10,6 +10,7 @@ const SIZES = enumValues({
10
10
  small: true,
11
11
  medium: true,
12
12
  large: true,
13
+ chat: true,
13
14
  hero: true,
14
15
  });
15
16
  const ADORNMENTS = {
@@ -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 = { current: null };
7
- const objRef2 = { current: null };
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]);