@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
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";
@@ -0,0 +1,6 @@
1
+ import type { AiChatProps } from "../ai";
2
+ type CreateChatOptions = {
3
+ root: HTMLElement;
4
+ } & AiChatProps;
5
+ declare const aiChat: (opts: CreateChatOptions) => void;
6
+ export { aiChat };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.aiChat = void 0;
4
+ const React = require("react");
5
+ const client_1 = require("react-dom/client");
6
+ const ai_1 = require("../ai");
7
+ const index_1 = require("../index");
8
+ const aiChat = (opts) => {
9
+ const root = opts.root;
10
+ (0, client_1.createRoot)(root).render(React.createElement(index_1.ThemeProvider, null,
11
+ React.createElement(ai_1.AiChat, Object.assign({}, opts))));
12
+ };
13
+ exports.aiChat = aiChat;
@@ -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,30 @@ 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 AskTimTitle = styled_1.default.div(({ theme }) => ({
45
+ display: "flex",
46
+ alignItems: "center",
47
+ gap: "8px",
48
+ color: theme.custom.colors.darkGray2,
49
+ img: {
50
+ width: "24px",
51
+ height: "24px",
52
+ },
53
+ svg: {
54
+ fill: theme.custom.colors.red,
55
+ width: "24px",
56
+ height: "24px",
57
+ },
58
+ }));
59
+ const MessagesContainer = (0, styled_1.default)(ScrollSnap_1.ScrollSnap)({
48
60
  display: "flex",
49
61
  flexDirection: "column",
50
62
  flex: 1,
51
- padding: "24px",
52
- paddingBottom: "12px",
63
+ paddingTop: "14px",
64
+ paddingBottom: "24px",
53
65
  overflow: "auto",
54
66
  gap: "24px",
55
- backgroundColor: theme.custom.colors.lightGray1,
56
- borderColor: theme.custom.colors.silverGrayLight,
57
- borderStyle: "solid",
58
- borderWidth: "0 1px",
59
- }));
67
+ });
60
68
  const MessageRow = styled_1.default.div({
61
69
  display: "flex",
62
70
  width: "100%",
@@ -69,45 +77,25 @@ const MessageRow = styled_1.default.div({
69
77
  },
70
78
  position: "relative",
71
79
  });
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": {
80
+ const Message = styled_1.default.div(({ theme }) => (Object.assign(Object.assign({ backgroundColor: theme.custom.colors.white, padding: "12px 16px" }, theme.typography.body2), { "p:first-of-type": {
98
81
  marginTop: 0,
99
82
  }, "p:last-of-type": {
100
83
  marginBottom: 0,
101
84
  }, a: {
102
- color: theme.custom.colors.mitRed,
103
- textDecoration: "none",
104
- }, "a:hover": {
105
85
  color: theme.custom.colors.red,
106
- textDecoration: "underline",
86
+ fontWeight: "normal",
107
87
  }, borderRadius: "12px", [`.${classes.messageRowAssistant} &`]: {
108
- borderRadius: "0 12px 12px 12px",
88
+ border: `1px solid ${theme.custom.colors.lightGray2}`,
89
+ color: theme.custom.colors.darkGray2,
90
+ borderRadius: "0px 8px 8px 8px",
91
+ svg: {
92
+ fill: theme.custom.colors.silverGrayDark,
93
+ display: "block",
94
+ },
109
95
  }, [`.${classes.messageRowUser} &`]: {
110
- borderRadius: "12px 0 12px 12px",
96
+ borderRadius: "8px 0px 8px 8px",
97
+ color: theme.custom.colors.white,
98
+ backgroundColor: theme.custom.colors.silverGrayDark,
111
99
  } })));
112
100
  const StarterContainer = styled_1.default.div({
113
101
  alignSelf: "flex-end",
@@ -116,74 +104,63 @@ const StarterContainer = styled_1.default.div({
116
104
  flexDirection: "column",
117
105
  gap: "12px",
118
106
  });
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
- }));
107
+ 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": {
108
+ color: theme.custom.colors.white,
109
+ backgroundColor: theme.custom.colors.silverGrayDark,
110
+ borderColor: "transparent",
111
+ }, borderRadius: "8px" })));
154
112
  const RobotIcon = (0, styled_1.default)(react_1.RiRobot2Line)({
155
113
  width: "40px",
156
114
  height: "40px",
157
115
  });
158
- const ChatTitle = (0, styled_1.default)(({ title, onClose, className }) => {
116
+ const StyledInput = (0, styled_1.default)(Input_1.Input)(({ theme }) => ({
117
+ backgroundColor: theme.custom.colors.lightGray1,
118
+ borderRadius: "8px",
119
+ border: `1px solid ${theme.custom.colors.lightGray2}`,
120
+ }));
121
+ const StyledSendButton = (0, styled_1.default)(react_1.RiSendPlaneFill)(({ theme }) => ({
122
+ fill: theme.custom.colors.red,
123
+ }));
124
+ const StyledStopButton = (0, styled_1.default)(react_1.RiStopFill)(({ theme }) => ({
125
+ fill: theme.custom.colors.red,
126
+ }));
127
+ const ChatTitle = (0, styled_1.default)(({ title, askTimTitle, onClose, className }) => {
159
128
  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" },
129
+ askTimTitle ? (React.createElement(AskTimTitle, null,
130
+ React.createElement(react_1.RiSparkling2Line, null),
131
+ React.createElement(Typography_1.default, { variant: "body1" },
132
+ "Ask",
133
+ React.createElement("strong", null, "TIM"),
134
+ "\u00A0",
135
+ askTimTitle))) : null,
136
+ title ? (React.createElement(React.Fragment, null,
137
+ React.createElement(RobotIcon, null),
138
+ React.createElement(Typography_1.default, { flex: 1, variant: "h5" }, title))) : null,
139
+ onClose ? (React.createElement(ActionButton_1.ActionButton, { variant: "text", edge: "none", onClick: onClose, "aria-label": "Close chat" },
163
140
  React.createElement(react_1.RiCloseLine, null))) : null));
164
141
  })(({ theme }) => ({
165
- backgroundColor: theme.custom.colors.red,
166
142
  display: "flex",
167
143
  alignItems: "center",
168
144
  justifyContent: "space-between",
169
- padding: "12px 24px",
145
+ padding: "12px 0",
170
146
  gap: "16px",
171
147
  color: theme.custom.colors.white,
172
148
  borderRadius: "8px 8px 0 0",
173
149
  }));
174
150
  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
151
+ var _b, _c;
152
+ 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
153
  ;
178
154
  const messagesRef = React.useRef(null);
179
155
  const initialMessages = React.useMemo(() => {
180
156
  const prefix = Math.random().toString().slice(2);
181
157
  return initMsgs.map((m, i) => (Object.assign(Object.assign({}, m), { id: `initial-${prefix}-${i}` })));
182
158
  }, [initMsgs]);
183
- const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, } = (0, utils_1.useAiChat)(requestOpts, {
159
+ const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, stop, } = (0, utils_1.useAiChat)(requestOpts, {
184
160
  initialMessages: initialMessages,
185
161
  id: chatId,
186
162
  });
163
+ React.useImperativeHandle(ref, () => ({ append }), [append]);
187
164
  const messages = React.useMemo(() => {
188
165
  const initial = initialMessages.map((m) => m.id);
189
166
  return unparsed.map((m) => {
@@ -196,6 +173,7 @@ const AiChatInternal = function AiChat(_a) {
196
173
  }, [parseContent, unparsed, initialMessages]);
197
174
  const showStarters = messages.length === initialMessages.length;
198
175
  const waiting = !showStarters && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
176
+ const stoppable = isLoading && ((_c = messages[messages.length - 1]) === null || _c === void 0 ? void 0 : _c.role) !== "user";
199
177
  const scrollToBottom = () => {
200
178
  var _a;
201
179
  (_a = messagesRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({
@@ -205,14 +183,12 @@ const AiChatInternal = function AiChat(_a) {
205
183
  };
206
184
  const lastMsg = messages[messages.length - 1];
207
185
  return (React.createElement(ChatContainer, Object.assign({ className: (0, classnames_1.default)(className, classes.root) }, others),
208
- React.createElement(ChatTitle, { title: title, onClose: onClose }),
186
+ React.createElement(ChatTitle, { title: title, askTimTitle: askTimTitle, onClose: onClose, className: (0, classnames_1.default)(className, classes.title) }),
209
187
  React.createElement(MessagesContainer, { className: classes.messagesContainer, ref: messagesRef },
210
188
  messages.map((m) => (React.createElement(MessageRow, { key: m.id, "data-chat-role": m.role, className: (0, classnames_1.default)(classes.messageRow, {
211
189
  [classes.messageRowUser]: m.role === "user",
212
190
  [classes.messageRowAssistant]: m.role === "assistant",
213
191
  }) },
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
192
  React.createElement(Message, { className: classes.message },
217
193
  React.createElement(VisuallyHidden_1.VisuallyHidden, null, m.role === "user" ? "You said: " : "Assistant said: "),
218
194
  React.createElement(react_markdown_1.default, { skipHtml: true }, m.content))))),
@@ -221,16 +197,24 @@ const AiChatInternal = function AiChat(_a) {
221
197
  append({ role: "user", content: m.content });
222
198
  } }, m.content))))) : null,
223
199
  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
200
  React.createElement(Message, null,
227
- React.createElement(Dots, null)))) : null),
201
+ React.createElement(react_1.RiMoreFill, null)))) : null),
228
202
  React.createElement("form", { onSubmit: (e) => {
229
- scrollToBottom();
230
- handleSubmit(e);
203
+ e.preventDefault();
204
+ if (isLoading && stoppable) {
205
+ stop();
206
+ }
207
+ else {
208
+ scrollToBottom();
209
+ handleSubmit(e);
210
+ }
231
211
  } },
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)) })),
212
+ 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 },
213
+ React.createElement(StyledStopButton, null))) : (React.createElement(Input_1.AdornmentButton, { "aria-label": "Send", type: "submit", disabled: !input, onClick: (e) => {
214
+ scrollToBottom();
215
+ handleSubmit(e);
216
+ } },
217
+ React.createElement(StyledSendButton, null))) })),
234
218
  React.createElement(SrAnnouncer_1.SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: lastMsg.role === "assistant" ? lastMsg.content : "" })));
235
219
  };
236
220
  const AiChat = (props) => (
@@ -239,7 +223,7 @@ const AiChat = (props) => (
239
223
  * hook calls. This can cause strange effects like loading API responses
240
224
  * for previous chatId into new chatId.
241
225
  *
242
- * To avoid this, let's chnge the key, this will force React to make a new component
226
+ * To avoid this, let's change the key, this will force React to make a new component
243
227
  * not sharing any of the old state.
244
228
  */
245
229
  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,7 +29,7 @@ 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
35
  title: "smoot-design/ai/AiChat",
@@ -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";
@@ -0,0 +1,6 @@
1
+ import type { AiChatProps } from "../ai";
2
+ type CreateChatOptions = {
3
+ root: HTMLElement;
4
+ } & AiChatProps;
5
+ declare const aiChat: (opts: CreateChatOptions) => void;
6
+ export { aiChat };
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { AiChat } from "../ai";
4
+ import { ThemeProvider } from "../index";
5
+ const aiChat = (opts) => {
6
+ const root = opts.root;
7
+ createRoot(root).render(React.createElement(ThemeProvider, null,
8
+ React.createElement(AiChat, Object.assign({}, opts))));
9
+ };
10
+ export { aiChat };