@mitodl/smoot-design 3.3.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 +27608 -0
- package/dist/bundles/remoteAiChatDrawer.umd.js +198 -0
- package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.d.ts +28 -0
- package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.js +44 -0
- package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.d.ts +6 -0
- package/dist/cjs/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +69 -0
- package/dist/cjs/bundles/remoteAiChatDrawer.d.ts +3 -0
- package/dist/cjs/bundles/remoteAiChatDrawer.js +15 -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.d.ts +28 -0
- package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.js +41 -0
- package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.d.ts +6 -0
- package/dist/esm/bundles/RemoteAiChatDrawer/RemoteAiChatDrawer.stories.js +66 -0
- package/dist/esm/bundles/remoteAiChatDrawer.d.ts +3 -0
- package/dist/esm/bundles/remoteAiChatDrawer.js +12 -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/bundles/aiChat.es.js +0 -24878
- package/dist/bundles/aiChat.umd.js +0 -125
- package/dist/cjs/bundles/aiChat.d.ts +0 -6
- package/dist/cjs/bundles/aiChat.js +0 -13
- package/dist/cjs/components/AiChat/story-utils.d.ts +0 -3
- package/dist/esm/bundles/aiChat.d.ts +0 -6
- package/dist/esm/bundles/aiChat.js +0 -10
- package/dist/esm/components/AiChat/story-utils.d.ts +0 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { AiChat } from "../../components/AiChat/AiChat";
|
|
3
|
+
import Drawer from "@mui/material/Drawer";
|
|
4
|
+
const AiChatDrawer = ({ messageOrigin, transformBody, className, }) => {
|
|
5
|
+
const [open, setOpen] = React.useState(false);
|
|
6
|
+
const [chatSettings, setChatSettings] = React.useState(null);
|
|
7
|
+
React.useEffect(() => {
|
|
8
|
+
const cb = (event) => {
|
|
9
|
+
if (event.origin !== messageOrigin) {
|
|
10
|
+
if (process.env.NODE_ENV === "development") {
|
|
11
|
+
console.warn(`AiChatDrawer: received message from unexpected origin: ${event.origin}`);
|
|
12
|
+
}
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (event.data.type === "smoot-design::chat-open") {
|
|
16
|
+
setOpen(true);
|
|
17
|
+
setChatSettings(event.data.payload);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
console.log("Attaching listener");
|
|
21
|
+
window.addEventListener("message", cb);
|
|
22
|
+
return () => {
|
|
23
|
+
window.removeEventListener("message", cb);
|
|
24
|
+
};
|
|
25
|
+
}, [messageOrigin]);
|
|
26
|
+
return (React.createElement(Drawer, { className: className, PaperProps: {
|
|
27
|
+
sx: {
|
|
28
|
+
width: "600px",
|
|
29
|
+
maxWidth: "100%",
|
|
30
|
+
boxSizing: "border-box",
|
|
31
|
+
padding: "24px 40px",
|
|
32
|
+
".MitAiChat--title": {
|
|
33
|
+
paddingTop: "0px",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
}, anchor: "right", open: open, onClose: () => setOpen(false) }, chatSettings ? (React.createElement(AiChat, Object.assign({}, chatSettings, { requestOpts: {
|
|
37
|
+
transformBody,
|
|
38
|
+
apiUrl: chatSettings === null || chatSettings === void 0 ? void 0 : chatSettings.apiUrl,
|
|
39
|
+
}, onClose: () => setOpen(false) }))) : null));
|
|
40
|
+
};
|
|
41
|
+
export { AiChatDrawer };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { AiChatDrawer } from "./RemoteAiChatDrawer";
|
|
3
|
+
declare const meta: Meta<typeof AiChatDrawer>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof AiChatDrawer>;
|
|
6
|
+
export declare const StreamingResponses: Story;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { AiChatDrawer } from "./RemoteAiChatDrawer";
|
|
4
|
+
import invariant from "tiny-invariant";
|
|
5
|
+
import { handlers } from "../../components/AiChat/test-utils/api";
|
|
6
|
+
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
7
|
+
const INITIAL_MESSAGES = [
|
|
8
|
+
{
|
|
9
|
+
content: "Hi! What are you interested in learning about?",
|
|
10
|
+
role: "assistant",
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
const STARTERS = [
|
|
14
|
+
{ content: "I'm interested in quantum computing" },
|
|
15
|
+
{ content: "I want to understand global warming. " },
|
|
16
|
+
{ content: "I am curious about AI applications for business" },
|
|
17
|
+
];
|
|
18
|
+
const meta = {
|
|
19
|
+
title: "smoot-design/AI/RemoteAiChatDrawer",
|
|
20
|
+
component: AiChatDrawer,
|
|
21
|
+
render: () => {
|
|
22
|
+
return (React.createElement(React.Fragment, null,
|
|
23
|
+
React.createElement("iframe", { width: "600px", height: "300px", ref: (el) => {
|
|
24
|
+
var _a;
|
|
25
|
+
if (!el)
|
|
26
|
+
return;
|
|
27
|
+
const doc = el.contentDocument;
|
|
28
|
+
const parent = (_a = el.contentWindow) === null || _a === void 0 ? void 0 : _a.parent;
|
|
29
|
+
invariant(doc && parent);
|
|
30
|
+
const button = doc.createElement("button");
|
|
31
|
+
button.textContent = "Trigger chat (Send message to parent)";
|
|
32
|
+
doc.body.appendChild(button);
|
|
33
|
+
const div = doc.createElement("div");
|
|
34
|
+
doc.body.appendChild(div);
|
|
35
|
+
const label = doc.createElement("label");
|
|
36
|
+
label.textContent = "Message Data:";
|
|
37
|
+
div.appendChild(label);
|
|
38
|
+
const textarea = doc.createElement("textarea");
|
|
39
|
+
div.append(textarea);
|
|
40
|
+
textarea.style["display"] = "block";
|
|
41
|
+
textarea.style["width"] = "100%";
|
|
42
|
+
textarea.style["height"] = "225px";
|
|
43
|
+
const message = {
|
|
44
|
+
type: "smoot-design::chat-open",
|
|
45
|
+
payload: {
|
|
46
|
+
askTimTitle: "for help with Problem: Derivatives 1.1",
|
|
47
|
+
apiUrl: TEST_API_STREAMING,
|
|
48
|
+
initialMessages: INITIAL_MESSAGES,
|
|
49
|
+
conversationStarters: STARTERS,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
textarea.value = JSON.stringify(message, null, 2);
|
|
53
|
+
button.addEventListener("click", () => {
|
|
54
|
+
parent.postMessage(JSON.parse(textarea.value));
|
|
55
|
+
});
|
|
56
|
+
}, title: "button frame" }),
|
|
57
|
+
React.createElement(AiChatDrawer, { messageOrigin: "http://localhost:6006" })));
|
|
58
|
+
},
|
|
59
|
+
parameters: {
|
|
60
|
+
msw: {
|
|
61
|
+
handlers,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export default meta;
|
|
66
|
+
export const StreamingResponses = {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { AiChatDrawer } from "./RemoteAiChatDrawer/RemoteAiChatDrawer";
|
|
4
|
+
import { ThemeProvider } from "../components/ThemeProvider/ThemeProvider";
|
|
5
|
+
const init = (opts) => {
|
|
6
|
+
const root = document.createElement("div");
|
|
7
|
+
root.id = "smoot-chat-drawer-root";
|
|
8
|
+
document.body.append(root);
|
|
9
|
+
createRoot(root).render(React.createElement(ThemeProvider, null,
|
|
10
|
+
React.createElement(AiChatDrawer, Object.assign({}, opts))));
|
|
11
|
+
};
|
|
12
|
+
export { init };
|
|
@@ -21,6 +21,7 @@ import classNames from "classnames";
|
|
|
21
21
|
import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer";
|
|
22
22
|
import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden";
|
|
23
23
|
import Typography from "@mui/material/Typography";
|
|
24
|
+
import { Alert } from "../Alert/Alert";
|
|
24
25
|
const classes = {
|
|
25
26
|
root: "MitAiChat--root",
|
|
26
27
|
title: "MitAiChat--title",
|
|
@@ -162,7 +163,7 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
162
163
|
const prefix = Math.random().toString().slice(2);
|
|
163
164
|
return initMsgs.map((m, i) => (Object.assign(Object.assign({}, m), { id: `initial-${prefix}-${i}` })));
|
|
164
165
|
}, [initMsgs]);
|
|
165
|
-
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, stop, } = useAiChat(requestOpts, {
|
|
166
|
+
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, stop, error, } = useAiChat(requestOpts, {
|
|
166
167
|
initialMessages: initialMessages,
|
|
167
168
|
id: chatId,
|
|
168
169
|
});
|
|
@@ -178,7 +179,7 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
178
179
|
});
|
|
179
180
|
}, [parseContent, unparsed, initialMessages]);
|
|
180
181
|
const showStarters = messages.length === initialMessages.length;
|
|
181
|
-
const waiting = !showStarters && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
182
|
+
const waiting = !showStarters && !error && ((_b = messages[messages.length - 1]) === null || _b === void 0 ? void 0 : _b.role) === "user";
|
|
182
183
|
const stoppable = isLoading && ((_c = messages[messages.length - 1]) === null || _c === void 0 ? void 0 : _c.role) !== "user";
|
|
183
184
|
const scrollToBottom = () => {
|
|
184
185
|
var _a;
|
|
@@ -204,7 +205,8 @@ const AiChatInternal = function AiChat(_a) {
|
|
|
204
205
|
} }, m.content))))) : null,
|
|
205
206
|
waiting ? (React.createElement(MessageRow, { className: classNames(classes.messageRow, classes.messageRowAssistant), key: "loading" },
|
|
206
207
|
React.createElement(Message, null,
|
|
207
|
-
React.createElement(RiMoreFill, null)))) : null
|
|
208
|
+
React.createElement(RiMoreFill, null)))) : null,
|
|
209
|
+
error ? (React.createElement(Alert, { severity: "error", closable: true }, "An unexpected error has occurred.")) : null),
|
|
208
210
|
React.createElement("form", { onSubmit: (e) => {
|
|
209
211
|
e.preventDefault();
|
|
210
212
|
if (isLoading && stoppable) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { AiChat } from "./AiChat";
|
|
3
|
-
import { mockJson, mockStreaming } from "./story-utils";
|
|
4
3
|
import styled from "@emotion/styled";
|
|
5
4
|
import { fn } from "@storybook/test";
|
|
5
|
+
import { handlers } from "./test-utils/api";
|
|
6
6
|
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
7
7
|
const TEST_API_JSON = "http://localhost:4567/json";
|
|
8
8
|
const INITIAL_MESSAGES = [
|
|
@@ -31,6 +31,9 @@ const Container = styled.div({
|
|
|
31
31
|
const meta = {
|
|
32
32
|
title: "smoot-design/AI/AiChat",
|
|
33
33
|
component: AiChat,
|
|
34
|
+
parameters: {
|
|
35
|
+
msw: { handlers },
|
|
36
|
+
},
|
|
34
37
|
render: (args) => React.createElement(AiChat, Object.assign({}, args)),
|
|
35
38
|
decorators: (Story) => {
|
|
36
39
|
return (React.createElement(Container, null,
|
|
@@ -55,18 +58,6 @@ const meta = {
|
|
|
55
58
|
table: { readonly: true }, // See above
|
|
56
59
|
},
|
|
57
60
|
},
|
|
58
|
-
beforeEach: () => {
|
|
59
|
-
const originalFetch = window.fetch;
|
|
60
|
-
window.fetch = (url, opts) => {
|
|
61
|
-
if (url === TEST_API_STREAMING) {
|
|
62
|
-
return mockStreaming();
|
|
63
|
-
}
|
|
64
|
-
else if (url === TEST_API_JSON) {
|
|
65
|
-
return mockJson();
|
|
66
|
-
}
|
|
67
|
-
return originalFetch(url, opts);
|
|
68
|
-
};
|
|
69
|
-
},
|
|
70
61
|
};
|
|
71
62
|
export default meta;
|
|
72
63
|
export const StreamingResponses = {};
|
|
@@ -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
|
+
};
|