@mitodl/smoot-design 1.2.1 → 3.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.
- package/README.md +4 -0
- package/dist/cjs/components/AiChat/AiChat.js +150 -58
- package/dist/cjs/components/AiChat/AiChat.stories.js +5 -2
- package/dist/cjs/components/AiChat/AiChat.test.js +39 -7
- package/dist/cjs/components/AiChat/story-utils.js +6 -3
- package/dist/cjs/components/AiChat/types.d.ts +26 -6
- package/dist/cjs/components/AiChat/utils.js +13 -3
- package/dist/cjs/components/Button/ActionButton.stories.js +0 -3
- package/dist/cjs/components/Button/Button.d.ts +2 -4
- package/dist/cjs/components/Button/Button.js +0 -33
- package/dist/cjs/components/Button/Button.stories.js +0 -3
- package/dist/cjs/components/ImageAdapter/ImageAdapter.d.ts +23 -0
- package/dist/cjs/components/ImageAdapter/ImageAdapter.js +30 -0
- package/dist/cjs/components/LinkAdapter/LinkAdapter.d.ts +1 -1
- package/dist/cjs/components/ThemeProvider/ThemeProvider.d.ts +1 -3
- package/dist/cjs/components/ThemeProvider/ThemeProvider.js +4 -8
- package/dist/cjs/components/ThemeProvider/ThemeProvider.stories.d.ts +6 -1
- package/dist/cjs/components/ThemeProvider/ThemeProvider.stories.js +6 -1
- package/dist/cjs/components/ThemeProvider/breakpoints.js +0 -1
- package/dist/cjs/components/ThemeProvider/typography.js +0 -1
- package/dist/cjs/index.d.ts +0 -1
- package/dist/cjs/index.js +1 -3
- package/dist/esm/components/AiChat/AiChat.js +151 -59
- package/dist/esm/components/AiChat/AiChat.stories.js +5 -2
- package/dist/esm/components/AiChat/AiChat.test.js +39 -7
- package/dist/esm/components/AiChat/story-utils.js +6 -3
- package/dist/esm/components/AiChat/types.d.ts +26 -6
- package/dist/esm/components/AiChat/utils.js +13 -3
- package/dist/esm/components/Button/ActionButton.stories.js +0 -3
- package/dist/esm/components/Button/Button.d.ts +2 -4
- package/dist/esm/components/Button/Button.js +0 -33
- package/dist/esm/components/Button/Button.stories.js +0 -3
- package/dist/esm/components/ImageAdapter/ImageAdapter.d.ts +23 -0
- package/dist/esm/components/ImageAdapter/ImageAdapter.js +27 -0
- package/dist/esm/components/LinkAdapter/LinkAdapter.d.ts +1 -1
- package/dist/esm/components/ThemeProvider/ThemeProvider.d.ts +1 -3
- package/dist/esm/components/ThemeProvider/ThemeProvider.js +4 -8
- package/dist/esm/components/ThemeProvider/ThemeProvider.stories.d.ts +6 -1
- package/dist/esm/components/ThemeProvider/ThemeProvider.stories.js +6 -1
- package/dist/esm/components/ThemeProvider/breakpoints.js +0 -1
- package/dist/esm/components/ThemeProvider/typography.js +0 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +0 -1
- package/dist/static/images/mit_mascot_tim.png +0 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/type-augmentation/TypescriptDocs.mdx +1 -1
- package/dist/type-augmentation/imports.d.ts +3 -0
- package/dist/type-augmentation/index.d.ts +1 -0
- package/dist/type-augmentation/theme.d.ts +2 -1
- package/package.json +20 -16
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* ImageAdapterPropsOverrides can be used with module augmentation to provide
|
|
4
|
+
* extra props to ButtonLink.
|
|
5
|
+
*
|
|
6
|
+
* For example, in a NextJS App, you might set `next/image` as your default
|
|
7
|
+
* image implementation, and use ImageAdapterPropsOverrides to provide
|
|
8
|
+
* `next/image`-specific props.
|
|
9
|
+
*/
|
|
10
|
+
interface ImageAdapterPropsOverrides {
|
|
11
|
+
}
|
|
12
|
+
type ImageAdapterProps = React.ComponentProps<"img"> & {
|
|
13
|
+
Component?: React.ElementType;
|
|
14
|
+
} & ImageAdapterPropsOverrides;
|
|
15
|
+
/**
|
|
16
|
+
* Overrideable Image component.
|
|
17
|
+
* - If `Component` is provided, renders as `Component`
|
|
18
|
+
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
19
|
+
* - else, renders as `img` tag
|
|
20
|
+
*/
|
|
21
|
+
declare const ImageAdapter: React.ForwardRefExoticComponent<Omit<ImageAdapterProps, "ref"> & React.RefAttributes<HTMLImageElement>>;
|
|
22
|
+
export { ImageAdapter };
|
|
23
|
+
export type { ImageAdapterPropsOverrides, ImageAdapterProps };
|
|
@@ -0,0 +1,30 @@
|
|
|
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.ImageAdapter = void 0;
|
|
15
|
+
const React = require("react");
|
|
16
|
+
const react_1 = require("@emotion/react");
|
|
17
|
+
/**
|
|
18
|
+
* Overrideable Image component.
|
|
19
|
+
* - If `Component` is provided, renders as `Component`
|
|
20
|
+
* - else, if `theme.custom.ImageAdapter` is provided, renders as `theme.custom.ImageAdapter`
|
|
21
|
+
* - else, renders as `img` tag
|
|
22
|
+
*/
|
|
23
|
+
const ImageAdapter = React.forwardRef(function ImageAdapter(_a, ref) {
|
|
24
|
+
var _b;
|
|
25
|
+
var { Component } = _a, props = __rest(_a, ["Component"]);
|
|
26
|
+
const theme = (0, react_1.useTheme)();
|
|
27
|
+
const ImgComponent = (_b = Component !== null && Component !== void 0 ? Component : theme.custom.ImgAdapter) !== null && _b !== void 0 ? _b : "img";
|
|
28
|
+
return React.createElement(ImgComponent, Object.assign({ ref: ref }, props));
|
|
29
|
+
});
|
|
30
|
+
exports.ImageAdapter = ImageAdapter;
|
|
@@ -20,4 +20,4 @@ type LinkAdapterProps = React.ComponentProps<"a"> & {
|
|
|
20
20
|
*/
|
|
21
21
|
declare const LinkAdapter: React.ForwardRefExoticComponent<Omit<LinkAdapterProps, "ref"> & React.RefAttributes<HTMLAnchorElement>>;
|
|
22
22
|
export { LinkAdapter };
|
|
23
|
-
export type { LinkAdapterPropsOverrides };
|
|
23
|
+
export type { LinkAdapterPropsOverrides, LinkAdapterProps };
|
|
@@ -6,9 +6,7 @@ import type { ThemeOptions, Theme } from "@mui/material/styles";
|
|
|
6
6
|
* See [ThemeProvider Docs](https://mitodl.github.io/smoot-design/?path=/docs/smoot-design-themeprovider--docs#further-customized-theme-with-createtheme)
|
|
7
7
|
* for more.
|
|
8
8
|
*/
|
|
9
|
-
declare const createTheme: (options?:
|
|
10
|
-
custom: Partial<ThemeOptions["custom"]>;
|
|
11
|
-
}) => Theme;
|
|
9
|
+
declare const createTheme: (options?: ThemeOptions) => Theme;
|
|
12
10
|
type ThemeProviderProps = {
|
|
13
11
|
children?: React.ReactNode;
|
|
14
12
|
theme?: Theme;
|
|
@@ -7,6 +7,7 @@ const typography = require("./typography");
|
|
|
7
7
|
const buttons = require("./buttons");
|
|
8
8
|
const chips = require("./chips");
|
|
9
9
|
const colors_1 = require("./colors");
|
|
10
|
+
const deepmerge_1 = require("@mui/utils/deepmerge");
|
|
10
11
|
const custom = {
|
|
11
12
|
colors: colors_1.colors,
|
|
12
13
|
dimensions: {
|
|
@@ -48,13 +49,6 @@ const defaultThemeOptions = {
|
|
|
48
49
|
components: {
|
|
49
50
|
MuiButtonBase: buttons.buttonBaseComponent,
|
|
50
51
|
MuiTypography: typography.component,
|
|
51
|
-
MuiTabPanel: {
|
|
52
|
-
styleOverrides: {
|
|
53
|
-
root: {
|
|
54
|
-
padding: "0px",
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
52
|
MuiMenu: {
|
|
59
53
|
styleOverrides: { paper: { borderRadius: "4px" } },
|
|
60
54
|
},
|
|
@@ -76,7 +70,9 @@ const defaultThemeOptions = {
|
|
|
76
70
|
* See [ThemeProvider Docs](https://mitodl.github.io/smoot-design/?path=/docs/smoot-design-themeprovider--docs#further-customized-theme-with-createtheme)
|
|
77
71
|
* for more.
|
|
78
72
|
*/
|
|
79
|
-
const createTheme = (options) =>
|
|
73
|
+
const createTheme = (options) => {
|
|
74
|
+
return (0, styles_1.createTheme)((0, deepmerge_1.default)(defaultThemeOptions, options));
|
|
75
|
+
};
|
|
80
76
|
exports.createTheme = createTheme;
|
|
81
77
|
const defaultTheme = createTheme();
|
|
82
78
|
/**
|
|
@@ -22,7 +22,6 @@ type Story = StoryObj<typeof ThemeProvider>;
|
|
|
22
22
|
* </ThemeProvider>
|
|
23
23
|
* ```
|
|
24
24
|
*
|
|
25
|
-
*
|
|
26
25
|
* ### Custom Link Adapter
|
|
27
26
|
* One particularly notable property is `theme.custom.LinkAdapter`. Some `smoot-design`
|
|
28
27
|
* components render links. These links are native anchor tags by default. In
|
|
@@ -53,6 +52,12 @@ type Story = StoryObj<typeof ThemeProvider>;
|
|
|
53
52
|
* }
|
|
54
53
|
* }
|
|
55
54
|
* ```
|
|
55
|
+
*
|
|
56
|
+
* ### ImageAdapter
|
|
57
|
+
* Similarly, `theme.custom.ImageAdapter` can be used to customize the image
|
|
58
|
+
* component used by `smoot-design`. By default, `ImageAdapter` uses a simple `img`
|
|
59
|
+
* tag. Interface `ImageAdapterPropsOverrides` is similarly available for
|
|
60
|
+
* augmentation.
|
|
56
61
|
*/
|
|
57
62
|
export declare const LinkAdapterOverride: Story;
|
|
58
63
|
export default meta;
|
|
@@ -53,7 +53,6 @@ const meta = {
|
|
|
53
53
|
* </ThemeProvider>
|
|
54
54
|
* ```
|
|
55
55
|
*
|
|
56
|
-
*
|
|
57
56
|
* ### Custom Link Adapter
|
|
58
57
|
* One particularly notable property is `theme.custom.LinkAdapter`. Some `smoot-design`
|
|
59
58
|
* components render links. These links are native anchor tags by default. In
|
|
@@ -84,6 +83,12 @@ const meta = {
|
|
|
84
83
|
* }
|
|
85
84
|
* }
|
|
86
85
|
* ```
|
|
86
|
+
*
|
|
87
|
+
* ### ImageAdapter
|
|
88
|
+
* Similarly, `theme.custom.ImageAdapter` can be used to customize the image
|
|
89
|
+
* component used by `smoot-design`. By default, `ImageAdapter` uses a simple `img`
|
|
90
|
+
* tag. Interface `ImageAdapterPropsOverrides` is similarly available for
|
|
91
|
+
* augmentation.
|
|
87
92
|
*/
|
|
88
93
|
exports.LinkAdapterOverride = {
|
|
89
94
|
args: {
|
|
@@ -14,7 +14,6 @@ const BREAKPOINT_VALUES = {
|
|
|
14
14
|
exports.BREAKPOINT_VALUES = BREAKPOINT_VALUES;
|
|
15
15
|
const { breakpoints } = (0, styles_1.createTheme)({
|
|
16
16
|
breakpoints: BREAKPOINT_VALUES,
|
|
17
|
-
// @ts-expect-error only using breakpoints
|
|
18
17
|
custom: {},
|
|
19
18
|
});
|
|
20
19
|
exports.breakpoints = breakpoints;
|
|
@@ -168,7 +168,6 @@ const component = {
|
|
|
168
168
|
exports.component = component;
|
|
169
169
|
const { typography } = (0, styles_1.createTheme)({
|
|
170
170
|
typography: globalSettings,
|
|
171
|
-
// @ts-expect-error: we only care about typography from this theme
|
|
172
171
|
custom: {},
|
|
173
172
|
});
|
|
174
173
|
exports.typography = typography;
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default as styled } from "@emotion/styled";
|
|
2
2
|
export { css, Global } from "@emotion/react";
|
|
3
|
-
export { AppRouterCacheProvider as NextJsAppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
|
|
4
3
|
export { ThemeProvider, createTheme, } from "./components/ThemeProvider/ThemeProvider";
|
|
5
4
|
export { Button, ButtonLink } from "./components/Button/Button";
|
|
6
5
|
export type { ButtonProps, ButtonLinkProps } from "./components/Button/Button";
|
package/dist/cjs/index.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
"use client";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.VisuallyHidden = exports.SrAnnouncer = exports.TextField = exports.Input = exports.ActionButtonLink = exports.ActionButton = exports.ButtonLink = exports.Button = exports.createTheme = exports.ThemeProvider = exports.
|
|
4
|
+
exports.VisuallyHidden = exports.SrAnnouncer = exports.TextField = exports.Input = exports.ActionButtonLink = exports.ActionButton = exports.ButtonLink = exports.Button = exports.createTheme = exports.ThemeProvider = exports.Global = exports.css = exports.styled = void 0;
|
|
5
5
|
var styled_1 = require("@emotion/styled");
|
|
6
6
|
Object.defineProperty(exports, "styled", { enumerable: true, get: function () { return styled_1.default; } });
|
|
7
7
|
var react_1 = require("@emotion/react");
|
|
8
8
|
Object.defineProperty(exports, "css", { enumerable: true, get: function () { return react_1.css; } });
|
|
9
9
|
Object.defineProperty(exports, "Global", { enumerable: true, get: function () { return react_1.Global; } });
|
|
10
|
-
var v15_appRouter_1 = require("@mui/material-nextjs/v15-appRouter");
|
|
11
|
-
Object.defineProperty(exports, "NextJsAppRouterCacheProvider", { enumerable: true, get: function () { return v15_appRouter_1.AppRouterCacheProvider; } });
|
|
12
10
|
var ThemeProvider_1 = require("./components/ThemeProvider/ThemeProvider");
|
|
13
11
|
Object.defineProperty(exports, "ThemeProvider", { enumerable: true, get: function () { return ThemeProvider_1.ThemeProvider; } });
|
|
14
12
|
Object.defineProperty(exports, "createTheme", { enumerable: true, get: function () { return ThemeProvider_1.createTheme; } });
|
|
@@ -1,40 +1,93 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
1
12
|
import * as React from "react";
|
|
2
13
|
import styled from "@emotion/styled";
|
|
3
14
|
import Skeleton from "@mui/material/Skeleton";
|
|
4
15
|
import { Input } from "../Input/Input";
|
|
5
16
|
import { ActionButton } from "../Button/ActionButton";
|
|
6
|
-
import { RiSendPlaneFill } from "@remixicon/react";
|
|
17
|
+
import { RiCloseLine, RiRobot2Line, RiSendPlaneFill } from "@remixicon/react";
|
|
7
18
|
import { useAiChat } from "./utils";
|
|
8
19
|
import Markdown from "react-markdown";
|
|
9
20
|
import { ScrollSnap } from "../ScrollSnap/ScrollSnap";
|
|
10
21
|
import classNames from "classnames";
|
|
11
22
|
import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer";
|
|
12
|
-
|
|
23
|
+
import mascot from "../../../static/images/mit_mascot_tim.png";
|
|
24
|
+
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden";
|
|
25
|
+
import { ImageAdapter } from "../ImageAdapter/ImageAdapter";
|
|
26
|
+
import Typography from "@mui/material/Typography";
|
|
27
|
+
const classes = {
|
|
28
|
+
root: "MitAiChat--root",
|
|
29
|
+
conversationStarter: "MitAiChat--conversationStarter",
|
|
30
|
+
messagesContainer: "MitAiChat--messagesContainer",
|
|
31
|
+
messageRow: "MitAiChat--messageRow",
|
|
32
|
+
messageRowUser: "MitAiChat--messageRowUser",
|
|
33
|
+
messageRowAssistant: "MitAiChat--messageRowAssistant",
|
|
34
|
+
message: "MitAiChat--message",
|
|
35
|
+
avatar: "MitAiChat--avatar",
|
|
36
|
+
input: "MitAiChat--input",
|
|
37
|
+
};
|
|
38
|
+
const ChatContainer = styled.div({
|
|
13
39
|
width: "100%",
|
|
14
40
|
height: "100%",
|
|
15
|
-
border: `1px solid ${theme.custom.colors.silverGrayLight}`,
|
|
16
|
-
backgroundColor: theme.custom.colors.lightGray1,
|
|
17
41
|
display: "flex",
|
|
18
42
|
flexDirection: "column",
|
|
19
|
-
})
|
|
20
|
-
const MessagesContainer = styled(ScrollSnap)({
|
|
43
|
+
});
|
|
44
|
+
const MessagesContainer = styled(ScrollSnap)(({ theme }) => ({
|
|
21
45
|
display: "flex",
|
|
22
46
|
flexDirection: "column",
|
|
23
47
|
flex: 1,
|
|
24
48
|
padding: "24px",
|
|
25
|
-
paddingBottom: "
|
|
49
|
+
paddingBottom: "12px",
|
|
26
50
|
overflow: "auto",
|
|
51
|
+
gap: "24px",
|
|
52
|
+
backgroundColor: theme.custom.colors.lightGray1,
|
|
53
|
+
borderColor: theme.custom.colors.silverGrayLight,
|
|
54
|
+
borderStyle: "solid",
|
|
55
|
+
borderWidth: "0 1px",
|
|
56
|
+
}));
|
|
57
|
+
const MessageRow = styled.div({
|
|
58
|
+
display: "flex",
|
|
59
|
+
width: "100%",
|
|
60
|
+
gap: "10px",
|
|
61
|
+
[`&.${classes.messageRowUser}`]: {
|
|
62
|
+
flexDirection: "row-reverse",
|
|
63
|
+
},
|
|
64
|
+
"> *": {
|
|
65
|
+
minWidth: 0,
|
|
66
|
+
},
|
|
67
|
+
position: "relative",
|
|
27
68
|
});
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
+
},
|
|
79
|
+
width: "32px",
|
|
80
|
+
height: "32px",
|
|
81
|
+
position: "absolute",
|
|
82
|
+
top: "-16px",
|
|
83
|
+
[`.${classes.messageRowAssistant} &`]: {
|
|
84
|
+
left: "-10px",
|
|
85
|
+
},
|
|
86
|
+
[`.${classes.messageRowUser} &`]: {
|
|
87
|
+
right: "16px",
|
|
34
88
|
},
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white, borderRadius: "24px", padding: "4px 16px" }, theme.typography.body2), { "p:first-of-type": {
|
|
89
|
+
}));
|
|
90
|
+
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": {
|
|
38
91
|
marginTop: 0,
|
|
39
92
|
}, "p:last-of-type": {
|
|
40
93
|
marginBottom: 0,
|
|
@@ -44,27 +97,32 @@ const Message = styled.div(({ theme }) => (Object.assign(Object.assign({ border:
|
|
|
44
97
|
}, "a:hover": {
|
|
45
98
|
color: theme.custom.colors.red,
|
|
46
99
|
textDecoration: "underline",
|
|
100
|
+
}, borderRadius: "12px", [`.${classes.messageRowAssistant} &`]: {
|
|
101
|
+
borderRadius: "0 12px 12px 12px",
|
|
102
|
+
}, [`.${classes.messageRowUser} &`]: {
|
|
103
|
+
borderRadius: "12px 0 12px 12px",
|
|
47
104
|
} })));
|
|
48
105
|
const StarterContainer = styled.div({
|
|
49
106
|
alignSelf: "flex-end",
|
|
107
|
+
alignItems: "end",
|
|
50
108
|
display: "flex",
|
|
51
109
|
flexDirection: "column",
|
|
52
|
-
gap: "
|
|
110
|
+
gap: "12px",
|
|
53
111
|
});
|
|
54
|
-
const Starter = styled.button(({ theme }) => (Object.assign(Object.assign({ border: `1px solid ${theme.custom.colors.silverGrayLight}`, backgroundColor: theme.custom.colors.white,
|
|
112
|
+
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": {
|
|
55
113
|
backgroundColor: theme.custom.colors.lightGray1,
|
|
56
|
-
} })));
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
backgroundColor: theme.custom.colors.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
}, borderRadius: "100vh" })));
|
|
115
|
+
const InputStyled = styled(Input)({
|
|
116
|
+
borderRadius: "0 0 8px 8px",
|
|
117
|
+
});
|
|
118
|
+
const ActionButtonStyled = styled(ActionButton)(({ theme }) => ({
|
|
119
|
+
backgroundColor: theme.custom.colors.red,
|
|
120
|
+
flexShrink: 0,
|
|
121
|
+
marginRight: "24px",
|
|
122
|
+
marginLeft: "12px",
|
|
123
|
+
"&:hover:not(:disabled)": {
|
|
124
|
+
backgroundColor: theme.custom.colors.mitRed,
|
|
125
|
+
},
|
|
68
126
|
}));
|
|
69
127
|
const DotsContainer = styled.span(({ theme }) => ({
|
|
70
128
|
display: "inline-flex",
|
|
@@ -79,18 +137,37 @@ const Dots = () => {
|
|
|
79
137
|
React.createElement(Skeleton, { variant: "circular", width: "8px", height: "8px" }),
|
|
80
138
|
React.createElement(Skeleton, { variant: "circular", width: "8px", height: "8px" })));
|
|
81
139
|
};
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
140
|
+
const CloseButton = styled(ActionButton)(({ theme }) => ({
|
|
141
|
+
color: "inherit",
|
|
142
|
+
backgroundColor: theme.custom.colors.red,
|
|
143
|
+
"&:hover:not(:disabled)": {
|
|
144
|
+
backgroundColor: theme.custom.colors.mitRed,
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
const RobotIcon = styled(RiRobot2Line)({
|
|
148
|
+
width: "40px",
|
|
149
|
+
height: "40px",
|
|
150
|
+
});
|
|
151
|
+
const ChatTitle = styled(({ title, onClose, className }) => {
|
|
152
|
+
return (React.createElement("div", { className: className },
|
|
153
|
+
React.createElement(RobotIcon, null),
|
|
154
|
+
React.createElement(Typography, { flex: 1, variant: "h5" }, title),
|
|
155
|
+
onClose ? (React.createElement(CloseButton, { variant: "text", onClick: onClose, "aria-label": "Close chat" },
|
|
156
|
+
React.createElement(RiCloseLine, null))) : null));
|
|
157
|
+
})(({ theme }) => ({
|
|
158
|
+
backgroundColor: theme.custom.colors.red,
|
|
159
|
+
display: "flex",
|
|
160
|
+
alignItems: "center",
|
|
161
|
+
justifyContent: "space-between",
|
|
162
|
+
padding: "12px 24px",
|
|
163
|
+
gap: "16px",
|
|
164
|
+
color: theme.custom.colors.white,
|
|
165
|
+
borderRadius: "8px 8px 0 0",
|
|
166
|
+
}));
|
|
167
|
+
const AiChatInternal = function AiChat(_a) {
|
|
168
|
+
var _b;
|
|
169
|
+
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
|
|
170
|
+
;
|
|
94
171
|
const messagesRef = React.useRef(null);
|
|
95
172
|
const initialMessages = React.useMemo(() => {
|
|
96
173
|
const prefix = Math.random().toString().slice(2);
|
|
@@ -98,6 +175,7 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
98
175
|
}, [initMsgs]);
|
|
99
176
|
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, } = useAiChat(requestOpts, {
|
|
100
177
|
initialMessages: initialMessages,
|
|
178
|
+
id: chatId,
|
|
101
179
|
});
|
|
102
180
|
const messages = React.useMemo(() => {
|
|
103
181
|
const initial = initialMessages.map((m) => m.id);
|
|
@@ -109,7 +187,8 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
109
187
|
return m;
|
|
110
188
|
});
|
|
111
189
|
}, [parseContent, unparsed, initialMessages]);
|
|
112
|
-
const
|
|
190
|
+
const showStarters = messages.length === initialMessages.length;
|
|
191
|
+
const waiting = !showStarters && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
113
192
|
const scrollToBottom = () => {
|
|
114
193
|
var _a;
|
|
115
194
|
(_a = messagesRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({
|
|
@@ -118,30 +197,43 @@ const AiChat = function AiChat({ className, conversationStarters, requestOpts, i
|
|
|
118
197
|
});
|
|
119
198
|
};
|
|
120
199
|
const lastMsg = messages[messages.length - 1];
|
|
121
|
-
return (React.createElement(ChatContainer, { className: classNames(className, classes.root) },
|
|
200
|
+
return (React.createElement(ChatContainer, Object.assign({ className: classNames(className, classes.root) }, others),
|
|
201
|
+
React.createElement(ChatTitle, { title: title, onClose: onClose }),
|
|
122
202
|
React.createElement(MessagesContainer, { className: classes.messagesContainer, ref: messagesRef },
|
|
123
|
-
messages.map((m) => (React.createElement(MessageRow, { key: m.id,
|
|
124
|
-
|
|
203
|
+
messages.map((m) => (React.createElement(MessageRow, { key: m.id, "data-chat-role": m.role, className: classNames(classes.messageRow, {
|
|
204
|
+
[classes.messageRowUser]: m.role === "user",
|
|
205
|
+
[classes.messageRowAssistant]: m.role === "assistant",
|
|
206
|
+
}) },
|
|
207
|
+
m.role === "assistant" ? (React.createElement(Avatar, { className: classes.avatar },
|
|
208
|
+
React.createElement(ImageAdapter, { src: mascot, alt: "", Component: ImgComponent }))) : null,
|
|
125
209
|
React.createElement(Message, { className: classes.message },
|
|
126
|
-
React.createElement(
|
|
210
|
+
React.createElement(VisuallyHidden, null, m.role === "user" ? "You said: " : "Assistant said: "),
|
|
211
|
+
React.createElement(Markdown, { skipHtml: true }, m.content))))),
|
|
127
212
|
showStarters ? (React.createElement(StarterContainer, null, conversationStarters === null || conversationStarters === void 0 ? void 0 : conversationStarters.map((m) => (React.createElement(Starter, { className: classes.conversationStarter, key: m.content, onClick: () => {
|
|
128
|
-
setShowStarters(false);
|
|
129
213
|
scrollToBottom();
|
|
130
214
|
append({ role: "user", content: m.content });
|
|
131
215
|
} }, m.content))))) : null,
|
|
132
|
-
waiting ? (React.createElement(MessageRow, { key: "loading" },
|
|
133
|
-
React.createElement(Avatar, { className: classes.avatar }
|
|
216
|
+
waiting ? (React.createElement(MessageRow, { className: classNames(classes.messageRow, classes.messageRowAssistant), key: "loading" },
|
|
217
|
+
React.createElement(Avatar, { className: classes.avatar },
|
|
218
|
+
React.createElement(ImageAdapter, { src: mascot, alt: "", Component: ImgComponent })),
|
|
134
219
|
React.createElement(Message, null,
|
|
135
220
|
React.createElement(Dots, null)))) : null),
|
|
136
|
-
React.createElement(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
React.createElement(Input, { className: classes.input, placeholder: "Type a message...", name: "message", sx: { flex: 1 }, value: input, onChange: handleInputChange }),
|
|
143
|
-
React.createElement(ActionButton, { "aria-label": "Send", type: "submit", disabled: isLoading || !input },
|
|
144
|
-
React.createElement(RiSendPlaneFill, null)))),
|
|
221
|
+
React.createElement("form", { onSubmit: (e) => {
|
|
222
|
+
scrollToBottom();
|
|
223
|
+
handleSubmit(e);
|
|
224
|
+
} },
|
|
225
|
+
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 },
|
|
226
|
+
React.createElement(RiSendPlaneFill, null)) })),
|
|
145
227
|
React.createElement(SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: lastMsg.role === "assistant" ? lastMsg.content : "" })));
|
|
146
228
|
};
|
|
229
|
+
const AiChat = (props) => (
|
|
230
|
+
/**
|
|
231
|
+
* Changing the `useChat` chatId seems to persist some state between
|
|
232
|
+
* hook calls. This can cause strange effects like loading API responses
|
|
233
|
+
* for previous chatId into new chatId.
|
|
234
|
+
*
|
|
235
|
+
* To avoid this, let's chnge the key, this will force React to make a new component
|
|
236
|
+
* not sharing any of the old state.
|
|
237
|
+
*/
|
|
238
|
+
React.createElement(AiChatInternal, Object.assign({ key: props.chatId }, props)));
|
|
147
239
|
export { AiChat };
|
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { AiChat } from "./AiChat";
|
|
3
3
|
import { mockJson, mockStreaming } from "./story-utils";
|
|
4
4
|
import styled from "@emotion/styled";
|
|
5
|
+
import { fn } from "@storybook/test";
|
|
5
6
|
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
6
7
|
const TEST_API_JSON = "http://localhost:4567/json";
|
|
7
8
|
const INITIAL_MESSAGES = [
|
|
@@ -17,10 +18,10 @@ const STARTERS = [
|
|
|
17
18
|
];
|
|
18
19
|
const Container = styled.div({
|
|
19
20
|
width: "100%",
|
|
20
|
-
height: "
|
|
21
|
+
height: "500px",
|
|
21
22
|
});
|
|
22
23
|
const meta = {
|
|
23
|
-
title: "smoot-design/AiChat",
|
|
24
|
+
title: "smoot-design/ai/AiChat",
|
|
24
25
|
component: AiChat,
|
|
25
26
|
render: (args) => React.createElement(AiChat, Object.assign({}, args)),
|
|
26
27
|
decorators: (Story) => {
|
|
@@ -31,6 +32,8 @@ const meta = {
|
|
|
31
32
|
initialMessages: INITIAL_MESSAGES,
|
|
32
33
|
requestOpts: { apiUrl: TEST_API_STREAMING },
|
|
33
34
|
conversationStarters: STARTERS,
|
|
35
|
+
title: "Chat with AI",
|
|
36
|
+
onClose: fn(),
|
|
34
37
|
},
|
|
35
38
|
argTypes: {
|
|
36
39
|
conversationStarters: {
|
|
@@ -32,6 +32,10 @@ jest.mock("react-markdown", () => {
|
|
|
32
32
|
default: ({ children }) => React.createElement("div", null, children),
|
|
33
33
|
};
|
|
34
34
|
});
|
|
35
|
+
const msg = {
|
|
36
|
+
ai: (text) => `Assistant said: ${text}`,
|
|
37
|
+
you: (text) => `You said: ${text}`,
|
|
38
|
+
};
|
|
35
39
|
const getMessages = () => {
|
|
36
40
|
return Array.from(document.querySelectorAll(".MitAiChat--message"));
|
|
37
41
|
};
|
|
@@ -54,8 +58,11 @@ describe("AiChat", () => {
|
|
|
54
58
|
{ content: faker.lorem.sentence() },
|
|
55
59
|
{ content: faker.lorem.sentence() },
|
|
56
60
|
];
|
|
57
|
-
render(React.createElement(AiChat, Object.assign({ initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, props)), { wrapper: ThemeProvider });
|
|
58
|
-
|
|
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 });
|
|
62
|
+
const rerender = (newProps) => {
|
|
63
|
+
view.rerender(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, newProps)));
|
|
64
|
+
};
|
|
65
|
+
return { initialMessages, conversationStarters, rerender };
|
|
59
66
|
};
|
|
60
67
|
test("Clicking conversation starters and sending chats", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
68
|
const { initialMessages, conversationStarters } = setup();
|
|
@@ -83,6 +90,21 @@ describe("AiChat", () => {
|
|
|
83
90
|
expect(afterSending[3]).toHaveTextContent("User message");
|
|
84
91
|
expect(afterSending[4]).toHaveTextContent("AI Response 1");
|
|
85
92
|
}));
|
|
93
|
+
test("Messages persist if chat has same chatId", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
94
|
+
const { rerender } = setup({ chatId: "test-123" });
|
|
95
|
+
const starterEls = getConversationStarters();
|
|
96
|
+
const chosen = faker.helpers.arrayElement([0, 1]);
|
|
97
|
+
yield user.click(starterEls[chosen]);
|
|
98
|
+
yield whenCount(getMessages, 3);
|
|
99
|
+
// New chat ... starters should be shown
|
|
100
|
+
rerender({ chatId: "test-345" });
|
|
101
|
+
expect(getConversationStarters().length).toBeGreaterThan(0);
|
|
102
|
+
yield whenCount(getMessages, 1);
|
|
103
|
+
// existing chat ... starters should not be shown, messages should be restored
|
|
104
|
+
rerender({ chatId: "test-123" });
|
|
105
|
+
expect(getConversationStarters().length).toBe(0);
|
|
106
|
+
yield whenCount(getMessages, 3);
|
|
107
|
+
}));
|
|
86
108
|
test("transformBody is called before sending requests", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
87
109
|
const fakeBody = { message: faker.lorem.sentence() };
|
|
88
110
|
const apiUrl = faker.internet.url();
|
|
@@ -118,11 +140,21 @@ describe("AiChat", () => {
|
|
|
118
140
|
yield whenCount(getMessages, initialMessages.length + 4);
|
|
119
141
|
const messagesTexts = getMessages().map((el) => el.textContent);
|
|
120
142
|
expect(messagesTexts).toEqual([
|
|
121
|
-
initialMessages[0].content,
|
|
122
|
-
conversationStarters[0].content,
|
|
123
|
-
"Parsed: AI Response 0",
|
|
124
|
-
"User message",
|
|
125
|
-
"Parsed: AI Response 1",
|
|
143
|
+
msg.ai(initialMessages[0].content),
|
|
144
|
+
msg.you(conversationStarters[0].content),
|
|
145
|
+
msg.ai("Parsed: AI Response 0"),
|
|
146
|
+
msg.you("User message"),
|
|
147
|
+
msg.ai("Parsed: AI Response 1"),
|
|
126
148
|
]);
|
|
127
149
|
}));
|
|
150
|
+
test("Passes extra attributes to root", () => {
|
|
151
|
+
const fakeBody = { message: faker.lorem.sentence() };
|
|
152
|
+
const apiUrl = faker.internet.url();
|
|
153
|
+
const transformBody = jest.fn(() => fakeBody);
|
|
154
|
+
setup({
|
|
155
|
+
requestOpts: { apiUrl, transformBody },
|
|
156
|
+
parseContent: jest.fn((content) => `Parsed: ${content}`),
|
|
157
|
+
});
|
|
158
|
+
expect(screen.getByTestId("ai-chat")).toBeInTheDocument();
|
|
159
|
+
});
|
|
128
160
|
});
|
|
@@ -13,6 +13,7 @@ const SAMPLE_RESPONSES = [
|
|
|
13
13
|
1. **[Machine Learning, Modeling, and Simulation Principles](https://xpro.mit.edu/courses/course-v1:xPRO+MLx1/)**: Offered by MIT xPRO, this course is part of the program "Machine Learning, Modeling, and Simulation: Engineering Problem-Solving in the Age of AI." It focuses on the principles of machine learning and how they can be applied to solve engineering problems, which is highly relevant for business applications of AI.
|
|
14
14
|
|
|
15
15
|
This course is not free, but it provides a certification upon completion, which can be valuable for professionals looking to apply AI in business contexts. It covers essential concepts that can help you understand how AI can be leveraged to improve business processes and decision-making.
|
|
16
|
+
<!-- Comment! -->
|
|
16
17
|
`,
|
|
17
18
|
`
|
|
18
19
|
To understand global warming, I recommend the following resources from MIT:
|
|
@@ -22,6 +23,7 @@ To understand global warming, I recommend the following resources from MIT:
|
|
|
22
23
|
2. **[Global Warming Science](https://openlearninglibrary.mit.edu/courses/course-v1:MITx+12.340x+1T2020/about)**: Another offering of the same course by MITx, available through the Open Learning Library. It provides the same in-depth exploration of the earth's climate system.
|
|
23
24
|
|
|
24
25
|
These courses are free and provide a solid foundation in understanding the scientific aspects of global warming. They are suitable for anyone interested in the topic, regardless of prior knowledge.
|
|
26
|
+
<!-- Comment! -->
|
|
25
27
|
`,
|
|
26
28
|
`
|
|
27
29
|
Here are some courses on linear algebra that you can explore:
|
|
@@ -33,6 +35,7 @@ Here are some courses on linear algebra that you can explore:
|
|
|
33
35
|
3. **[Quantum Information Science I, Part 1 (MITx)](https://openlearninglibrary.mit.edu/courses/course-v1:MITx+8.370.1x+1T2018/about)**: While primarily focused on quantum information science, this course requires some knowledge of linear algebra and is suitable for those interested in quantum mechanics. It is free and available through MITx.
|
|
34
36
|
|
|
35
37
|
These courses provide a comprehensive introduction to linear algebra and its applications across various fields.
|
|
38
|
+
<!-- Comment! -->
|
|
36
39
|
`,
|
|
37
40
|
];
|
|
38
41
|
const rand = (min, max) => {
|
|
@@ -58,7 +61,7 @@ const mockStreaming = function mockApi() {
|
|
|
58
61
|
}, []);
|
|
59
62
|
const num = chunks.length;
|
|
60
63
|
let i = 0;
|
|
61
|
-
yield new Promise((resolve) => setTimeout(resolve,
|
|
64
|
+
yield new Promise((resolve) => setTimeout(resolve, 800));
|
|
62
65
|
const body = new ReadableStream({
|
|
63
66
|
start(controller) {
|
|
64
67
|
timerId = setInterval(() => {
|
|
@@ -69,7 +72,7 @@ const mockStreaming = function mockApi() {
|
|
|
69
72
|
controller.close();
|
|
70
73
|
clearInterval(timerId);
|
|
71
74
|
}
|
|
72
|
-
},
|
|
75
|
+
}, 100);
|
|
73
76
|
},
|
|
74
77
|
cancel() {
|
|
75
78
|
if (timerId) {
|
|
@@ -86,7 +89,7 @@ const mockStreaming = function mockApi() {
|
|
|
86
89
|
};
|
|
87
90
|
const mockJson = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
88
91
|
const message = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)];
|
|
89
|
-
yield new Promise((res) => setTimeout(res,
|
|
92
|
+
yield new Promise((res) => setTimeout(res, 1000));
|
|
90
93
|
return Promise.resolve(new Response(JSON.stringify({ message }), {
|
|
91
94
|
headers: {
|
|
92
95
|
"Content-Type": "application/json",
|