@mitodl/smoot-design 4.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/bundles/remoteTutorDrawer.es.js +14071 -13718
  2. package/dist/bundles/remoteTutorDrawer.umd.js +44 -45
  3. package/dist/cjs/bundles/RemoteTutorDrawer/FlashcardsScreen.d.ts +9 -0
  4. package/dist/cjs/bundles/RemoteTutorDrawer/FlashcardsScreen.js +87 -0
  5. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts +5 -3
  6. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +103 -50
  7. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +141 -25
  8. package/dist/cjs/components/AiChat/AiChat.d.ts +2 -2
  9. package/dist/cjs/components/AiChat/AiChat.js +121 -122
  10. package/dist/cjs/components/AiChat/AiChat.stories.d.ts +6 -0
  11. package/dist/cjs/components/AiChat/AiChat.stories.js +139 -18
  12. package/dist/cjs/components/AiChat/AiChat.test.js +12 -2
  13. package/dist/cjs/components/AiChat/ChatTitle.d.ts +8 -0
  14. package/dist/cjs/components/AiChat/ChatTitle.js +43 -0
  15. package/dist/cjs/components/AiChat/EntryScreen.d.ts +11 -0
  16. package/dist/cjs/components/AiChat/EntryScreen.js +118 -0
  17. package/dist/cjs/components/AiChat/TimLogo.d.ts +5 -0
  18. package/dist/cjs/components/AiChat/TimLogo.js +15 -0
  19. package/dist/cjs/components/AiChat/types.d.ts +20 -18
  20. package/dist/cjs/components/AiChat/types.js +1 -1
  21. package/dist/cjs/components/Input/Input.js +1 -0
  22. package/dist/cjs/components/ScrollSnap/useScrollSnap.d.ts +6 -0
  23. package/dist/cjs/components/ScrollSnap/useScrollSnap.js +36 -0
  24. package/dist/esm/bundles/RemoteTutorDrawer/FlashcardsScreen.d.ts +9 -0
  25. package/dist/esm/bundles/RemoteTutorDrawer/FlashcardsScreen.js +83 -0
  26. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts +5 -3
  27. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +105 -52
  28. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +141 -25
  29. package/dist/esm/components/AiChat/AiChat.d.ts +2 -2
  30. package/dist/esm/components/AiChat/AiChat.js +119 -120
  31. package/dist/esm/components/AiChat/AiChat.stories.d.ts +6 -0
  32. package/dist/esm/components/AiChat/AiChat.stories.js +138 -17
  33. package/dist/esm/components/AiChat/AiChat.test.js +12 -2
  34. package/dist/esm/components/AiChat/ChatTitle.d.ts +8 -0
  35. package/dist/esm/components/AiChat/ChatTitle.js +40 -0
  36. package/dist/esm/components/AiChat/EntryScreen.d.ts +11 -0
  37. package/dist/esm/components/AiChat/EntryScreen.js +115 -0
  38. package/dist/esm/components/AiChat/TimLogo.d.ts +5 -0
  39. package/dist/esm/components/AiChat/TimLogo.js +13 -0
  40. package/dist/esm/components/AiChat/types.d.ts +20 -18
  41. package/dist/esm/components/AiChat/types.js +1 -1
  42. package/dist/esm/components/Input/Input.js +1 -0
  43. package/dist/esm/components/ScrollSnap/useScrollSnap.d.ts +6 -0
  44. package/dist/esm/components/ScrollSnap/useScrollSnap.js +33 -0
  45. package/dist/tsconfig.tsbuildinfo +1 -1
  46. package/package.json +2 -2
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ export type Flashcard = {
3
+ question: string;
4
+ answer: string;
5
+ };
6
+ export declare const FlashcardsScreen: ({ flashcards, wasKeyboardFocus, }: {
7
+ flashcards?: Flashcard[];
8
+ wasKeyboardFocus: boolean;
9
+ }) => React.JSX.Element | null;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FlashcardsScreen = void 0;
4
+ const ActionButton_1 = require("../../components/Button/ActionButton");
5
+ const Typography_1 = require("@mui/material/Typography");
6
+ const React = require("react");
7
+ const react_1 = require("react");
8
+ const styled_1 = require("@emotion/styled");
9
+ const react_2 = require("@remixicon/react");
10
+ const Container = styled_1.default.div ``;
11
+ const FlashcardContainer = styled_1.default.div(({ theme }) => ({
12
+ display: "flex",
13
+ height: 300,
14
+ padding: 40,
15
+ flexDirection: "column",
16
+ justifyContent: "center",
17
+ alignItems: "center",
18
+ alignSelf: "stretch",
19
+ borderRadius: 8,
20
+ border: `1px solid ${theme.custom.colors.lightGray2}`,
21
+ marginTop: "8px",
22
+ cursor: "pointer",
23
+ textAlign: "center",
24
+ }));
25
+ const Navigation = styled_1.default.div({
26
+ display: "flex",
27
+ justifyContent: "space-between",
28
+ alignItems: "center",
29
+ width: "100%",
30
+ marginTop: "24px",
31
+ });
32
+ const Page = styled_1.default.div(({ theme }) => (Object.assign({ color: theme.custom.colors.silverGrayDark }, theme.typography.body2)));
33
+ const Flashcard = React.forwardRef(({ content, "aria-label": ariaLabel }, ref) => {
34
+ const [screen, setScreen] = (0, react_1.useState)(0);
35
+ (0, react_1.useEffect)(() => setScreen(0), [content]);
36
+ const handleKeyDown = (e) => {
37
+ if (e.key === "Enter" || e.key === " ") {
38
+ setScreen(screen === 0 ? 1 : 0);
39
+ }
40
+ };
41
+ return (React.createElement(FlashcardContainer, { ref: ref, onClick: () => setScreen(screen === 0 ? 1 : 0), onKeyDown: handleKeyDown, tabIndex: 0, "aria-label": ariaLabel },
42
+ React.createElement(Typography_1.default, { variant: "h5" }, screen === 0 ? `Q: ${content.question}` : `Answer: ${content.answer}`)));
43
+ });
44
+ Flashcard.displayName = "Flashcard";
45
+ const FlashcardsScreen = ({ flashcards, wasKeyboardFocus, }) => {
46
+ const [cardIndex, setCardIndex] = (0, react_1.useState)(0);
47
+ const containerRef = (0, react_1.useRef)(null);
48
+ const flashcardRef = (0, react_1.useRef)(null);
49
+ const handleKeyDown = (0, react_1.useCallback)((e) => {
50
+ var _a;
51
+ if (!flashcards)
52
+ return;
53
+ if (!((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.contains(document.activeElement)) &&
54
+ wasKeyboardFocus) {
55
+ return;
56
+ }
57
+ if (e.key === "ArrowRight") {
58
+ setCardIndex((prevIndex) => (prevIndex + 1) % flashcards.length);
59
+ }
60
+ else if (e.key === "ArrowLeft") {
61
+ setCardIndex((prevIndex) => (prevIndex - 1 + flashcards.length) % flashcards.length);
62
+ }
63
+ }, [flashcards, wasKeyboardFocus]);
64
+ (0, react_1.useEffect)(() => {
65
+ var _a;
66
+ (_a = flashcardRef.current) === null || _a === void 0 ? void 0 : _a.focus();
67
+ }, [cardIndex]);
68
+ (0, react_1.useEffect)(() => {
69
+ window.addEventListener("keydown", handleKeyDown);
70
+ return () => window.removeEventListener("keydown", handleKeyDown);
71
+ }, [handleKeyDown]);
72
+ if (!flashcards) {
73
+ return null;
74
+ }
75
+ return (React.createElement(Container, { ref: containerRef },
76
+ React.createElement(Flashcard, { ref: flashcardRef, content: flashcards[cardIndex], "aria-label": `Flashcard ${cardIndex + 1} of ${flashcards.length}` }),
77
+ React.createElement(Navigation, null,
78
+ React.createElement(ActionButton_1.ActionButton, { onClick: () => setCardIndex(cardIndex - 1), disabled: cardIndex === 0, variant: "secondary", color: "secondary", size: "small" },
79
+ React.createElement(react_2.RiArrowLeftLine, { "aria-hidden": true })),
80
+ React.createElement(Page, null,
81
+ cardIndex + 1,
82
+ " / ",
83
+ flashcards.length),
84
+ React.createElement(ActionButton_1.ActionButton, { onClick: () => setCardIndex(cardIndex + 1), disabled: cardIndex === flashcards.length - 1, variant: "secondary", color: "secondary", size: "small" },
85
+ React.createElement(react_2.RiArrowRightLine, { "aria-hidden": true })))));
86
+ };
87
+ exports.FlashcardsScreen = FlashcardsScreen;
@@ -6,21 +6,23 @@ type RemoteTutorDrawerInitMessage = {
6
6
  payload: {
7
7
  blockType?: "problem" | "video";
8
8
  target?: string;
9
+ /**
10
+ * If the title begins "AskTIM", it is styled as the AskTIM logo.
11
+ */
12
+ title?: string;
9
13
  chat: {
10
14
  chatId?: AiChatProps["chatId"];
11
- askTimTitle?: AiChatProps["title"];
12
15
  conversationStarters?: AiChatProps["conversationStarters"];
13
16
  initialMessages: AiChatProps["initialMessages"];
14
17
  apiUrl: AiChatProps["requestOpts"]["apiUrl"];
15
18
  requestBody?: Record<string, unknown>;
16
19
  };
17
20
  summary?: {
18
- contentUrl: string;
21
+ apiUrl: string;
19
22
  };
20
23
  };
21
24
  };
22
25
  type RemoteTutorDrawerProps = {
23
- blockType?: "problem" | "video";
24
26
  className?: string;
25
27
  /**
26
28
  * The origin of the messages that will be received to open the chat.
@@ -23,34 +23,72 @@ const TabContext_1 = require("@mui/lab/TabContext");
23
23
  const TabPanel_1 = require("@mui/lab/TabPanel");
24
24
  const AiChat_1 = require("../../components/AiChat/AiChat");
25
25
  const ActionButton_1 = require("../../components/Button/ActionButton");
26
+ const FlashcardsScreen_1 = require("./FlashcardsScreen");
27
+ const Header = styled_1.default.div(({ theme }) => ({
28
+ display: "flex",
29
+ alignItems: "center",
30
+ justifyContent: "space-between",
31
+ gap: "4px",
32
+ color: theme.custom.colors.white,
33
+ position: "sticky",
34
+ top: 0,
35
+ padding: "32px 0 16px 0",
36
+ zIndex: 2,
37
+ backgroundColor: theme.custom.colors.white,
38
+ borderRadius: 0,
39
+ }));
40
+ const Title = styled_1.default.div(({ theme }) => ({
41
+ display: "flex",
42
+ alignItems: "center",
43
+ gap: "8px",
44
+ color: theme.custom.colors.darkGray2,
45
+ img: {
46
+ width: "24px",
47
+ height: "24px",
48
+ },
49
+ svg: {
50
+ fill: theme.custom.colors.red,
51
+ width: "24px",
52
+ height: "24px",
53
+ flexShrink: 0,
54
+ },
55
+ overflow: "hidden",
56
+ p: {
57
+ textOverflow: "ellipsis",
58
+ overflow: "hidden",
59
+ whiteSpace: "nowrap",
60
+ },
61
+ }));
26
62
  const CloseButton = (0, styled_1.default)(ActionButton_1.ActionButton)(({ theme }) => ({
27
- position: "fixed",
28
- top: "24px",
29
- right: "40px",
30
63
  backgroundColor: theme.custom.colors.lightGray2,
31
64
  "&&:hover": {
32
65
  backgroundColor: theme.custom.colors.red,
33
66
  color: theme.custom.colors.white,
34
67
  },
35
- zIndex: 2,
68
+ zIndex: 3,
69
+ flexShrink: 0,
36
70
  }));
37
71
  const StyledTabButtonList = (0, styled_1.default)(TabButtonList_1.TabButtonList)(({ theme }) => ({
38
- padding: "80px 0 16px",
72
+ padding: "0 0 16px",
39
73
  backgroundColor: theme.custom.colors.white,
40
74
  position: "sticky",
41
- top: 0,
42
- zIndex: 1,
75
+ top: "84px",
76
+ zIndex: 2,
43
77
  overflow: "visible",
44
78
  }));
45
79
  const StyledTabPanel = (0, styled_1.default)(TabPanel_1.default)({
46
80
  padding: "0",
47
81
  height: "calc(100% - 138px)",
82
+ position: "relative",
48
83
  });
49
- const StyledAiChat = (0, styled_1.default)(AiChat_1.AiChat)({
50
- ".MitAiChat--title": {
51
- paddingTop: "8px",
84
+ const StyledAiChat = (0, styled_1.default)(AiChat_1.AiChat)(({ hasTabs }) => ({
85
+ ".MitAiChat--chatScreenContainer": {
86
+ padding: hasTabs ? 0 : "0 25px 0 40px",
52
87
  },
53
- });
88
+ ".MitAiChat--messagesContainer": {
89
+ paddingTop: hasTabs ? 0 : "88px",
90
+ },
91
+ }));
54
92
  const StyledHTML = styled_1.default.div(({ theme }) => (Object.assign(Object.assign({ color: theme.custom.colors.darkGray2, backgroundColor: theme.custom.colors.white, padding: "12px 0 100px" }, theme.typography.body2), { "p:first-of-type": {
55
93
  marginTop: 0,
56
94
  }, "p:last-of-type": {
@@ -70,25 +108,8 @@ const identity = (x) => x;
70
108
  const DEFAULT_FETCH_OPTS = {
71
109
  credentials: "include",
72
110
  };
73
- const parseContent = (contentString) => {
74
- var _a;
75
- try {
76
- const parsed = JSON.parse(contentString);
77
- const content = (_a = parsed[0]) === null || _a === void 0 ? void 0 : _a.content;
78
- const unescaped = content
79
- .replace(/\\n/g, "\n")
80
- .replace(/\\"/g, '"')
81
- .replace(/\\'/g, "'");
82
- return unescaped;
83
- }
84
- catch (e) {
85
- console.warn("Could not parse content:", e);
86
- return contentString;
87
- }
88
- };
89
111
  const useContentFetch = (contentUrl) => {
90
- const [summary, setSummary] = (0, react_1.useState)(null);
91
- const [error, setError] = (0, react_1.useState)(null);
112
+ const [response, setResponse] = (0, react_1.useState)(null);
92
113
  const [loading, setLoading] = (0, react_1.useState)(false);
93
114
  (0, react_1.useEffect)(() => {
94
115
  if (!contentUrl)
@@ -98,11 +119,20 @@ const useContentFetch = (contentUrl) => {
98
119
  try {
99
120
  const response = yield fetch(contentUrl);
100
121
  const result = yield response.json();
101
- const parsedContent = parseContent(result.content);
102
- setSummary(parsedContent);
122
+ if (!result.results) {
123
+ throw new Error("Unexpected response");
124
+ }
125
+ const [contentFile] = result.results;
126
+ if (!contentFile) {
127
+ throw new Error("No result found");
128
+ }
129
+ setResponse({
130
+ summary: contentFile.summary,
131
+ flashcards: contentFile.flashcards,
132
+ });
103
133
  }
104
- catch (err) {
105
- setError(err instanceof Error ? err : new Error("Failed to fetch"));
134
+ catch (error) {
135
+ console.error("Error fetching content", error);
106
136
  }
107
137
  finally {
108
138
  setLoading(false);
@@ -110,24 +140,40 @@ const useContentFetch = (contentUrl) => {
110
140
  });
111
141
  fetchData();
112
142
  }, [contentUrl]);
113
- return { summary, error, loading };
143
+ return { response, loading };
114
144
  };
115
- const ChatComponent = ({ payload, transformBody, fetchOpts, }) => {
145
+ const ChatComponent = ({ payload, transformBody, fetchOpts, scrollElement, hasTabs, }) => {
116
146
  if (!payload)
117
147
  return null;
118
- return (React.createElement(StyledAiChat, { chatId: payload.chatId, askTimTitle: payload.askTimTitle, conversationStarters: payload.conversationStarters, initialMessages: payload.initialMessages, requestOpts: {
148
+ return (React.createElement(StyledAiChat, { chatId: payload.chatId, conversationStarters: payload.conversationStarters, initialMessages: payload.initialMessages, entryScreenEnabled: false, scrollElement: scrollElement, requestOpts: {
119
149
  transformBody: (messages) => (Object.assign(Object.assign({}, payload.requestBody), transformBody === null || transformBody === void 0 ? void 0 : transformBody(messages))),
120
150
  apiUrl: payload.apiUrl,
121
151
  fetchOpts: Object.assign(Object.assign({}, DEFAULT_FETCH_OPTS), fetchOpts),
122
- } }));
152
+ }, hasTabs: hasTabs }));
123
153
  };
124
154
  const RemoteTutorDrawer = ({ messageOrigin, transformBody = identity, className, fetchOpts, target, }) => {
125
- var _a;
155
+ var _a, _b, _c;
126
156
  const [open, setOpen] = (0, react_1.useState)(false);
127
157
  const [payload, setPayload] = (0, react_1.useState)(null);
128
158
  const [tab, setTab] = (0, react_1.useState)("chat");
129
- const paperRef = (0, react_1.useRef)(null);
130
- const { summary } = useContentFetch((_a = payload === null || payload === void 0 ? void 0 : payload.summary) === null || _a === void 0 ? void 0 : _a.contentUrl);
159
+ const { response } = useContentFetch((_a = payload === null || payload === void 0 ? void 0 : payload.summary) === null || _a === void 0 ? void 0 : _a.apiUrl);
160
+ const [_wasKeyboardFocus, setWasKeyboardFocus] = (0, react_1.useState)(false);
161
+ const mouseInteracted = (0, react_1.useRef)(false);
162
+ const handleMouseDown = () => {
163
+ mouseInteracted.current = true;
164
+ };
165
+ const handleFocus = () => {
166
+ if (!mouseInteracted.current) {
167
+ setWasKeyboardFocus(true);
168
+ }
169
+ mouseInteracted.current = false;
170
+ };
171
+ const [scrollElement, setScrollElement] = (0, react_1.useState)(null);
172
+ const paperRefCallback = (node) => {
173
+ if (node) {
174
+ setScrollElement(node);
175
+ }
176
+ };
131
177
  (0, react_1.useEffect)(() => {
132
178
  const cb = (event) => {
133
179
  if (event.origin !== messageOrigin) {
@@ -153,30 +199,37 @@ const RemoteTutorDrawer = ({ messageOrigin, transformBody = identity, className,
153
199
  const { blockType, chat } = payload;
154
200
  const hasTabs = blockType === "video";
155
201
  return (React.createElement(Drawer_1.default, { className: className, PaperProps: {
156
- ref: paperRef,
202
+ ref: paperRefCallback,
157
203
  sx: {
158
204
  width: "900px",
159
205
  maxWidth: "100%",
160
206
  boxSizing: "border-box",
161
207
  scrollbarGutter: "stable",
162
- padding: hasTabs ? "0 25px 24px 40px" : "24px 25px 24px 40px",
163
- ".MitAiChat--title": {
164
- paddingTop: "0px",
165
- },
208
+ padding: "0 25px 0 40px",
166
209
  },
167
210
  }, anchor: "right", open: open, onClose: () => setOpen(false) },
168
- React.createElement(CloseButton, { variant: "text", size: "medium", onClick: () => setOpen(false), "aria-label": "Close" },
169
- React.createElement(react_2.RiCloseLine, null)),
170
- blockType === "problem" ? (React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts })) : null,
211
+ React.createElement(Header, null,
212
+ React.createElement(Title, null,
213
+ React.createElement(react_2.RiSparkling2Line, null),
214
+ React.createElement(Typography_1.default, { variant: "body1" }, ((_b = payload.title) === null || _b === void 0 ? void 0 : _b.includes("AskTIM")) ? (React.createElement(React.Fragment, null,
215
+ "Ask",
216
+ React.createElement("strong", null, "TIM"),
217
+ payload.title.replace("AskTIM", ""))) : (payload.title))),
218
+ React.createElement(CloseButton, { variant: "text", size: "medium", onClick: () => setOpen(false), "aria-label": "Close" },
219
+ React.createElement(react_2.RiCloseLine, null))),
220
+ blockType === "problem" ? (React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, hasTabs: hasTabs })) : null,
171
221
  blockType === "video" ? (React.createElement(TabContext_1.default, { value: tab },
172
222
  React.createElement(StyledTabButtonList, { styleVariant: "chat", onChange: (_event, val) => setTab(val) },
173
223
  React.createElement(TabButtonList_1.TabButton, { value: "chat", label: "Chat" }),
224
+ React.createElement(TabButtonList_1.TabButton, { value: "flashcards", label: "Flashcards", onMouseDown: handleMouseDown, onFocus: handleFocus }),
174
225
  React.createElement(TabButtonList_1.TabButton, { value: "summary", label: "Summary" })),
175
226
  React.createElement(StyledTabPanel, { value: "chat" },
176
- React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts })),
227
+ React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts, scrollElement: scrollElement, hasTabs: hasTabs })),
228
+ React.createElement(StyledTabPanel, { value: "flashcards" },
229
+ React.createElement(FlashcardsScreen_1.FlashcardsScreen, { flashcards: response === null || response === void 0 ? void 0 : response.flashcards, wasKeyboardFocus: _wasKeyboardFocus })),
177
230
  React.createElement(StyledTabPanel, { value: "summary" },
178
231
  React.createElement(Typography_1.default, { variant: "h4", component: "h4" }),
179
232
  React.createElement(StyledHTML, null,
180
- React.createElement(react_markdown_1.default, { rehypePlugins: [rehype_raw_1.default] }, summary))))) : null));
233
+ React.createElement(react_markdown_1.default, { rehypePlugins: [rehype_raw_1.default] }, (_c = response === null || response === void 0 ? void 0 : response.summary) !== null && _c !== void 0 ? _c : ""))))) : null));
181
234
  };
182
235
  exports.RemoteTutorDrawer = RemoteTutorDrawer;