@mitodl/smoot-design 3.8.0 → 5.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 (40) hide show
  1. package/dist/bundles/remoteTutorDrawer.es.js +38037 -0
  2. package/dist/bundles/remoteTutorDrawer.umd.js +208 -0
  3. package/dist/cjs/bundles/RemoteTutorDrawer/FlashcardsScreen.d.ts +9 -0
  4. package/dist/cjs/bundles/RemoteTutorDrawer/FlashcardsScreen.js +87 -0
  5. package/dist/{esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.d.ts → cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts} +24 -13
  6. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +200 -0
  7. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.d.ts +7 -0
  8. package/dist/cjs/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +157 -0
  9. package/dist/cjs/bundles/remoteTutorDrawer.d.ts +7 -0
  10. package/dist/cjs/bundles/{remoteAiChatDrawer.js → remoteTutorDrawer.js} +3 -3
  11. package/dist/cjs/components/AiChat/types.d.ts +1 -1
  12. package/dist/cjs/components/TabButtons/TabButtonList.d.ts +16 -0
  13. package/dist/cjs/components/TabButtons/TabButtonList.js +86 -0
  14. package/dist/cjs/components/TabButtons/TabButtonList.stories.d.ts +24 -0
  15. package/dist/cjs/components/TabButtons/TabButtonList.stories.js +139 -0
  16. package/dist/esm/bundles/RemoteTutorDrawer/FlashcardsScreen.d.ts +9 -0
  17. package/dist/esm/bundles/RemoteTutorDrawer/FlashcardsScreen.js +83 -0
  18. package/dist/{cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.d.ts → esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.d.ts} +24 -13
  19. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.js +197 -0
  20. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.d.ts +7 -0
  21. package/dist/esm/bundles/RemoteTutorDrawer/RemoteTutorDrawer.stories.js +154 -0
  22. package/dist/esm/bundles/remoteTutorDrawer.d.ts +7 -0
  23. package/dist/esm/bundles/{remoteAiChatDrawer.js → remoteTutorDrawer.js} +3 -3
  24. package/dist/esm/components/AiChat/types.d.ts +1 -1
  25. package/dist/esm/components/TabButtons/TabButtonList.d.ts +16 -0
  26. package/dist/esm/components/TabButtons/TabButtonList.js +81 -0
  27. package/dist/esm/components/TabButtons/TabButtonList.stories.d.ts +24 -0
  28. package/dist/esm/components/TabButtons/TabButtonList.stories.js +136 -0
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/package.json +23 -20
  31. package/dist/bundles/remoteAiChatDrawer.es.js +0 -27902
  32. package/dist/bundles/remoteAiChatDrawer.umd.js +0 -198
  33. package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.js +0 -50
  34. package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.d.ts +0 -6
  35. package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +0 -69
  36. package/dist/cjs/bundles/remoteAiChatDrawer.d.ts +0 -7
  37. package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.js +0 -47
  38. package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.d.ts +0 -6
  39. package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +0 -66
  40. package/dist/esm/bundles/remoteAiChatDrawer.d.ts +0 -7
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = void 0;
4
4
  const React = require("react");
5
5
  const client_1 = require("react-dom/client");
6
- const RemoteAiChatDrawer_1 = require("./RemoteAiChatDrawer/RemoteAiChatDrawer");
6
+ const RemoteTutorDrawer_1 = require("./RemoteTutorDrawer/RemoteTutorDrawer");
7
7
  const ThemeProvider_1 = require("../components/ThemeProvider/ThemeProvider");
8
8
  const react_1 = require("@emotion/react");
9
9
  const cache_1 = require("@emotion/cache");
10
10
  /**
11
- * Renders the AiChatDrawer in an shadow DOM in order to isolate the drawer
11
+ * Renders the RemoteTutorDrawer in an shadow DOM in order to isolate the drawer
12
12
  * styles from external stylesheets.
13
13
  */
14
14
  const init = (opts) => {
@@ -35,6 +35,6 @@ const init = (opts) => {
35
35
  });
36
36
  (0, client_1.createRoot)(shadowRootEl).render(React.createElement(react_1.CacheProvider, { value: cache },
37
37
  React.createElement(ThemeProvider_1.ThemeProvider, { theme: theme },
38
- React.createElement(RemoteAiChatDrawer_1.AiChatDrawer, Object.assign({}, opts)))));
38
+ React.createElement(RemoteTutorDrawer_1.RemoteTutorDrawer, Object.assign({}, opts)))));
39
39
  };
40
40
  exports.init = init;
@@ -69,7 +69,7 @@ type AiChatProps = {
69
69
  * Where the scroll container is provided by the component,
70
70
  * the AiChat will scroll to the bottom when a prompt is submitted.
71
71
  */
72
- scrollContainer?: HTMLElement;
72
+ scrollContainer?: HTMLElement | null;
73
73
  /**
74
74
  * Provide a ref to the chat component to access the `append` method.
75
75
  */
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import type { TabProps } from "@mui/material/Tab";
3
+ import type { TabListProps } from "@mui/lab/TabList";
4
+ import type { ButtonLinkProps } from "../Button/Button";
5
+ type StyleVariant = "chat";
6
+ export type TabButtonListProps = TabListProps & {
7
+ styleVariant?: StyleVariant;
8
+ };
9
+ declare const TabButtonList: React.FC<TabButtonListProps>;
10
+ declare const TabLinkInner: React.ForwardRefExoticComponent<Omit<ButtonLinkProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
11
+ type TabButtonProps = Omit<TabProps<"button">, "color"> & {
12
+ styleVariant?: StyleVariant;
13
+ };
14
+ declare const TabButton: (props: TabButtonProps) => React.JSX.Element;
15
+ declare const TabButtonLink: ({ ...props }: TabProps<typeof TabLinkInner>) => React.JSX.Element;
16
+ export { TabButtonList, TabButton, TabButtonLink };
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.TabButtonLink = exports.TabButton = exports.TabButtonList = void 0;
15
+ const React = require("react");
16
+ const Tab_1 = require("@mui/material/Tab");
17
+ const TabList_1 = require("@mui/lab/TabList");
18
+ const styled_1 = require("@emotion/styled");
19
+ const Button_1 = require("../Button/Button");
20
+ const react_1 = require("@emotion/react");
21
+ const defaultTabListProps = {
22
+ variant: "scrollable",
23
+ allowScrollButtonsMobile: true,
24
+ scrollButtons: "auto",
25
+ };
26
+ const TabButtonList = (0, styled_1.default)((props) => (React.createElement(TabList_1.default, Object.assign({}, defaultTabListProps, props))))(({ theme, styleVariant }) => (Object.assign({ minHeight: "unset", ".MuiTabs-indicator": {
27
+ display: "none",
28
+ }, ".MuiTabs-flexContainer": {
29
+ gap: "8px",
30
+ alignItems: "center",
31
+ }, ".MuiTabs-scroller": {
32
+ display: "flex",
33
+ } }, (styleVariant === "chat" && {
34
+ flexGrow: 1,
35
+ ".MuiTabs-flexContainer": {
36
+ flexGrow: 1,
37
+ gap: "8px",
38
+ alignItems: "center",
39
+ backgroundColor: theme.custom.colors.lightGray1,
40
+ padding: "4px",
41
+ borderRadius: "8px",
42
+ },
43
+ button: Object.assign({ flexGrow: 1, backgroundColor: "transparent" }, theme.typography.body2),
44
+ "button[aria-selected=true], button[aria-selected=true]:hover": {
45
+ backgroundColor: theme.custom.colors.darkGray1,
46
+ color: theme.custom.colors.white,
47
+ cursor: "default",
48
+ },
49
+ }))));
50
+ exports.TabButtonList = TabButtonList;
51
+ const tabStyles = ({ theme }) => (0, react_1.css)({
52
+ minWidth: "auto",
53
+ ":focus-visible": {
54
+ outlineOffset: "-1px",
55
+ },
56
+ '&[aria-selected="true"]': {
57
+ backgroundColor: theme.custom.colors.white,
58
+ borderColor: theme.custom.colors.darkGray2,
59
+ },
60
+ });
61
+ const TabButtonStyled = (0, styled_1.default)(Button_1.Button)(tabStyles);
62
+ const TabLinkStyled = (0, styled_1.default)(Button_1.ButtonLink)(tabStyles);
63
+ const defaultTabButtonProps = {
64
+ variant: "tertiary",
65
+ size: "small",
66
+ };
67
+ const TabButtonInner = React.forwardRef(
68
+ // Omits the `className` prop from the underlying Button so that MUI does not
69
+ // style it. We style it ourselves.
70
+ (props, ref) => {
71
+ const { className } = props, others = __rest(props, ["className"]);
72
+ return React.createElement(TabButtonStyled, Object.assign({}, defaultTabButtonProps, others, { ref: ref }));
73
+ });
74
+ TabButtonInner.displayName = "TabButtonInner";
75
+ const TabLinkInner = React.forwardRef((props, ref) => {
76
+ const { className } = props, others = __rest(props, ["className"]);
77
+ return React.createElement(TabLinkStyled, Object.assign({}, defaultTabButtonProps, others, { ref: ref }));
78
+ });
79
+ TabLinkInner.displayName = "TabLinkInner";
80
+ const TabButton = (props) => (React.createElement(Tab_1.default, Object.assign({}, props, { component: TabButtonInner })));
81
+ exports.TabButton = TabButton;
82
+ const TabButtonLink = (_a) => {
83
+ var props = __rest(_a, []);
84
+ return (React.createElement(Tab_1.default, Object.assign({}, props, { component: TabLinkInner })));
85
+ };
86
+ exports.TabButtonLink = TabButtonLink;
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import type { TabButtonListProps } from "./TabButtonList";
3
+ type StoryProps = TabButtonListProps & {
4
+ count: number;
5
+ };
6
+ declare const meta: Meta<StoryProps>;
7
+ export default meta;
8
+ type Story = StoryObj<StoryProps>;
9
+ /**
10
+ * Use `TabButtonList` and `TabButton` to render a list of tabs styled as our tertiary buttons:
11
+ */
12
+ export declare const ButtonTabs: Story;
13
+ /**
14
+ * `TabButtonList` chat style variant:
15
+ */
16
+ export declare const ButtonTabsChatVariant: Story;
17
+ /**
18
+ * By default, the tabs will be scrollable if there are too many to fit in the container:
19
+ */
20
+ export declare const ManyButtonTabs: Story;
21
+ /**
22
+ * Use `TabButtonLink` for tabs that should affect the URL:
23
+ */
24
+ export declare const LinkTabs: Story;
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.LinkTabs = exports.ManyButtonTabs = exports.ButtonTabsChatVariant = exports.ButtonTabs = void 0;
15
+ /* eslint-disable react-hooks/rules-of-hooks */
16
+ const React = require("react");
17
+ const react_1 = require("react");
18
+ const TabButtonList_1 = require("./TabButtonList");
19
+ const TabContext_1 = require("@mui/lab/TabContext");
20
+ const Button_1 = require("../Button/Button");
21
+ const Stack_1 = require("@mui/material/Stack");
22
+ const TabPanel_1 = require("@mui/lab/TabPanel");
23
+ const Typography_1 = require("@mui/material/Typography");
24
+ const en_1 = require("@faker-js/faker/locale/en");
25
+ const Container_1 = require("@mui/material/Container");
26
+ const meta = {
27
+ title: "smoot-design/TabButtons",
28
+ argTypes: {
29
+ variant: {
30
+ options: ["scrollable", "fullWidth", "standard"],
31
+ control: { type: "radio" },
32
+ },
33
+ styleVariant: {
34
+ options: ["default", "chat"],
35
+ control: { type: "radio" },
36
+ defaultValue: "default",
37
+ },
38
+ scrollButtons: {
39
+ options: ["auto", true, false],
40
+ control: { type: "radio" },
41
+ },
42
+ },
43
+ args: {
44
+ count: 4,
45
+ variant: "scrollable",
46
+ allowScrollButtonsMobile: true,
47
+ scrollButtons: "auto",
48
+ },
49
+ render: (_a) => {
50
+ var { count } = _a, others = __rest(_a, ["count"]);
51
+ const [value, setValue] = React.useState("tab1");
52
+ return (React.createElement(Container_1.default, { maxWidth: "sm" },
53
+ React.createElement(TabContext_1.default, { value: value },
54
+ React.createElement(Stack_1.default, { direction: "row" },
55
+ React.createElement(TabButtonList_1.TabButtonList, Object.assign({}, others, { onChange: (_event, val) => setValue(val) }), Array(count)
56
+ .fill(null)
57
+ .map((_, i) => (React.createElement(TabButtonList_1.TabButton, { key: `tab-${i}`, value: `tab${i + 1}`, label: `Tab ${i + 1}` })))),
58
+ React.createElement(Stack_1.default, { direction: "row", justifyContent: "end", sx: { paddingLeft: "16px" } },
59
+ React.createElement(Button_1.Button, null, "Other UI"))),
60
+ Array(count)
61
+ .fill(null)
62
+ .map((_, i) => (React.createElement(TabPanel_1.default, { key: `tab-${i}`, value: `tab${i + 1}` },
63
+ React.createElement(Typography_1.default, { variant: "h4", component: "h4" },
64
+ "Header ",
65
+ i + 1),
66
+ en_1.faker.lorem.paragraphs(2)))))));
67
+ },
68
+ };
69
+ exports.default = meta;
70
+ /**
71
+ * Use `TabButtonList` and `TabButton` to render a list of tabs styled as our tertiary buttons:
72
+ */
73
+ exports.ButtonTabs = {};
74
+ /**
75
+ * `TabButtonList` chat style variant:
76
+ */
77
+ exports.ButtonTabsChatVariant = {
78
+ args: {
79
+ styleVariant: "chat",
80
+ variant: "fullWidth",
81
+ visibleScrollbar: false,
82
+ },
83
+ render: (_a) => {
84
+ var { count } = _a, others = __rest(_a, ["count"]);
85
+ const [value, setValue] = React.useState("tab1");
86
+ return (React.createElement(Container_1.default, { maxWidth: "sm" },
87
+ React.createElement(TabContext_1.default, { value: value },
88
+ React.createElement(Stack_1.default, { direction: "row" },
89
+ React.createElement(TabButtonList_1.TabButtonList, Object.assign({}, others, { onChange: (_event, val) => setValue(val) }), Array(count)
90
+ .fill(null)
91
+ .map((_, i) => (React.createElement(TabButtonList_1.TabButton, { key: `tab-${i}`, value: `tab${i + 1}`, label: `Tab ${i + 1}` }))))),
92
+ Array(count)
93
+ .fill(null)
94
+ .map((_, i) => (React.createElement(TabPanel_1.default, { key: `tab-${i}`, value: `tab${i + 1}` },
95
+ React.createElement(Typography_1.default, { variant: "h4", component: "h4" },
96
+ "Header ",
97
+ i + 1),
98
+ en_1.faker.lorem.paragraphs(2)))))));
99
+ },
100
+ };
101
+ /**
102
+ * By default, the tabs will be scrollable if there are too many to fit in the container:
103
+ */
104
+ exports.ManyButtonTabs = {
105
+ args: {
106
+ count: 12,
107
+ },
108
+ };
109
+ /**
110
+ * Use `TabButtonLink` for tabs that should affect the URL:
111
+ */
112
+ exports.LinkTabs = {
113
+ parameters: {
114
+ nextjs: {
115
+ appDirectory: true,
116
+ navigation: {
117
+ pathname: "/#link2",
118
+ },
119
+ },
120
+ },
121
+ render: () => {
122
+ const [hash, setHash] = (0, react_1.useState)(() => window.location.hash);
123
+ React.useEffect(() => {
124
+ const handler = () => setHash(window.location.hash);
125
+ window.addEventListener("hashchange", handler);
126
+ return () => {
127
+ window.removeEventListener("hashchange", handler);
128
+ };
129
+ }, []);
130
+ return (React.createElement("div", null,
131
+ "Current Location:",
132
+ React.createElement("pre", null, hash),
133
+ React.createElement(TabContext_1.default, { value: hash },
134
+ React.createElement(TabButtonList_1.TabButtonList, null,
135
+ React.createElement(TabButtonList_1.TabButtonLink, { value: "#link1", href: "#link1", label: "Tab 1" }),
136
+ React.createElement(TabButtonList_1.TabButtonLink, { value: "#link2", href: "#link2", label: "Tab 2" }),
137
+ React.createElement(TabButtonList_1.TabButtonLink, { value: "#link3", href: "#link3", label: "Tab 3" })))));
138
+ },
139
+ };
@@ -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,83 @@
1
+ import { ActionButton } from "../../components/Button/ActionButton";
2
+ import Typography from "@mui/material/Typography";
3
+ import * as React from "react";
4
+ import { useState, useCallback, useEffect, useRef } from "react";
5
+ import styled from "@emotion/styled";
6
+ import { RiArrowRightLine, RiArrowLeftLine } from "@remixicon/react";
7
+ const Container = styled.div ``;
8
+ const FlashcardContainer = styled.div(({ theme }) => ({
9
+ display: "flex",
10
+ height: 300,
11
+ padding: 40,
12
+ flexDirection: "column",
13
+ justifyContent: "center",
14
+ alignItems: "center",
15
+ alignSelf: "stretch",
16
+ borderRadius: 8,
17
+ border: `1px solid ${theme.custom.colors.lightGray2}`,
18
+ marginTop: "8px",
19
+ cursor: "pointer",
20
+ textAlign: "center",
21
+ }));
22
+ const Navigation = styled.div({
23
+ display: "flex",
24
+ justifyContent: "space-between",
25
+ alignItems: "center",
26
+ width: "100%",
27
+ marginTop: "24px",
28
+ });
29
+ const Page = styled.div(({ theme }) => (Object.assign({ color: theme.custom.colors.silverGrayDark }, theme.typography.body2)));
30
+ const Flashcard = React.forwardRef(({ content, "aria-label": ariaLabel }, ref) => {
31
+ const [screen, setScreen] = useState(0);
32
+ useEffect(() => setScreen(0), [content]);
33
+ const handleKeyDown = (e) => {
34
+ if (e.key === "Enter" || e.key === " ") {
35
+ setScreen(screen === 0 ? 1 : 0);
36
+ }
37
+ };
38
+ return (React.createElement(FlashcardContainer, { ref: ref, onClick: () => setScreen(screen === 0 ? 1 : 0), onKeyDown: handleKeyDown, tabIndex: 0, "aria-label": ariaLabel },
39
+ React.createElement(Typography, { variant: "h5" }, screen === 0 ? `Q: ${content.question}` : `Answer: ${content.answer}`)));
40
+ });
41
+ Flashcard.displayName = "Flashcard";
42
+ export const FlashcardsScreen = ({ flashcards, wasKeyboardFocus, }) => {
43
+ const [cardIndex, setCardIndex] = useState(0);
44
+ const containerRef = useRef(null);
45
+ const flashcardRef = useRef(null);
46
+ const handleKeyDown = useCallback((e) => {
47
+ var _a;
48
+ if (!flashcards)
49
+ return;
50
+ if (!((_a = containerRef.current) === null || _a === void 0 ? void 0 : _a.contains(document.activeElement)) &&
51
+ wasKeyboardFocus) {
52
+ return;
53
+ }
54
+ if (e.key === "ArrowRight") {
55
+ setCardIndex((prevIndex) => (prevIndex + 1) % flashcards.length);
56
+ }
57
+ else if (e.key === "ArrowLeft") {
58
+ setCardIndex((prevIndex) => (prevIndex - 1 + flashcards.length) % flashcards.length);
59
+ }
60
+ }, [flashcards, wasKeyboardFocus]);
61
+ useEffect(() => {
62
+ var _a;
63
+ (_a = flashcardRef.current) === null || _a === void 0 ? void 0 : _a.focus();
64
+ }, [cardIndex]);
65
+ useEffect(() => {
66
+ window.addEventListener("keydown", handleKeyDown);
67
+ return () => window.removeEventListener("keydown", handleKeyDown);
68
+ }, [handleKeyDown]);
69
+ if (!flashcards) {
70
+ return null;
71
+ }
72
+ return (React.createElement(Container, { ref: containerRef },
73
+ React.createElement(Flashcard, { ref: flashcardRef, content: flashcards[cardIndex], "aria-label": `Flashcard ${cardIndex + 1} of ${flashcards.length}` }),
74
+ React.createElement(Navigation, null,
75
+ React.createElement(ActionButton, { onClick: () => setCardIndex(cardIndex - 1), disabled: cardIndex === 0, variant: "secondary", color: "secondary", size: "small" },
76
+ React.createElement(RiArrowLeftLine, { "aria-hidden": true })),
77
+ React.createElement(Page, null,
78
+ cardIndex + 1,
79
+ " / ",
80
+ flashcards.length),
81
+ React.createElement(ActionButton, { onClick: () => setCardIndex(cardIndex + 1), disabled: cardIndex === flashcards.length - 1, variant: "secondary", color: "secondary", size: "small" },
82
+ React.createElement(RiArrowRightLine, { "aria-hidden": true })))));
83
+ };
@@ -1,18 +1,25 @@
1
- import * as React from "react";
1
+ import { FC } from "react";
2
2
  import { AiChatMessage } from "../../components/AiChat/types";
3
3
  import type { AiChatProps } from "../../components/AiChat/AiChat";
4
- type ChatInitMessage = {
5
- type: "smoot-design::chat-open";
4
+ type RemoteTutorDrawerInitMessage = {
5
+ type: "smoot-design::tutor-drawer-open";
6
6
  payload: {
7
- chatId?: AiChatProps["chatId"];
8
- askTimTitle?: AiChatProps["title"];
9
- conversationStarters?: AiChatProps["conversationStarters"];
10
- initialMessages: AiChatProps["initialMessages"];
11
- apiUrl: AiChatProps["requestOpts"]["apiUrl"];
12
- requestBody?: Record<string, unknown>;
7
+ blockType?: "problem" | "video";
8
+ target?: string;
9
+ chat: {
10
+ chatId?: AiChatProps["chatId"];
11
+ askTimTitle?: AiChatProps["title"];
12
+ conversationStarters?: AiChatProps["conversationStarters"];
13
+ initialMessages: AiChatProps["initialMessages"];
14
+ apiUrl: AiChatProps["requestOpts"]["apiUrl"];
15
+ requestBody?: Record<string, unknown>;
16
+ };
17
+ summary?: {
18
+ apiUrl: string;
19
+ };
13
20
  };
14
21
  };
15
- type AiChatDrawerProps = {
22
+ type RemoteTutorDrawerProps = {
16
23
  className?: string;
17
24
  /**
18
25
  * The origin of the messages that will be received to open the chat.
@@ -35,7 +42,11 @@ type AiChatDrawerProps = {
35
42
  * identifying cookies.
36
43
  */
37
44
  fetchOpts?: AiChatProps["requestOpts"]["fetchOpts"];
45
+ /**
46
+ * Pass to target a specific drawer instance where multiple are on the page.
47
+ */
48
+ target?: string;
38
49
  };
39
- declare const AiChatDrawer: React.FC<AiChatDrawerProps>;
40
- export { AiChatDrawer };
41
- export type { AiChatDrawerProps, ChatInitMessage };
50
+ declare const RemoteTutorDrawer: FC<RemoteTutorDrawerProps>;
51
+ export { RemoteTutorDrawer };
52
+ export type { RemoteTutorDrawerProps, RemoteTutorDrawerInitMessage };
@@ -0,0 +1,197 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import * as React from "react";
11
+ import { useEffect, useRef, useState } from "react";
12
+ import styled from "@emotion/styled";
13
+ import Markdown from "react-markdown";
14
+ import rehypeRaw from "rehype-raw";
15
+ import { RiCloseLine } from "@remixicon/react";
16
+ import Drawer from "@mui/material/Drawer";
17
+ import { TabButtonList, TabButton, } from "../../components/TabButtons/TabButtonList";
18
+ import Typography from "@mui/material/Typography";
19
+ import TabContext from "@mui/lab/TabContext";
20
+ import TabPanel from "@mui/lab/TabPanel";
21
+ import { AiChat } from "../../components/AiChat/AiChat";
22
+ import { ActionButton } from "../../components/Button/ActionButton";
23
+ import { FlashcardsScreen } from "./FlashcardsScreen";
24
+ const CloseButton = styled(ActionButton)(({ theme }) => ({
25
+ position: "fixed",
26
+ top: "24px",
27
+ right: "40px",
28
+ backgroundColor: theme.custom.colors.lightGray2,
29
+ "&&:hover": {
30
+ backgroundColor: theme.custom.colors.red,
31
+ color: theme.custom.colors.white,
32
+ },
33
+ zIndex: 2,
34
+ }));
35
+ const StyledTabButtonList = styled(TabButtonList)(({ theme }) => ({
36
+ padding: "80px 0 16px",
37
+ backgroundColor: theme.custom.colors.white,
38
+ position: "sticky",
39
+ top: 0,
40
+ zIndex: 1,
41
+ overflow: "visible",
42
+ }));
43
+ const StyledTabPanel = styled(TabPanel)({
44
+ padding: "0",
45
+ height: "calc(100% - 138px)",
46
+ });
47
+ const StyledAiChat = styled(AiChat)({
48
+ ".MitAiChat--title": {
49
+ paddingTop: "8px",
50
+ },
51
+ });
52
+ const StyledHTML = styled.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": {
53
+ marginTop: 0,
54
+ }, "p:last-of-type": {
55
+ marginBottom: 0,
56
+ }, "ol, ul": {
57
+ paddingInlineStart: "32px",
58
+ li: {
59
+ margin: "16px 0",
60
+ },
61
+ }, ul: {
62
+ marginInlineStart: "-16px",
63
+ }, a: {
64
+ color: theme.custom.colors.red,
65
+ fontWeight: "normal",
66
+ } })));
67
+ const identity = (x) => x;
68
+ const DEFAULT_FETCH_OPTS = {
69
+ credentials: "include",
70
+ };
71
+ const parseContent = (summaryString) => {
72
+ var _a;
73
+ try {
74
+ const parsed = JSON.parse(summaryString);
75
+ const content = (_a = parsed[0]) === null || _a === void 0 ? void 0 : _a.content;
76
+ const unescaped = content
77
+ .replace(/\\n/g, "\n")
78
+ .replace(/\\"/g, '"')
79
+ .replace(/\\'/g, "'");
80
+ return unescaped;
81
+ }
82
+ catch (e) {
83
+ console.warn("Could not parse summary:", e);
84
+ return summaryString;
85
+ }
86
+ };
87
+ const useContentFetch = (contentUrl) => {
88
+ const [response, setResponse] = useState(null);
89
+ const [error, setError] = useState(null);
90
+ const [loading, setLoading] = useState(false);
91
+ useEffect(() => {
92
+ if (!contentUrl)
93
+ return;
94
+ const fetchData = () => __awaiter(void 0, void 0, void 0, function* () {
95
+ setLoading(true);
96
+ try {
97
+ const response = yield fetch(contentUrl);
98
+ const result = yield response.json();
99
+ const parsedSummary = parseContent(result.summary);
100
+ setResponse({
101
+ summary: parsedSummary,
102
+ flashcards: result.flashcards,
103
+ });
104
+ }
105
+ catch (err) {
106
+ setError(err instanceof Error ? err : new Error("Failed to fetch"));
107
+ }
108
+ finally {
109
+ setLoading(false);
110
+ }
111
+ });
112
+ fetchData();
113
+ }, [contentUrl]);
114
+ return { response, error, loading };
115
+ };
116
+ const ChatComponent = ({ payload, transformBody, fetchOpts, }) => {
117
+ if (!payload)
118
+ return null;
119
+ return (React.createElement(StyledAiChat, { chatId: payload.chatId, askTimTitle: payload.askTimTitle, conversationStarters: payload.conversationStarters, initialMessages: payload.initialMessages, requestOpts: {
120
+ transformBody: (messages) => (Object.assign(Object.assign({}, payload.requestBody), transformBody === null || transformBody === void 0 ? void 0 : transformBody(messages))),
121
+ apiUrl: payload.apiUrl,
122
+ fetchOpts: Object.assign(Object.assign({}, DEFAULT_FETCH_OPTS), fetchOpts),
123
+ } }));
124
+ };
125
+ const RemoteTutorDrawer = ({ messageOrigin, transformBody = identity, className, fetchOpts, target, }) => {
126
+ var _a, _b;
127
+ const [open, setOpen] = useState(false);
128
+ const [payload, setPayload] = useState(null);
129
+ const [tab, setTab] = useState("chat");
130
+ const paperRef = useRef(null);
131
+ const { response } = useContentFetch((_a = payload === null || payload === void 0 ? void 0 : payload.summary) === null || _a === void 0 ? void 0 : _a.apiUrl);
132
+ const [_wasKeyboardFocus, setWasKeyboardFocus] = useState(false);
133
+ const mouseInteracted = useRef(false);
134
+ const handleMouseDown = () => {
135
+ mouseInteracted.current = true;
136
+ };
137
+ const handleFocus = () => {
138
+ if (!mouseInteracted.current) {
139
+ setWasKeyboardFocus(true);
140
+ }
141
+ mouseInteracted.current = false;
142
+ };
143
+ useEffect(() => {
144
+ const cb = (event) => {
145
+ if (event.origin !== messageOrigin) {
146
+ if (process.env.NODE_ENV === "development") {
147
+ console.warn(`RemoteTutorDrawer: received message from unexpected origin: ${event.origin}`);
148
+ }
149
+ return;
150
+ }
151
+ if (event.data.type === "smoot-design::tutor-drawer-open" &&
152
+ event.data.payload.target === target) {
153
+ setOpen(true);
154
+ setPayload(event.data.payload);
155
+ }
156
+ };
157
+ window.addEventListener("message", cb);
158
+ return () => {
159
+ window.removeEventListener("message", cb);
160
+ };
161
+ }, [messageOrigin, target]);
162
+ if (!payload) {
163
+ return null;
164
+ }
165
+ const { blockType, chat } = payload;
166
+ const hasTabs = blockType === "video";
167
+ return (React.createElement(Drawer, { className: className, PaperProps: {
168
+ ref: paperRef,
169
+ sx: {
170
+ width: "900px",
171
+ maxWidth: "100%",
172
+ boxSizing: "border-box",
173
+ scrollbarGutter: "stable",
174
+ padding: hasTabs ? "0 25px 24px 40px" : "24px 25px 24px 40px",
175
+ ".MitAiChat--title": {
176
+ paddingTop: "0px",
177
+ },
178
+ },
179
+ }, anchor: "right", open: open, onClose: () => setOpen(false) },
180
+ React.createElement(CloseButton, { variant: "text", size: "medium", onClick: () => setOpen(false), "aria-label": "Close" },
181
+ React.createElement(RiCloseLine, null)),
182
+ blockType === "problem" ? (React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts })) : null,
183
+ blockType === "video" ? (React.createElement(TabContext, { value: tab },
184
+ React.createElement(StyledTabButtonList, { styleVariant: "chat", onChange: (_event, val) => setTab(val) },
185
+ React.createElement(TabButton, { value: "chat", label: "Chat" }),
186
+ React.createElement(TabButton, { value: "flashcards", label: "Flashcards", onMouseDown: handleMouseDown, onFocus: handleFocus }),
187
+ React.createElement(TabButton, { value: "summary", label: "Summary" })),
188
+ React.createElement(StyledTabPanel, { value: "chat" },
189
+ React.createElement(ChatComponent, { payload: chat, transformBody: transformBody, fetchOpts: fetchOpts })),
190
+ React.createElement(StyledTabPanel, { value: "flashcards" },
191
+ React.createElement(FlashcardsScreen, { flashcards: response === null || response === void 0 ? void 0 : response.flashcards, wasKeyboardFocus: _wasKeyboardFocus })),
192
+ React.createElement(StyledTabPanel, { value: "summary" },
193
+ React.createElement(Typography, { variant: "h4", component: "h4" }),
194
+ React.createElement(StyledHTML, null,
195
+ React.createElement(Markdown, { rehypePlugins: [rehypeRaw] }, (_b = response === null || response === void 0 ? void 0 : response.summary) !== null && _b !== void 0 ? _b : ""))))) : null));
196
+ };
197
+ export { RemoteTutorDrawer };
@@ -0,0 +1,7 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { RemoteTutorDrawer } from "./RemoteTutorDrawer";
3
+ declare const meta: Meta<typeof RemoteTutorDrawer>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof RemoteTutorDrawer>;
6
+ export declare const ProblemStory: Story;
7
+ export declare const VideoStory: Story;