@mitodl/smoot-design 3.4.0 → 3.4.1
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/dist/bundles/remoteAiChatDrawer.es.js +13737 -12491
- package/dist/bundles/remoteAiChatDrawer.umd.js +114 -41
- package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +6 -0
- package/dist/cjs/components/AiChat/AiChat.js +5 -3
- package/dist/cjs/components/AiChat/AiChat.stories.js +4 -13
- package/dist/cjs/components/AiChat/AiChat.test.js +25 -17
- package/dist/cjs/components/AiChat/test-utils/api.d.ts +2 -0
- package/dist/{esm/components/AiChat/story-utils.js → cjs/components/AiChat/test-utils/api.js} +64 -51
- package/dist/cjs/components/Alert/Alert.d.ts +15 -0
- package/dist/cjs/components/Alert/Alert.js +62 -0
- package/dist/cjs/components/Alert/Alert.stories.d.ts +8 -0
- package/dist/cjs/components/Alert/Alert.stories.js +53 -0
- package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +6 -0
- package/dist/esm/components/AiChat/AiChat.js +5 -3
- package/dist/esm/components/AiChat/AiChat.stories.js +4 -13
- package/dist/esm/components/AiChat/AiChat.test.js +25 -17
- package/dist/esm/components/AiChat/test-utils/api.d.ts +2 -0
- package/dist/{cjs/components/AiChat/story-utils.js → esm/components/AiChat/test-utils/api.js} +61 -55
- package/dist/esm/components/Alert/Alert.d.ts +15 -0
- package/dist/esm/components/Alert/Alert.js +59 -0
- package/dist/esm/components/Alert/Alert.stories.d.ts +8 -0
- package/dist/esm/components/Alert/Alert.stories.js +50 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +9 -1
- package/dist/cjs/components/AiChat/story-utils.d.ts +0 -3
- package/dist/esm/components/AiChat/story-utils.d.ts +0 -3
|
@@ -15,17 +15,18 @@ import { AiChat } from "./AiChat";
|
|
|
15
15
|
import { ThemeProvider } from "../ThemeProvider/ThemeProvider";
|
|
16
16
|
import * as React from "react";
|
|
17
17
|
import { faker } from "@faker-js/faker/locale/en";
|
|
18
|
+
import { http, HttpResponse } from "msw";
|
|
19
|
+
import { setupServer } from "msw/node";
|
|
18
20
|
const counter = jest.fn(); // use jest.fn as counter because it resets on each test
|
|
19
|
-
const
|
|
21
|
+
const API_URL = "http://localhost:4567/test";
|
|
22
|
+
const server = setupServer(http.post(API_URL, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
20
23
|
const count = counter.mock.calls.length;
|
|
21
24
|
counter();
|
|
22
|
-
return
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}));
|
|
28
|
-
window.fetch = mockFetch;
|
|
25
|
+
return HttpResponse.text(`AI Response ${count}`);
|
|
26
|
+
})));
|
|
27
|
+
beforeAll(() => server.listen());
|
|
28
|
+
afterEach(() => server.resetHandlers());
|
|
29
|
+
afterAll(() => server.close());
|
|
29
30
|
jest.mock("react-markdown", () => {
|
|
30
31
|
return {
|
|
31
32
|
__esModule: true,
|
|
@@ -58,9 +59,9 @@ describe("AiChat", () => {
|
|
|
58
59
|
{ content: faker.lorem.sentence() },
|
|
59
60
|
{ content: faker.lorem.sentence() },
|
|
60
61
|
];
|
|
61
|
-
const view = render(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl:
|
|
62
|
+
const view = render(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: API_URL }, placeholder: "Type a message..." }, props)), { wrapper: ThemeProvider });
|
|
62
63
|
const rerender = (newProps) => {
|
|
63
|
-
view.rerender(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl:
|
|
64
|
+
view.rerender(React.createElement(AiChat, Object.assign({ "data-testid": "ai-chat", initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: API_URL } }, newProps)));
|
|
64
65
|
};
|
|
65
66
|
return { initialMessages, conversationStarters, rerender };
|
|
66
67
|
};
|
|
@@ -106,11 +107,11 @@ describe("AiChat", () => {
|
|
|
106
107
|
yield whenCount(getMessages, 3);
|
|
107
108
|
}));
|
|
108
109
|
test("transformBody is called before sending requests", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
const mockFetch = jest.spyOn(window, "fetch");
|
|
109
111
|
const fakeBody = { message: faker.lorem.sentence() };
|
|
110
|
-
const apiUrl = faker.internet.url();
|
|
111
112
|
const transformBody = jest.fn(() => fakeBody);
|
|
112
113
|
const { initialMessages } = setup({
|
|
113
|
-
requestOpts: { apiUrl, transformBody },
|
|
114
|
+
requestOpts: { apiUrl: API_URL, transformBody },
|
|
114
115
|
});
|
|
115
116
|
yield user.click(screen.getByPlaceholderText("Type a message..."));
|
|
116
117
|
yield user.paste("User message");
|
|
@@ -120,16 +121,15 @@ describe("AiChat", () => {
|
|
|
120
121
|
expect.objectContaining({ content: "User message", role: "user" }),
|
|
121
122
|
]);
|
|
122
123
|
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
123
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
124
|
+
expect(mockFetch).toHaveBeenCalledWith(API_URL, expect.objectContaining({
|
|
124
125
|
body: JSON.stringify(fakeBody),
|
|
125
126
|
}));
|
|
126
127
|
}));
|
|
127
128
|
test("parseContent is called on the API-received message content", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
128
129
|
const fakeBody = { message: faker.lorem.sentence() };
|
|
129
|
-
const apiUrl = faker.internet.url();
|
|
130
130
|
const transformBody = jest.fn(() => fakeBody);
|
|
131
131
|
const { initialMessages, conversationStarters } = setup({
|
|
132
|
-
requestOpts: { apiUrl, transformBody },
|
|
132
|
+
requestOpts: { apiUrl: API_URL, transformBody },
|
|
133
133
|
parseContent: jest.fn((content) => `Parsed: ${content}`),
|
|
134
134
|
});
|
|
135
135
|
yield user.click(getConversationStarters()[0]);
|
|
@@ -149,12 +149,20 @@ describe("AiChat", () => {
|
|
|
149
149
|
}));
|
|
150
150
|
test("Passes extra attributes to root", () => {
|
|
151
151
|
const fakeBody = { message: faker.lorem.sentence() };
|
|
152
|
-
const apiUrl = faker.internet.url();
|
|
153
152
|
const transformBody = jest.fn(() => fakeBody);
|
|
154
153
|
setup({
|
|
155
|
-
requestOpts: { apiUrl, transformBody },
|
|
154
|
+
requestOpts: { apiUrl: API_URL, transformBody },
|
|
156
155
|
parseContent: jest.fn((content) => `Parsed: ${content}`),
|
|
157
156
|
});
|
|
158
157
|
expect(screen.getByTestId("ai-chat")).toBeInTheDocument();
|
|
159
158
|
});
|
|
159
|
+
test("If the API returns an error, an alert is shown", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
160
|
+
setup();
|
|
161
|
+
server.use(http.post(API_URL, () => __awaiter(void 0, void 0, void 0, function* () {
|
|
162
|
+
return new HttpResponse(null, { status: 500 });
|
|
163
|
+
})));
|
|
164
|
+
yield user.click(getConversationStarters()[0]);
|
|
165
|
+
const alert = yield screen.findByRole("alert");
|
|
166
|
+
expect(alert).toHaveTextContent("An unexpected error has occurred");
|
|
167
|
+
}));
|
|
160
168
|
});
|
package/dist/{cjs/components/AiChat/story-utils.js → esm/components/AiChat/test-utils/api.js}
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
2
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
3
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -8,8 +7,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
8
|
});
|
|
10
9
|
};
|
|
11
|
-
|
|
12
|
-
exports.mockJson = exports.mockStreaming = void 0;
|
|
10
|
+
import { http, HttpResponse, delay } from "msw";
|
|
13
11
|
const SAMPLE_RESPONSES = [
|
|
14
12
|
`For exploring AI applications in business, I recommend the following course from MIT:
|
|
15
13
|
|
|
@@ -45,59 +43,67 @@ const rand = (min, max) => {
|
|
|
45
43
|
// min and max included
|
|
46
44
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
47
45
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (i === num) {
|
|
75
|
-
controller.close();
|
|
76
|
-
clearInterval(timerId);
|
|
77
|
-
}
|
|
78
|
-
}, 100);
|
|
79
|
-
},
|
|
80
|
-
cancel() {
|
|
81
|
-
if (timerId) {
|
|
46
|
+
const getReadableStream = () => {
|
|
47
|
+
let timerId;
|
|
48
|
+
const response = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)];
|
|
49
|
+
const chunks = response.split(" ").reduce((acc, word) => {
|
|
50
|
+
const last = acc[acc.length - 1];
|
|
51
|
+
if (acc.length === 0) {
|
|
52
|
+
acc.push(word);
|
|
53
|
+
}
|
|
54
|
+
else if (Math.random() < 0.75) {
|
|
55
|
+
acc[acc.length - 1] = `${last} ${word}`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
acc.push(` ${word}`);
|
|
59
|
+
}
|
|
60
|
+
return acc;
|
|
61
|
+
}, []);
|
|
62
|
+
const num = chunks.length;
|
|
63
|
+
let i = 0;
|
|
64
|
+
return new ReadableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
timerId = setInterval(() => {
|
|
67
|
+
const msg = new TextEncoder().encode(chunks[i]);
|
|
68
|
+
controller.enqueue(msg);
|
|
69
|
+
i++;
|
|
70
|
+
if (i === num) {
|
|
71
|
+
controller.close();
|
|
82
72
|
clearInterval(timerId);
|
|
83
73
|
}
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
}
|
|
74
|
+
}, 100);
|
|
75
|
+
},
|
|
76
|
+
cancel() {
|
|
77
|
+
if (timerId) {
|
|
78
|
+
clearInterval(timerId);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
91
81
|
});
|
|
92
82
|
};
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
const handlers = [
|
|
84
|
+
http.post("http://localhost:4567/streaming", (_a) => __awaiter(void 0, [_a], void 0, function* ({ request }) {
|
|
85
|
+
yield delay(600);
|
|
86
|
+
const body = getReadableStream();
|
|
87
|
+
const requestBody = yield request.json();
|
|
88
|
+
if (Array.isArray(requestBody)) {
|
|
89
|
+
const last = requestBody[requestBody.length - 1];
|
|
90
|
+
const { content } = last;
|
|
91
|
+
if (content === "error") {
|
|
92
|
+
return new HttpResponse("Internal Server Error", {
|
|
93
|
+
status: 500,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return new HttpResponse(body, {
|
|
98
|
+
headers: {
|
|
99
|
+
"Content-Type": "text/plain",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
})),
|
|
103
|
+
http.post("http://localhost:4567/json", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
const message = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)];
|
|
105
|
+
yield delay(800);
|
|
106
|
+
return HttpResponse.json({ message });
|
|
107
|
+
})),
|
|
108
|
+
];
|
|
109
|
+
export { handlers };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AlertColor } from "@mui/material/Alert";
|
|
3
|
+
type AlertProps = {
|
|
4
|
+
visible?: boolean;
|
|
5
|
+
closable?: boolean;
|
|
6
|
+
className?: string;
|
|
7
|
+
severity?: AlertColor;
|
|
8
|
+
/**
|
|
9
|
+
* Alert Content
|
|
10
|
+
*/
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
};
|
|
13
|
+
declare const Alert: React.FC<AlertProps>;
|
|
14
|
+
export { Alert };
|
|
15
|
+
export type { AlertProps };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import styled from "@emotion/styled";
|
|
4
|
+
import { default as MuiAlert } from "@mui/material/Alert";
|
|
5
|
+
const getColor = (theme, severity) => {
|
|
6
|
+
return {
|
|
7
|
+
info: theme.custom.colors.blue,
|
|
8
|
+
success: theme.custom.colors.green,
|
|
9
|
+
warning: theme.custom.colors.orange,
|
|
10
|
+
error: theme.custom.colors.lightRed,
|
|
11
|
+
}[severity];
|
|
12
|
+
};
|
|
13
|
+
const AlertStyled = styled(MuiAlert)(({ theme, severity }) => ({
|
|
14
|
+
padding: "11px 16px",
|
|
15
|
+
borderRadius: 4,
|
|
16
|
+
borderWidth: 2,
|
|
17
|
+
borderStyle: "solid",
|
|
18
|
+
borderColor: getColor(theme, severity),
|
|
19
|
+
background: "#FFF",
|
|
20
|
+
".MuiAlert-message": Object.assign(Object.assign({}, theme.typography.body2), { color: theme.custom.colors.darkGray2, alignSelf: "center" }),
|
|
21
|
+
"> div": {
|
|
22
|
+
paddingTop: 0,
|
|
23
|
+
paddingBottom: 0,
|
|
24
|
+
},
|
|
25
|
+
".MuiAlert-icon": {
|
|
26
|
+
marginRight: 8,
|
|
27
|
+
svg: {
|
|
28
|
+
width: 16,
|
|
29
|
+
fill: getColor(theme, severity),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
button: {
|
|
33
|
+
padding: 0,
|
|
34
|
+
":hover": {
|
|
35
|
+
margin: 0,
|
|
36
|
+
background: "none",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
const Hidden = styled.span({ display: "none" });
|
|
41
|
+
const Alert = ({ visible = true, severity = "info", closable, children, className, }) => {
|
|
42
|
+
const [_visible, setVisible] = React.useState(visible);
|
|
43
|
+
const id = React.useId();
|
|
44
|
+
const onCloseClick = () => {
|
|
45
|
+
setVisible(false);
|
|
46
|
+
};
|
|
47
|
+
React.useEffect(() => {
|
|
48
|
+
setVisible(visible);
|
|
49
|
+
}, [visible]);
|
|
50
|
+
if (!_visible) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return (React.createElement(AlertStyled, { severity: severity, onClose: closable ? onCloseClick : undefined, role: "alert", "aria-describedby": id, className: className },
|
|
54
|
+
children,
|
|
55
|
+
React.createElement(Hidden, { id: id },
|
|
56
|
+
severity,
|
|
57
|
+
" message")));
|
|
58
|
+
};
|
|
59
|
+
export { Alert };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { Alert } from "./Alert";
|
|
3
|
+
declare const meta: Meta<typeof Alert>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof Alert>;
|
|
6
|
+
export declare const Basic: Story;
|
|
7
|
+
export declare const Closable: Story;
|
|
8
|
+
export declare const Variants: Story;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Alert } from "./Alert";
|
|
3
|
+
import Stack from "@mui/material/Stack";
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "smoot-design/Alert",
|
|
6
|
+
component: Alert,
|
|
7
|
+
};
|
|
8
|
+
export default meta;
|
|
9
|
+
export const Basic = {
|
|
10
|
+
args: {
|
|
11
|
+
severity: "info",
|
|
12
|
+
},
|
|
13
|
+
render: (args) => (React.createElement(Alert, Object.assign({}, args),
|
|
14
|
+
"Alert with severity \"",
|
|
15
|
+
args.severity,
|
|
16
|
+
"\"")),
|
|
17
|
+
};
|
|
18
|
+
export const Closable = {
|
|
19
|
+
args: {
|
|
20
|
+
severity: "warning",
|
|
21
|
+
closable: true,
|
|
22
|
+
},
|
|
23
|
+
render: (args) => (React.createElement(Alert, Object.assign({}, args),
|
|
24
|
+
"Closable alert with severity \"",
|
|
25
|
+
args.severity,
|
|
26
|
+
"\"")),
|
|
27
|
+
};
|
|
28
|
+
export const Variants = {
|
|
29
|
+
argTypes: {
|
|
30
|
+
severity: {
|
|
31
|
+
table: {
|
|
32
|
+
disable: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
closable: {
|
|
36
|
+
table: {
|
|
37
|
+
disable: true,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
render: (args) => (React.createElement(Stack, { direction: "column", gap: 2, sx: { my: 2 } },
|
|
42
|
+
React.createElement(Alert, Object.assign({}, args, { severity: "info" }), "Alert with severity \"info\""),
|
|
43
|
+
React.createElement(Alert, Object.assign({}, args, { closable: true, severity: "info" }), "Closable alert with severity \"info\""),
|
|
44
|
+
React.createElement(Alert, Object.assign({}, args, { severity: "success" }), "Alert with severity \"success\""),
|
|
45
|
+
React.createElement(Alert, Object.assign({}, args, { closable: true, severity: "success" }), "Closable alert with severity \"success\""),
|
|
46
|
+
React.createElement(Alert, Object.assign({}, args, { severity: "warning" }), "Alert with severity \"warning\""),
|
|
47
|
+
React.createElement(Alert, Object.assign({}, args, { closable: true, severity: "warning" }), "Closable alert with severity \"warning\""),
|
|
48
|
+
React.createElement(Alert, Object.assign({}, args, { severity: "error" }), "Alert with severity \"error\""),
|
|
49
|
+
React.createElement(Alert, Object.assign({}, args, { closable: true, severity: "error" }), "Closable alert with severity \"error\""))),
|
|
50
|
+
};
|