@mitodl/smoot-design 1.1.0 → 1.2.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/dist/cjs/ai.d.ts +2 -0
- package/dist/cjs/ai.js +5 -0
- package/dist/cjs/components/AiChat/AiChat.d.ts +5 -0
- package/dist/cjs/components/AiChat/AiChat.js +150 -0
- package/dist/cjs/components/AiChat/AiChat.stories.d.ts +11 -0
- package/dist/cjs/components/AiChat/AiChat.stories.js +76 -0
- package/dist/cjs/components/AiChat/AiChat.test.d.ts +1 -0
- package/dist/cjs/components/AiChat/AiChat.test.js +130 -0
- package/dist/cjs/components/AiChat/story-utils.d.ts +3 -0
- package/dist/cjs/components/AiChat/story-utils.js +100 -0
- package/dist/cjs/components/AiChat/types.d.ts +45 -0
- package/dist/cjs/components/AiChat/types.js +3 -0
- package/dist/cjs/components/AiChat/utils.d.ts +9 -0
- package/dist/cjs/components/AiChat/utils.js +31 -0
- package/dist/cjs/components/Button/ActionButton.stories.d.ts +1 -1
- package/dist/cjs/components/Button/ActionButton.stories.js +20 -15
- package/dist/cjs/components/Button/Button.d.ts +1 -1
- package/dist/cjs/components/Button/Button.js +17 -10
- package/dist/cjs/components/Button/Button.stories.js +5 -0
- package/dist/cjs/components/ScrollSnap/ScrollSnap.d.ts +19 -0
- package/dist/cjs/components/ScrollSnap/ScrollSnap.js +57 -0
- package/dist/cjs/components/ScrollSnap/ScrollSnap.stories.d.ts +6 -0
- package/dist/cjs/components/ScrollSnap/ScrollSnap.stories.js +43 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.d.ts +25 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.js +43 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.stories.d.ts +6 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.stories.js +44 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.test.d.ts +1 -0
- package/dist/cjs/components/SrAnnouncer/SrAnnouncer.test.js +62 -0
- package/dist/cjs/components/VisuallyHidden/VisuallyHidden.d.ts +24 -0
- package/dist/cjs/components/VisuallyHidden/VisuallyHidden.js +33 -0
- package/dist/cjs/components/VisuallyHidden/VisuallyHidden.stories.d.ts +6 -0
- package/dist/cjs/components/VisuallyHidden/VisuallyHidden.stories.js +13 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/jest-setup.js +15 -0
- package/dist/cjs/jsdom-extended.d.ts +6 -0
- package/dist/cjs/jsdom-extended.js +14 -0
- package/dist/cjs/story-utils/index.d.ts +2 -1
- package/dist/cjs/story-utils/index.js +8 -1
- package/dist/cjs/utils/composeRefs.d.ts +7 -0
- package/dist/cjs/utils/composeRefs.js +20 -0
- package/dist/cjs/utils/composeRefs.test.d.ts +1 -0
- package/dist/cjs/utils/composeRefs.test.js +19 -0
- package/dist/cjs/utils/useDevCheckStable.d.ts +8 -0
- package/dist/cjs/utils/useDevCheckStable.js +29 -0
- package/dist/cjs/utils/useInterval.d.ts +7 -0
- package/dist/cjs/utils/useInterval.js +25 -0
- package/dist/esm/ai.d.ts +2 -0
- package/dist/esm/ai.js +1 -0
- package/dist/esm/components/AiChat/AiChat.d.ts +5 -0
- package/dist/esm/components/AiChat/AiChat.js +147 -0
- package/dist/esm/components/AiChat/AiChat.stories.d.ts +11 -0
- package/dist/esm/components/AiChat/AiChat.stories.js +73 -0
- package/dist/esm/components/AiChat/AiChat.test.d.ts +1 -0
- package/dist/esm/components/AiChat/AiChat.test.js +128 -0
- package/dist/esm/components/AiChat/story-utils.d.ts +3 -0
- package/dist/esm/components/AiChat/story-utils.js +96 -0
- package/dist/esm/components/AiChat/types.d.ts +45 -0
- package/dist/esm/components/AiChat/types.js +2 -0
- package/dist/esm/components/AiChat/utils.d.ts +9 -0
- package/dist/esm/components/AiChat/utils.js +28 -0
- package/dist/esm/components/Button/ActionButton.stories.d.ts +1 -1
- package/dist/esm/components/Button/ActionButton.stories.js +19 -14
- package/dist/esm/components/Button/Button.d.ts +1 -1
- package/dist/esm/components/Button/Button.js +17 -10
- package/dist/esm/components/Button/Button.stories.js +5 -0
- package/dist/esm/components/ScrollSnap/ScrollSnap.d.ts +19 -0
- package/dist/esm/components/ScrollSnap/ScrollSnap.js +54 -0
- package/dist/esm/components/ScrollSnap/ScrollSnap.stories.d.ts +6 -0
- package/dist/esm/components/ScrollSnap/ScrollSnap.stories.js +40 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.d.ts +25 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.js +40 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.stories.d.ts +6 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.stories.js +41 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.test.d.ts +1 -0
- package/dist/esm/components/SrAnnouncer/SrAnnouncer.test.js +60 -0
- package/dist/esm/components/VisuallyHidden/VisuallyHidden.d.ts +24 -0
- package/dist/esm/components/VisuallyHidden/VisuallyHidden.js +30 -0
- package/dist/esm/components/VisuallyHidden/VisuallyHidden.stories.d.ts +6 -0
- package/dist/esm/components/VisuallyHidden/VisuallyHidden.stories.js +10 -0
- package/dist/esm/index.d.ts +7 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/jest-setup.js +15 -0
- package/dist/esm/jsdom-extended.d.ts +6 -0
- package/dist/esm/jsdom-extended.js +12 -0
- package/dist/esm/story-utils/index.d.ts +2 -1
- package/dist/esm/story-utils/index.js +7 -1
- package/dist/esm/utils/composeRefs.d.ts +7 -0
- package/dist/esm/utils/composeRefs.js +17 -0
- package/dist/esm/utils/composeRefs.test.d.ts +1 -0
- package/dist/esm/utils/composeRefs.test.js +17 -0
- package/dist/esm/utils/useDevCheckStable.d.ts +8 -0
- package/dist/esm/utils/useDevCheckStable.js +26 -0
- package/dist/esm/utils/useInterval.d.ts +7 -0
- package/dist/esm/utils/useInterval.js +22 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +35 -18
package/dist/cjs/ai.d.ts
ADDED
package/dist/cjs/ai.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AiChat = void 0;
|
|
4
|
+
var AiChat_1 = require("./components/AiChat/AiChat");
|
|
5
|
+
Object.defineProperty(exports, "AiChat", { enumerable: true, get: function () { return AiChat_1.AiChat; } });
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AiChat = void 0;
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const styled_1 = require("@emotion/styled");
|
|
6
|
+
const Skeleton_1 = require("@mui/material/Skeleton");
|
|
7
|
+
const Input_1 = require("../Input/Input");
|
|
8
|
+
const ActionButton_1 = require("../Button/ActionButton");
|
|
9
|
+
const react_1 = require("@remixicon/react");
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
const react_markdown_1 = require("react-markdown");
|
|
12
|
+
const ScrollSnap_1 = require("../ScrollSnap/ScrollSnap");
|
|
13
|
+
const classnames_1 = require("classnames");
|
|
14
|
+
const SrAnnouncer_1 = require("../SrAnnouncer/SrAnnouncer");
|
|
15
|
+
const ChatContainer = styled_1.default.div(({ theme }) => ({
|
|
16
|
+
width: "100%",
|
|
17
|
+
height: "100%",
|
|
18
|
+
border: `1px solid ${theme.custom.colors.silverGrayLight}`,
|
|
19
|
+
backgroundColor: theme.custom.colors.lightGray1,
|
|
20
|
+
display: "flex",
|
|
21
|
+
flexDirection: "column",
|
|
22
|
+
}));
|
|
23
|
+
const MessagesContainer = (0, styled_1.default)(ScrollSnap_1.ScrollSnap)({
|
|
24
|
+
display: "flex",
|
|
25
|
+
flexDirection: "column",
|
|
26
|
+
flex: 1,
|
|
27
|
+
padding: "24px",
|
|
28
|
+
paddingBottom: "0px",
|
|
29
|
+
overflow: "auto",
|
|
30
|
+
});
|
|
31
|
+
const MessageRow = styled_1.default.div(({ reverse }) => [
|
|
32
|
+
{
|
|
33
|
+
margin: "8px 0",
|
|
34
|
+
display: "flex",
|
|
35
|
+
width: "100%",
|
|
36
|
+
flexDirection: reverse ? "row-reverse" : "row",
|
|
37
|
+
},
|
|
38
|
+
]);
|
|
39
|
+
const Avatar = styled_1.default.div({});
|
|
40
|
+
const Message = styled_1.default.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": {
|
|
41
|
+
marginTop: 0,
|
|
42
|
+
}, "p:last-of-type": {
|
|
43
|
+
marginBottom: 0,
|
|
44
|
+
}, a: {
|
|
45
|
+
color: theme.custom.colors.mitRed,
|
|
46
|
+
textDecoration: "none",
|
|
47
|
+
}, "a:hover": {
|
|
48
|
+
color: theme.custom.colors.red,
|
|
49
|
+
textDecoration: "underline",
|
|
50
|
+
} })));
|
|
51
|
+
const StarterContainer = styled_1.default.div({
|
|
52
|
+
alignSelf: "flex-end",
|
|
53
|
+
display: "flex",
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
gap: "4px",
|
|
56
|
+
});
|
|
57
|
+
const Starter = styled_1.default.button(({ 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), { cursor: "pointer", "&:hover": {
|
|
58
|
+
backgroundColor: theme.custom.colors.lightGray1,
|
|
59
|
+
} })));
|
|
60
|
+
const Controls = styled_1.default.div(({ theme }) => ({
|
|
61
|
+
display: "flex",
|
|
62
|
+
justifyContent: "space-around",
|
|
63
|
+
padding: "12px 24px",
|
|
64
|
+
backgroundColor: theme.custom.colors.white,
|
|
65
|
+
}));
|
|
66
|
+
const Form = styled_1.default.form(() => ({
|
|
67
|
+
display: "flex",
|
|
68
|
+
width: "80%",
|
|
69
|
+
gap: "12px",
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
}));
|
|
72
|
+
const DotsContainer = styled_1.default.span(({ theme }) => ({
|
|
73
|
+
display: "inline-flex",
|
|
74
|
+
gap: "4px",
|
|
75
|
+
".MuiSkeleton-root": {
|
|
76
|
+
backgroundColor: theme.custom.colors.silverGray,
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
const Dots = () => {
|
|
80
|
+
return (React.createElement(DotsContainer, null,
|
|
81
|
+
React.createElement(Skeleton_1.default, { variant: "circular", width: "8px", height: "8px" }),
|
|
82
|
+
React.createElement(Skeleton_1.default, { variant: "circular", width: "8px", height: "8px" }),
|
|
83
|
+
React.createElement(Skeleton_1.default, { variant: "circular", width: "8px", height: "8px" })));
|
|
84
|
+
};
|
|
85
|
+
const classes = {
|
|
86
|
+
root: "MitAiChat--root",
|
|
87
|
+
conversationStarter: "MitAiChat--conversationStarter",
|
|
88
|
+
messagesContainer: "MitAiChat--messagesContainer",
|
|
89
|
+
messageRow: "MitAiChat--messageRow",
|
|
90
|
+
message: "MitAiChat--message",
|
|
91
|
+
avatar: "MitAiChat--avatar",
|
|
92
|
+
input: "MitAiChat--input",
|
|
93
|
+
};
|
|
94
|
+
const AiChat = function AiChat({ className, conversationStarters, requestOpts, initialMessages: initMsgs, parseContent, srLoadingMessages, }) {
|
|
95
|
+
var _a;
|
|
96
|
+
const [showStarters, setShowStarters] = React.useState(true);
|
|
97
|
+
const messagesRef = React.useRef(null);
|
|
98
|
+
const initialMessages = React.useMemo(() => {
|
|
99
|
+
const prefix = Math.random().toString().slice(2);
|
|
100
|
+
return initMsgs.map((m, i) => (Object.assign(Object.assign({}, m), { id: `initial-${prefix}-${i}` })));
|
|
101
|
+
}, [initMsgs]);
|
|
102
|
+
const { messages: unparsed, input, handleInputChange, handleSubmit, append, isLoading, } = (0, utils_1.useAiChat)(requestOpts, {
|
|
103
|
+
initialMessages: initialMessages,
|
|
104
|
+
});
|
|
105
|
+
const messages = React.useMemo(() => {
|
|
106
|
+
const initial = initialMessages.map((m) => m.id);
|
|
107
|
+
return unparsed.map((m) => {
|
|
108
|
+
if (m.role === "assistant" && !initial.includes(m.id)) {
|
|
109
|
+
const content = parseContent ? parseContent(m.content) : m.content;
|
|
110
|
+
return Object.assign(Object.assign({}, m), { content });
|
|
111
|
+
}
|
|
112
|
+
return m;
|
|
113
|
+
});
|
|
114
|
+
}, [parseContent, unparsed, initialMessages]);
|
|
115
|
+
const waiting = !showStarters && ((_a = messages[messages.length - 1]) === null || _a === void 0 ? void 0 : _a.role) === "user";
|
|
116
|
+
const scrollToBottom = () => {
|
|
117
|
+
var _a;
|
|
118
|
+
(_a = messagesRef.current) === null || _a === void 0 ? void 0 : _a.scrollBy({
|
|
119
|
+
behavior: "instant",
|
|
120
|
+
top: messagesRef.current.scrollHeight,
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const lastMsg = messages[messages.length - 1];
|
|
124
|
+
return (React.createElement(ChatContainer, { className: (0, classnames_1.default)(className, classes.root) },
|
|
125
|
+
React.createElement(MessagesContainer, { className: classes.messagesContainer, ref: messagesRef },
|
|
126
|
+
messages.map((m) => (React.createElement(MessageRow, { key: m.id, reverse: m.role === "user", "data-chat-role": m.role, className: classes.messageRow },
|
|
127
|
+
React.createElement(Avatar, null),
|
|
128
|
+
React.createElement(Message, { className: classes.message },
|
|
129
|
+
React.createElement(react_markdown_1.default, null, m.content))))),
|
|
130
|
+
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: () => {
|
|
131
|
+
setShowStarters(false);
|
|
132
|
+
scrollToBottom();
|
|
133
|
+
append({ role: "user", content: m.content });
|
|
134
|
+
} }, m.content))))) : null,
|
|
135
|
+
waiting ? (React.createElement(MessageRow, { key: "loading" },
|
|
136
|
+
React.createElement(Avatar, { className: classes.avatar }),
|
|
137
|
+
React.createElement(Message, null,
|
|
138
|
+
React.createElement(Dots, null)))) : null),
|
|
139
|
+
React.createElement(Controls, null,
|
|
140
|
+
React.createElement(Form, { onSubmit: (e) => {
|
|
141
|
+
setShowStarters(false);
|
|
142
|
+
scrollToBottom();
|
|
143
|
+
handleSubmit(e);
|
|
144
|
+
} },
|
|
145
|
+
React.createElement(Input_1.Input, { className: classes.input, placeholder: "Type a message...", name: "message", sx: { flex: 1 }, value: input, onChange: handleInputChange }),
|
|
146
|
+
React.createElement(ActionButton_1.ActionButton, { "aria-label": "Send", type: "submit", disabled: isLoading || !input },
|
|
147
|
+
React.createElement(react_1.RiSendPlaneFill, null)))),
|
|
148
|
+
React.createElement(SrAnnouncer_1.SrAnnouncer, { isLoading: isLoading, loadingMessages: srLoadingMessages, message: lastMsg.role === "assistant" ? lastMsg.content : "" })));
|
|
149
|
+
};
|
|
150
|
+
exports.AiChat = AiChat;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { AiChat } from "./AiChat";
|
|
3
|
+
declare const meta: Meta<typeof AiChat>;
|
|
4
|
+
export default meta;
|
|
5
|
+
type Story = StoryObj<typeof AiChat>;
|
|
6
|
+
export declare const StreamingResponses: Story;
|
|
7
|
+
/**
|
|
8
|
+
* Here `AiChat` is used with a non-streaming JSON API. The JSON is converted
|
|
9
|
+
* to text via `parseContent`.
|
|
10
|
+
*/
|
|
11
|
+
export declare const JsonResponses: Story;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JsonResponses = exports.StreamingResponses = void 0;
|
|
4
|
+
const React = require("react");
|
|
5
|
+
const AiChat_1 = require("./AiChat");
|
|
6
|
+
const story_utils_1 = require("./story-utils");
|
|
7
|
+
const styled_1 = require("@emotion/styled");
|
|
8
|
+
const TEST_API_STREAMING = "http://localhost:4567/streaming";
|
|
9
|
+
const TEST_API_JSON = "http://localhost:4567/json";
|
|
10
|
+
const INITIAL_MESSAGES = [
|
|
11
|
+
{
|
|
12
|
+
content: "Hi! What are you interested in learning about?",
|
|
13
|
+
role: "assistant",
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
const STARTERS = [
|
|
17
|
+
{ content: "I'm interested in quantum computing" },
|
|
18
|
+
{ content: "I want to understand global warming. " },
|
|
19
|
+
{ content: "I am curious about AI applications for business" },
|
|
20
|
+
];
|
|
21
|
+
const Container = styled_1.default.div({
|
|
22
|
+
width: "100%",
|
|
23
|
+
height: "350px",
|
|
24
|
+
});
|
|
25
|
+
const meta = {
|
|
26
|
+
title: "smoot-design/AiChat",
|
|
27
|
+
component: AiChat_1.AiChat,
|
|
28
|
+
render: (args) => React.createElement(AiChat_1.AiChat, Object.assign({}, args)),
|
|
29
|
+
decorators: (Story) => {
|
|
30
|
+
return (React.createElement(Container, null,
|
|
31
|
+
React.createElement(Story, null)));
|
|
32
|
+
},
|
|
33
|
+
args: {
|
|
34
|
+
initialMessages: INITIAL_MESSAGES,
|
|
35
|
+
requestOpts: { apiUrl: TEST_API_STREAMING },
|
|
36
|
+
conversationStarters: STARTERS,
|
|
37
|
+
},
|
|
38
|
+
argTypes: {
|
|
39
|
+
conversationStarters: {
|
|
40
|
+
control: { type: "object", disable: true },
|
|
41
|
+
},
|
|
42
|
+
initialMessages: {
|
|
43
|
+
control: { type: "object", disable: true },
|
|
44
|
+
},
|
|
45
|
+
requestOpts: {
|
|
46
|
+
control: { type: "object", disable: true },
|
|
47
|
+
table: { readonly: true }, // See above
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
beforeEach: () => {
|
|
51
|
+
const originalFetch = window.fetch;
|
|
52
|
+
window.fetch = (url, opts) => {
|
|
53
|
+
if (url === TEST_API_STREAMING) {
|
|
54
|
+
return (0, story_utils_1.mockStreaming)();
|
|
55
|
+
}
|
|
56
|
+
else if (url === TEST_API_JSON) {
|
|
57
|
+
return (0, story_utils_1.mockJson)();
|
|
58
|
+
}
|
|
59
|
+
return originalFetch(url, opts);
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
exports.default = meta;
|
|
64
|
+
exports.StreamingResponses = {};
|
|
65
|
+
/**
|
|
66
|
+
* Here `AiChat` is used with a non-streaming JSON API. The JSON is converted
|
|
67
|
+
* to text via `parseContent`.
|
|
68
|
+
*/
|
|
69
|
+
exports.JsonResponses = {
|
|
70
|
+
args: {
|
|
71
|
+
requestOpts: { apiUrl: TEST_API_JSON },
|
|
72
|
+
parseContent: (content) => {
|
|
73
|
+
return JSON.parse(content).message;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
// This was giving false positives
|
|
13
|
+
/* eslint-disable testing-library/await-async-utils */
|
|
14
|
+
const react_1 = require("@testing-library/react");
|
|
15
|
+
const user_event_1 = require("@testing-library/user-event");
|
|
16
|
+
const AiChat_1 = require("./AiChat");
|
|
17
|
+
const ThemeProvider_1 = require("../ThemeProvider/ThemeProvider");
|
|
18
|
+
const React = require("react");
|
|
19
|
+
const en_1 = require("@faker-js/faker/locale/en");
|
|
20
|
+
const counter = jest.fn(); // use jest.fn as counter because it resets on each test
|
|
21
|
+
const mockFetch = jest.mocked(jest.fn(() => {
|
|
22
|
+
const count = counter.mock.calls.length;
|
|
23
|
+
counter();
|
|
24
|
+
return Promise.resolve(new Response(`AI Response ${count}`, {
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
}));
|
|
30
|
+
window.fetch = mockFetch;
|
|
31
|
+
jest.mock("react-markdown", () => {
|
|
32
|
+
return {
|
|
33
|
+
__esModule: true,
|
|
34
|
+
default: ({ children }) => React.createElement("div", null, children),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const getMessages = () => {
|
|
38
|
+
return Array.from(document.querySelectorAll(".MitAiChat--message"));
|
|
39
|
+
};
|
|
40
|
+
const getConversationStarters = () => {
|
|
41
|
+
return Array.from(document.querySelectorAll("button.MitAiChat--conversationStarter"));
|
|
42
|
+
};
|
|
43
|
+
const whenCount = (cb, count) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
return yield (0, react_1.waitFor)(() => {
|
|
45
|
+
const result = cb();
|
|
46
|
+
expect(result).toHaveLength(count);
|
|
47
|
+
return result;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("AiChat", () => {
|
|
51
|
+
const setup = (props = {}) => {
|
|
52
|
+
const initialMessages = [
|
|
53
|
+
{ role: "assistant", content: en_1.faker.lorem.sentence() },
|
|
54
|
+
];
|
|
55
|
+
const conversationStarters = [
|
|
56
|
+
{ content: en_1.faker.lorem.sentence() },
|
|
57
|
+
{ content: en_1.faker.lorem.sentence() },
|
|
58
|
+
];
|
|
59
|
+
(0, react_1.render)(React.createElement(AiChat_1.AiChat, Object.assign({ initialMessages: initialMessages, conversationStarters: conversationStarters, requestOpts: { apiUrl: "http://localhost:4567/test" } }, props)), { wrapper: ThemeProvider_1.ThemeProvider });
|
|
60
|
+
return { initialMessages, conversationStarters };
|
|
61
|
+
};
|
|
62
|
+
test("Clicking conversation starters and sending chats", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
63
|
+
const { initialMessages, conversationStarters } = setup();
|
|
64
|
+
const scrollBy = jest.spyOn(HTMLElement.prototype, "scrollBy");
|
|
65
|
+
const initialMessageEls = getMessages();
|
|
66
|
+
expect(initialMessageEls.length).toBe(1);
|
|
67
|
+
expect(initialMessageEls[0]).toHaveTextContent(initialMessages[0].content);
|
|
68
|
+
const starterEls = getConversationStarters();
|
|
69
|
+
expect(starterEls.length).toBe(2);
|
|
70
|
+
expect(starterEls[0]).toHaveTextContent(conversationStarters[0].content);
|
|
71
|
+
expect(starterEls[1]).toHaveTextContent(conversationStarters[1].content);
|
|
72
|
+
const chosen = en_1.faker.helpers.arrayElement([0, 1]);
|
|
73
|
+
yield user_event_1.default.click(starterEls[chosen]);
|
|
74
|
+
expect(scrollBy).toHaveBeenCalled();
|
|
75
|
+
scrollBy.mockReset();
|
|
76
|
+
const messageEls = yield whenCount(getMessages, 3);
|
|
77
|
+
expect(messageEls[0]).toHaveTextContent(initialMessages[0].content);
|
|
78
|
+
expect(messageEls[1]).toHaveTextContent(conversationStarters[chosen].content);
|
|
79
|
+
expect(messageEls[2]).toHaveTextContent("AI Response 0");
|
|
80
|
+
yield user_event_1.default.click(react_1.screen.getByPlaceholderText("Type a message..."));
|
|
81
|
+
yield user_event_1.default.paste("User message");
|
|
82
|
+
yield user_event_1.default.click(react_1.screen.getByRole("button", { name: "Send" }));
|
|
83
|
+
expect(scrollBy).toHaveBeenCalled();
|
|
84
|
+
const afterSending = yield whenCount(getMessages, 5);
|
|
85
|
+
expect(afterSending[3]).toHaveTextContent("User message");
|
|
86
|
+
expect(afterSending[4]).toHaveTextContent("AI Response 1");
|
|
87
|
+
}));
|
|
88
|
+
test("transformBody is called before sending requests", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
89
|
+
const fakeBody = { message: en_1.faker.lorem.sentence() };
|
|
90
|
+
const apiUrl = en_1.faker.internet.url();
|
|
91
|
+
const transformBody = jest.fn(() => fakeBody);
|
|
92
|
+
const { initialMessages } = setup({
|
|
93
|
+
requestOpts: { apiUrl, transformBody },
|
|
94
|
+
});
|
|
95
|
+
yield user_event_1.default.click(react_1.screen.getByPlaceholderText("Type a message..."));
|
|
96
|
+
yield user_event_1.default.paste("User message");
|
|
97
|
+
yield user_event_1.default.click(react_1.screen.getByRole("button", { name: "Send" }));
|
|
98
|
+
expect(transformBody).toHaveBeenCalledWith([
|
|
99
|
+
expect.objectContaining(initialMessages[0]),
|
|
100
|
+
expect.objectContaining({ content: "User message", role: "user" }),
|
|
101
|
+
]);
|
|
102
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
103
|
+
expect(mockFetch).toHaveBeenCalledWith(apiUrl, expect.objectContaining({
|
|
104
|
+
body: JSON.stringify(fakeBody),
|
|
105
|
+
}));
|
|
106
|
+
}));
|
|
107
|
+
test("parseContent is called on the API-received message content", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
108
|
+
const fakeBody = { message: en_1.faker.lorem.sentence() };
|
|
109
|
+
const apiUrl = en_1.faker.internet.url();
|
|
110
|
+
const transformBody = jest.fn(() => fakeBody);
|
|
111
|
+
const { initialMessages, conversationStarters } = setup({
|
|
112
|
+
requestOpts: { apiUrl, transformBody },
|
|
113
|
+
parseContent: jest.fn((content) => `Parsed: ${content}`),
|
|
114
|
+
});
|
|
115
|
+
yield user_event_1.default.click(getConversationStarters()[0]);
|
|
116
|
+
yield whenCount(getMessages, initialMessages.length + 2);
|
|
117
|
+
yield user_event_1.default.click(react_1.screen.getByPlaceholderText("Type a message..."));
|
|
118
|
+
yield user_event_1.default.paste("User message");
|
|
119
|
+
yield user_event_1.default.click(react_1.screen.getByRole("button", { name: "Send" }));
|
|
120
|
+
yield whenCount(getMessages, initialMessages.length + 4);
|
|
121
|
+
const messagesTexts = getMessages().map((el) => el.textContent);
|
|
122
|
+
expect(messagesTexts).toEqual([
|
|
123
|
+
initialMessages[0].content,
|
|
124
|
+
conversationStarters[0].content,
|
|
125
|
+
"Parsed: AI Response 0",
|
|
126
|
+
"User message",
|
|
127
|
+
"Parsed: AI Response 1",
|
|
128
|
+
]);
|
|
129
|
+
}));
|
|
130
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.mockJson = exports.mockStreaming = void 0;
|
|
13
|
+
const SAMPLE_RESPONSES = [
|
|
14
|
+
`For exploring AI applications in business, I recommend the following course from MIT:
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
`,
|
|
20
|
+
`
|
|
21
|
+
To understand global warming, I recommend the following resources from MIT:
|
|
22
|
+
|
|
23
|
+
1. **[Global Warming Science](https://www.edx.org/learn/global-warming/massachusetts-institute-of-technology-global-warming-science)**: This course offered by MITx covers the physics, chemistry, biology, and geology of the earth’s climate system. It's a comprehensive introduction to the scientific principles underlying global warming.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
`,
|
|
29
|
+
`
|
|
30
|
+
Here are some courses on linear algebra that you can explore:
|
|
31
|
+
|
|
32
|
+
1. **[Linear Algebra (MIT OpenCourseWare)](https://openlearninglibrary.mit.edu/courses/course-v1:OCW+18.06SC+2T2019/about)**: This course covers matrix theory and linear algebra, emphasizing topics useful in various disciplines such as physics, economics, social sciences, natural sciences, and engineering. It parallels the combination of theory and applications in Professor Strang's textbook "Introduction to Linear Algebra." This course is free and available through MIT OpenCourseWare.
|
|
33
|
+
|
|
34
|
+
2. **[Mathematical Methods for Quantitative Finance (MITx)](https://www.edx.org/learn/finance/massachusetts-institute-of-technology-mathematical-methods-for-quantitative-finance)**: This course covers the mathematical foundations essential for financial engineering and quantitative finance, including linear algebra, optimization, probability, stochastic processes, statistics, and applied computational techniques in R. It is free and offers certification upon completion.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
These courses provide a comprehensive introduction to linear algebra and its applications across various fields.
|
|
39
|
+
`,
|
|
40
|
+
];
|
|
41
|
+
const rand = (min, max) => {
|
|
42
|
+
// min and max included
|
|
43
|
+
return Math.floor(Math.random() * (max - min + 1) + min);
|
|
44
|
+
};
|
|
45
|
+
const mockStreaming = function mockApi() {
|
|
46
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
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
|
+
yield new Promise((resolve) => setTimeout(resolve, 1500));
|
|
65
|
+
const body = new ReadableStream({
|
|
66
|
+
start(controller) {
|
|
67
|
+
timerId = setInterval(() => {
|
|
68
|
+
const msg = new TextEncoder().encode(chunks[i]);
|
|
69
|
+
controller.enqueue(msg);
|
|
70
|
+
i++;
|
|
71
|
+
if (i === num) {
|
|
72
|
+
controller.close();
|
|
73
|
+
clearInterval(timerId);
|
|
74
|
+
}
|
|
75
|
+
}, 250);
|
|
76
|
+
},
|
|
77
|
+
cancel() {
|
|
78
|
+
if (timerId) {
|
|
79
|
+
clearInterval(timerId);
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
return Promise.resolve(new Response(body, {
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "text/event-stream",
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
exports.mockStreaming = mockStreaming;
|
|
91
|
+
const mockJson = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
92
|
+
const message = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)];
|
|
93
|
+
yield new Promise((res) => setTimeout(res, 2000));
|
|
94
|
+
return Promise.resolve(new Response(JSON.stringify({ message }), {
|
|
95
|
+
headers: {
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
},
|
|
98
|
+
}));
|
|
99
|
+
});
|
|
100
|
+
exports.mockJson = mockJson;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Role = "assistant" | "user";
|
|
2
|
+
type ChatMessage = {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
role: Role;
|
|
6
|
+
};
|
|
7
|
+
type RequestOpts = {
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* Transforms array of chat messages into request body. Messages
|
|
11
|
+
* are ordered oldest to newest.
|
|
12
|
+
*
|
|
13
|
+
* JSON.stringify is applied to the return value.
|
|
14
|
+
*/
|
|
15
|
+
transformBody?: (messages: ChatMessage[]) => unknown;
|
|
16
|
+
/**
|
|
17
|
+
* Extra options to pass to fetch.
|
|
18
|
+
*
|
|
19
|
+
* If headers are specified, they will override the headersOpts.
|
|
20
|
+
*/
|
|
21
|
+
fetchOpts?: RequestInit;
|
|
22
|
+
/**
|
|
23
|
+
* Extra headers to pass to fetch.
|
|
24
|
+
*/
|
|
25
|
+
headersOpts?: HeadersInit;
|
|
26
|
+
};
|
|
27
|
+
type AiChatProps = {
|
|
28
|
+
className?: string;
|
|
29
|
+
initialMessages: Omit<ChatMessage, "id">[];
|
|
30
|
+
conversationStarters?: {
|
|
31
|
+
content: string;
|
|
32
|
+
}[];
|
|
33
|
+
requestOpts: RequestOpts;
|
|
34
|
+
parseContent?: (content: unknown) => string;
|
|
35
|
+
/**
|
|
36
|
+
* A message to display while the component is in a loading state.
|
|
37
|
+
*
|
|
38
|
+
* Identical consecutive messages may not be read on some screen readers.
|
|
39
|
+
*/
|
|
40
|
+
srLoadingMessages?: {
|
|
41
|
+
delay: number;
|
|
42
|
+
text: string;
|
|
43
|
+
}[];
|
|
44
|
+
};
|
|
45
|
+
export type { RequestOpts, AiChatProps, ChatMessage };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { UseChatOptions } from "ai/react";
|
|
2
|
+
import type { RequestOpts } from "./types";
|
|
3
|
+
declare const useAiChat: (requestOpts: RequestOpts, opts: UseChatOptions) => import("@ai-sdk/react").UseChatHelpers & {
|
|
4
|
+
addToolResult: ({ toolCallId, result, }: {
|
|
5
|
+
toolCallId: string;
|
|
6
|
+
result: any;
|
|
7
|
+
}) => void;
|
|
8
|
+
};
|
|
9
|
+
export { useAiChat };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.useAiChat = void 0;
|
|
13
|
+
const react_1 = require("ai/react");
|
|
14
|
+
const react_2 = require("react");
|
|
15
|
+
const identity = (x) => x;
|
|
16
|
+
const getFetcher = (requestOpts) => (url, opts) => __awaiter(void 0, void 0, void 0, function* () {
|
|
17
|
+
var _a;
|
|
18
|
+
if (typeof (opts === null || opts === void 0 ? void 0 : opts.body) !== "string") {
|
|
19
|
+
console.error("Unexpected body type.");
|
|
20
|
+
return window.fetch(url, opts);
|
|
21
|
+
}
|
|
22
|
+
const messages = JSON.parse(opts === null || opts === void 0 ? void 0 : opts.body).messages;
|
|
23
|
+
const transformBody = (_a = requestOpts.transformBody) !== null && _a !== void 0 ? _a : identity;
|
|
24
|
+
const options = Object.assign(Object.assign(Object.assign({}, opts), { body: JSON.stringify(transformBody(messages)), headers: Object.assign(Object.assign(Object.assign({}, opts === null || opts === void 0 ? void 0 : opts.headers), { "Content-Type": "application/json" }), requestOpts.headersOpts) }), requestOpts.fetchOpts);
|
|
25
|
+
return fetch(url, options);
|
|
26
|
+
});
|
|
27
|
+
const useAiChat = (requestOpts, opts) => {
|
|
28
|
+
const fetcher = (0, react_2.useMemo)(() => getFetcher(requestOpts), [requestOpts]);
|
|
29
|
+
return (0, react_1.useChat)(Object.assign({ api: requestOpts.apiUrl, streamProtocol: "text", fetch: fetcher }, opts));
|
|
30
|
+
};
|
|
31
|
+
exports.useAiChat = useAiChat;
|
|
@@ -4,7 +4,6 @@ declare const meta: Meta<typeof ActionButton>;
|
|
|
4
4
|
export default meta;
|
|
5
5
|
type Story = StoryObj<typeof ActionButton>;
|
|
6
6
|
export declare const VariantsAndEdge: Story;
|
|
7
|
-
export declare const Showcase: Story;
|
|
8
7
|
/**
|
|
9
8
|
* `ActionButtonLink` is styled as a `ActionButton` that renders an anchor tag.
|
|
10
9
|
*
|
|
@@ -13,3 +12,4 @@ export declare const Showcase: Story;
|
|
|
13
12
|
* default link adapter via [Theme's LinkAdapter](../?path=/docs/smoot-design-themeprovider--docs)
|
|
14
13
|
*/
|
|
15
14
|
export declare const Links: Story;
|
|
15
|
+
export declare const Showcase: Story;
|