From 8375c8a3fae6781444df182d3274c4c6d4c14404 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Tue, 17 Dec 2024 19:56:12 -0500 Subject: [PATCH] feat: AI Chat UI (#25) --- .eslintrc.js | 1 + .storybook/main.ts | 13 + jest.config.ts | 2 +- package.json | 47 +- src/ai.ts | 2 + src/components/AiChat/AiChat.mdx | 25 + src/components/AiChat/AiChat.stories.tsx | 87 ++ src/components/AiChat/AiChat.test.tsx | 162 ++++ src/components/AiChat/AiChat.tsx | 251 ++++++ src/components/AiChat/story-utils.ts | 95 +++ src/components/AiChat/types.ts | 48 ++ src/components/AiChat/utils.ts | 39 + .../ScrollSnap/ScrollSnap.stories.tsx | 61 ++ src/components/ScrollSnap/ScrollSnap.tsx | 79 ++ .../SrAnnouncer/SrAnnouncer.stories.tsx | 57 ++ .../SrAnnouncer/SrAnnouncer.test.tsx | 91 +++ src/components/SrAnnouncer/SrAnnouncer.tsx | 77 ++ .../VisuallyHidden/VisuallyHidden.stories.tsx | 16 + .../VisuallyHidden/VisuallyHidden.tsx | 32 + src/index.ts | 4 + src/jest-setup.ts | 18 + src/jsdom-extended.ts | 16 + src/story-utils/index.ts | 9 +- src/utils/composeRefs.test.tsx | 25 + src/utils/composeRefs.ts | 25 + src/utils/useDevCheckStable.ts | 36 + src/utils/useInterval.ts | 25 + yarn.lock | 741 +++++++++++++----- 28 files changed, 1857 insertions(+), 227 deletions(-) create mode 100644 src/ai.ts create mode 100644 src/components/AiChat/AiChat.mdx create mode 100644 src/components/AiChat/AiChat.stories.tsx create mode 100644 src/components/AiChat/AiChat.test.tsx create mode 100644 src/components/AiChat/AiChat.tsx create mode 100644 src/components/AiChat/story-utils.ts create mode 100644 src/components/AiChat/types.ts create mode 100644 src/components/AiChat/utils.ts create mode 100644 src/components/ScrollSnap/ScrollSnap.stories.tsx create mode 100644 src/components/ScrollSnap/ScrollSnap.tsx create mode 100644 src/components/SrAnnouncer/SrAnnouncer.stories.tsx create mode 100644 src/components/SrAnnouncer/SrAnnouncer.test.tsx create mode 100644 src/components/SrAnnouncer/SrAnnouncer.tsx create mode 100644 src/components/VisuallyHidden/VisuallyHidden.stories.tsx create mode 100644 src/components/VisuallyHidden/VisuallyHidden.tsx create mode 100644 src/jsdom-extended.ts create mode 100644 src/utils/composeRefs.test.tsx create mode 100644 src/utils/composeRefs.ts create mode 100644 src/utils/useDevCheckStable.ts create mode 100644 src/utils/useInterval.ts diff --git a/.eslintrc.js b/.eslintrc.js index 20d2c77..495ab69 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -46,6 +46,7 @@ module.exports = { "**/*.test.tsx", "**/src/setupJest.ts", "**/jest-setup.ts", + "**/jsdom-extended.ts", "**/test-utils/**", "**/test-utils/**", "**/webpack.config.js", diff --git a/.storybook/main.ts b/.storybook/main.ts index 70ca997..091f199 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,5 +1,13 @@ import { StorybookConfig } from "@storybook/react-webpack5" import TsconfigPathsPlugin from "tsconfig-paths-webpack-plugin" +import { exec as execCb } from "child_process" +import { promisify } from "util" + +const exec = promisify(execCb) +const getGitSha = async (): Promise => { + const { stdout } = await exec("git rev-parse HEAD") + return stdout.trim() +} const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.tsx"], @@ -26,6 +34,11 @@ const config: StorybookConfig = { typescript: { reactDocgen: "react-docgen-typescript", }, + env: async () => { + return { + STORYBOOK_GIT_SHA: await getGitSha(), + } + }, } export default config diff --git a/jest.config.ts b/jest.config.ts index d74fb0d..2217db0 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -9,7 +9,7 @@ const config: Config.InitialOptions = { "jest-watch-typeahead/testname", ], setupFilesAfterEnv: ["./jest-setup.ts"], - testEnvironment: "jsdom", + testEnvironment: "/jsdom-extended.ts", transform: { "^.+\\.(t|j)sx?$": "@swc/jest", }, diff --git a/package.json b/package.json index b882118..abc2327 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,20 @@ "default": "./dist/esm/index.js" } }, + "./ai": { + "import": { + "types": "./dist/esm/ai.d.ts", + "default": "./dist/esm/ai.js" + }, + "require": { + "types": "./dist/cjs/ai.d.ts", + "default": "./dist/cjs/ai.js" + }, + "default": { + "types": "./dist/esm/ai.d.ts", + "default": "./dist/esm/ai.js" + } + }, "./type-augmentation": { "import": "./dist/type-augmentation/index.d.ts", "require": "./dist/type-augmentation/index.d.ts", @@ -55,27 +69,30 @@ "@mui/system": "^6.1.6", "@remixicon/react": "^4.2.0", "@types/jest": "^29.5.14", + "ai": "^4.0.13", "classnames": "^2.5.1", "lodash": "^4.17.21", - "material-ui-popup-state": "^5.1.0", - "tiny-invariant": "^1.3.1" + "react-markdown": "^9.0.1", + "tiny-invariant": "^1.3.1", + "zod": "^3.23.8" }, "devDependencies": { "@chromatic-com/storybook": "^1.9.0", "@faker-js/faker": "^9.0.0", - "@storybook/addon-actions": "^8.4.2", - "@storybook/addon-essentials": "^8.4.2", - "@storybook/addon-interactions": "^8.4.2", - "@storybook/addon-links": "^8.4.2", - "@storybook/addon-onboarding": "^8.4.2", + "@jest/environment": "^29.7.0", + "@storybook/addon-actions": "^8.4.7", + "@storybook/addon-essentials": "^8.4.7", + "@storybook/addon-interactions": "^8.4.7", + "@storybook/addon-links": "^8.4.7", + "@storybook/addon-onboarding": "^8.4.7", "@storybook/addon-webpack5-compiler-swc": "^1.0.5", - "@storybook/blocks": "^8.4.2", - "@storybook/nextjs": "^8.4.2", - "@storybook/preview-api": "^8.4.2", - "@storybook/react": "^8.4.2", - "@storybook/react-webpack5": "^8.4.2", - "@storybook/test": "^8.4.2", - "@storybook/types": "^8.4.2", + "@storybook/blocks": "^8.4.7", + "@storybook/nextjs": "^8.4.7", + "@storybook/preview-api": "^8.4.7", + "@storybook/react": "^8.4.7", + "@storybook/react-webpack5": "^8.4.7", + "@storybook/test": "^8.4.7", + "@storybook/types": "^8.4.7", "@swc/jest": "^0.2.37", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", @@ -99,7 +116,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", "jest-extended": "^4.0.2", - "jest-fail-on-console": "^3.2.0", + "jest-fail-on-console": "^3.3.1", "jest-watch-typeahead": "^2.2.2", "next": "^15.0.2", "prettier": "^3.3.3", diff --git a/src/ai.ts b/src/ai.ts new file mode 100644 index 0000000..d845dc6 --- /dev/null +++ b/src/ai.ts @@ -0,0 +1,2 @@ +export { AiChat } from "./components/AiChat/AiChat" +export type { AiChatProps } from "./components/AiChat/AiChat" diff --git a/src/components/AiChat/AiChat.mdx b/src/components/AiChat/AiChat.mdx new file mode 100644 index 0000000..b9cab15 --- /dev/null +++ b/src/components/AiChat/AiChat.mdx @@ -0,0 +1,25 @@ +import { Meta, Title, Primary, Controls, Stories } from "@storybook/blocks" + +import * as AiChat from "./AiChat.stories" +import { gitLink } from "../../story-utils" + + + + + +Exported from `smoot-design/ai`, the AiChat component is a chat interface +for use with AI services. It can be used with text-streaming or JSON APIs. + +This demo shows the AiChat component with a simple text-streaming API. + +<Primary /> + +## Inputs + +The component accepts the following inputs (props): + +<Controls /> + +See <a href={gitLink("src/components/AiChat/types.ts")}>AiChat/types.ts</a> for all Typescript interface definitions. + +<Stories includePrimary={false} /> diff --git a/src/components/AiChat/AiChat.stories.tsx b/src/components/AiChat/AiChat.stories.tsx new file mode 100644 index 0000000..f994526 --- /dev/null +++ b/src/components/AiChat/AiChat.stories.tsx @@ -0,0 +1,87 @@ +import * as React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import { AiChat } from "./AiChat" +import type { AiChatProps } from "./types" +import { mockJson, mockStreaming } from "./story-utils" +import styled from "@emotion/styled" + +const TEST_API_STREAMING = "http://localhost:4567/streaming" +const TEST_API_JSON = "http://localhost:4567/json" + +const INITIAL_MESSAGES: AiChatProps["initialMessages"] = [ + { + content: "Hi! What are you interested in learning about?", + role: "assistant", + }, +] + +const STARTERS = [ + { content: "I'm interested in quantum computing" }, + { content: "I want to understand global warming. " }, + { content: "I am curious about AI applications for business" }, +] + +const Container = styled.div({ + width: "100%", + height: "350px", +}) + +const meta: Meta<typeof AiChat> = { + title: "smoot-design/AiChat", + component: AiChat, + render: (args) => <AiChat {...args} />, + decorators: (Story) => { + return ( + <Container> + <Story /> + </Container> + ) + }, + args: { + initialMessages: INITIAL_MESSAGES, + requestOpts: { apiUrl: TEST_API_STREAMING }, + conversationStarters: STARTERS, + }, + argTypes: { + conversationStarters: { + control: { type: "object", disable: true }, + }, + initialMessages: { + control: { type: "object", disable: true }, + }, + requestOpts: { + control: { type: "object", disable: true }, + table: { readonly: true }, // See above + }, + }, + beforeEach: () => { + const originalFetch = window.fetch + window.fetch = (url, opts) => { + if (url === TEST_API_STREAMING) { + return mockStreaming() + } else if (url === TEST_API_JSON) { + return mockJson() + } + return originalFetch(url, opts) + } + }, +} + +export default meta + +type Story = StoryObj<typeof AiChat> + +export const StreamingResponses: Story = {} + +/** + * Here `AiChat` is used with a non-streaming JSON API. The JSON is converted + * to text via `parseContent`. + */ +export const JsonResponses: Story = { + args: { + requestOpts: { apiUrl: TEST_API_JSON }, + parseContent: (content: unknown) => { + return JSON.parse(content as string).message + }, + }, +} diff --git a/src/components/AiChat/AiChat.test.tsx b/src/components/AiChat/AiChat.test.tsx new file mode 100644 index 0000000..6a82af2 --- /dev/null +++ b/src/components/AiChat/AiChat.test.tsx @@ -0,0 +1,162 @@ +// This was giving false positives +/* eslint-disable testing-library/await-async-utils */ +import { render, screen, waitFor } from "@testing-library/react" +import user from "@testing-library/user-event" +import { AiChat } from "./AiChat" +import { ThemeProvider } from "../ThemeProvider/ThemeProvider" +import * as React from "react" +import { AiChatProps } from "./types" +import { faker } from "@faker-js/faker/locale/en" + +const counter = jest.fn() // use jest.fn as counter because it resets on each test +const mockFetch = jest.mocked( + jest.fn(() => { + const count = counter.mock.calls.length + counter() + return Promise.resolve( + new Response(`AI Response ${count}`, { + headers: { + "Content-Type": "application/json", + }, + }), + ) + }) as typeof fetch, +) +window.fetch = mockFetch +jest.mock("react-markdown", () => { + return { + __esModule: true, + default: ({ children }: { children: string }) => <div>{children}</div>, + } +}) + +const getMessages = (): HTMLElement[] => { + return Array.from(document.querySelectorAll(".MitAiChat--message")) +} +const getConversationStarters = (): HTMLElement[] => { + return Array.from( + document.querySelectorAll("button.MitAiChat--conversationStarter"), + ) +} +const whenCount = async <T,>(cb: () => T[], count: number) => { + return await waitFor(() => { + const result = cb() + expect(result).toHaveLength(count) + return result + }) +} + +describe("AiChat", () => { + const setup = (props: Partial<AiChatProps> = {}) => { + const initialMessages: AiChatProps["initialMessages"] = [ + { role: "assistant", content: faker.lorem.sentence() }, + ] + const conversationStarters: AiChatProps["conversationStarters"] = [ + { content: faker.lorem.sentence() }, + { content: faker.lorem.sentence() }, + ] + render( + <AiChat + initialMessages={initialMessages} + conversationStarters={conversationStarters} + requestOpts={{ apiUrl: "http://localhost:4567/test" }} + {...props} + />, + { wrapper: ThemeProvider }, + ) + + return { initialMessages, conversationStarters } + } + + test("Clicking conversation starters and sending chats", async () => { + const { initialMessages, conversationStarters } = setup() + + const scrollBy = jest.spyOn(HTMLElement.prototype, "scrollBy") + + const initialMessageEls = getMessages() + expect(initialMessageEls.length).toBe(1) + expect(initialMessageEls[0]).toHaveTextContent(initialMessages[0].content) + + const starterEls = getConversationStarters() + expect(starterEls.length).toBe(2) + expect(starterEls[0]).toHaveTextContent(conversationStarters[0].content) + expect(starterEls[1]).toHaveTextContent(conversationStarters[1].content) + + const chosen = faker.helpers.arrayElement([0, 1]) + + await user.click(starterEls[chosen]) + expect(scrollBy).toHaveBeenCalled() + scrollBy.mockReset() + + const messageEls = await whenCount(getMessages, 3) + + expect(messageEls[0]).toHaveTextContent(initialMessages[0].content) + expect(messageEls[1]).toHaveTextContent( + conversationStarters[chosen].content, + ) + expect(messageEls[2]).toHaveTextContent("AI Response 0") + + await user.click(screen.getByPlaceholderText("Type a message...")) + await user.paste("User message") + await user.click(screen.getByRole("button", { name: "Send" })) + expect(scrollBy).toHaveBeenCalled() + + const afterSending = await whenCount(getMessages, 5) + expect(afterSending[3]).toHaveTextContent("User message") + expect(afterSending[4]).toHaveTextContent("AI Response 1") + }) + + test("transformBody is called before sending requests", async () => { + const fakeBody = { message: faker.lorem.sentence() } + const apiUrl = faker.internet.url() + const transformBody = jest.fn(() => fakeBody) + const { initialMessages } = setup({ + requestOpts: { apiUrl, transformBody }, + }) + + await user.click(screen.getByPlaceholderText("Type a message...")) + await user.paste("User message") + await user.click(screen.getByRole("button", { name: "Send" })) + + expect(transformBody).toHaveBeenCalledWith([ + expect.objectContaining(initialMessages[0]), + expect.objectContaining({ content: "User message", role: "user" }), + ]) + expect(mockFetch).toHaveBeenCalledTimes(1) + expect(mockFetch).toHaveBeenCalledWith( + apiUrl, + expect.objectContaining({ + body: JSON.stringify(fakeBody), + }), + ) + }) + + test("parseContent is called on the API-received message content", async () => { + const fakeBody = { message: faker.lorem.sentence() } + const apiUrl = faker.internet.url() + const transformBody = jest.fn(() => fakeBody) + const { initialMessages, conversationStarters } = setup({ + requestOpts: { apiUrl, transformBody }, + parseContent: jest.fn((content) => `Parsed: ${content}`), + }) + + await user.click(getConversationStarters()[0]) + + await whenCount(getMessages, initialMessages.length + 2) + + await user.click(screen.getByPlaceholderText("Type a message...")) + await user.paste("User message") + await user.click(screen.getByRole("button", { name: "Send" })) + + await whenCount(getMessages, initialMessages.length + 4) + + const messagesTexts = getMessages().map((el) => el.textContent) + expect(messagesTexts).toEqual([ + initialMessages[0].content, + conversationStarters[0].content, + "Parsed: AI Response 0", + "User message", + "Parsed: AI Response 1", + ]) + }) +}) diff --git a/src/components/AiChat/AiChat.tsx b/src/components/AiChat/AiChat.tsx new file mode 100644 index 0000000..0a7ae64 --- /dev/null +++ b/src/components/AiChat/AiChat.tsx @@ -0,0 +1,251 @@ +import * as React from "react" +import styled from "@emotion/styled" +import Skeleton from "@mui/material/Skeleton" +import { Input } from "../Input/Input" +import { ActionButton } from "../Button/ActionButton" +import { RiSendPlaneFill } from "@remixicon/react" +import { useAiChat } from "./utils" +import Markdown from "react-markdown" + +import type { AiChatProps } from "./types" +import { ScrollSnap } from "../ScrollSnap/ScrollSnap" +import classNames from "classnames" +import { SrAnnouncer } from "../SrAnnouncer/SrAnnouncer" + +const ChatContainer = styled.div(({ theme }) => ({ + width: "100%", + height: "100%", + border: `1px solid ${theme.custom.colors.silverGrayLight}`, + backgroundColor: theme.custom.colors.lightGray1, + display: "flex", + flexDirection: "column", +})) + +const MessagesContainer = styled(ScrollSnap)({ + display: "flex", + flexDirection: "column", + flex: 1, + padding: "24px", + paddingBottom: "0px", + overflow: "auto", +}) +const MessageRow = styled.div<{ + reverse?: boolean +}>(({ reverse }) => [ + { + margin: "8px 0", + display: "flex", + width: "100%", + flexDirection: reverse ? "row-reverse" : "row", + }, +]) +const Avatar = styled.div({}) +const Message = styled.div(({ theme }) => ({ + border: `1px solid ${theme.custom.colors.silverGrayLight}`, + backgroundColor: theme.custom.colors.white, + borderRadius: "24px", + padding: "4px 16px", + ...theme.typography.body2, + "p:first-of-type": { + marginTop: 0, + }, + "p:last-of-type": { + marginBottom: 0, + }, + a: { + color: theme.custom.colors.mitRed, + textDecoration: "none", + }, + "a:hover": { + color: theme.custom.colors.red, + textDecoration: "underline", + }, +})) + +const StarterContainer = styled.div({ + alignSelf: "flex-end", + display: "flex", + flexDirection: "column", + gap: "4px", +}) +const Starter = styled.button(({ theme }) => ({ + border: `1px solid ${theme.custom.colors.silverGrayLight}`, + backgroundColor: theme.custom.colors.white, + borderRadius: "24px", + padding: "4px 16px", + ...theme.typography.body2, + cursor: "pointer", + "&:hover": { + backgroundColor: theme.custom.colors.lightGray1, + }, +})) + +const Controls = styled.div(({ theme }) => ({ + display: "flex", + justifyContent: "space-around", + padding: "12px 24px", + backgroundColor: theme.custom.colors.white, +})) +const Form = styled.form(() => ({ + display: "flex", + width: "80%", + gap: "12px", + alignItems: "center", +})) + +const DotsContainer = styled.span(({ theme }) => ({ + display: "inline-flex", + gap: "4px", + ".MuiSkeleton-root": { + backgroundColor: theme.custom.colors.silverGray, + }, +})) +const Dots = () => { + return ( + <DotsContainer> + <Skeleton variant="circular" width="8px" height="8px" /> + <Skeleton variant="circular" width="8px" height="8px" /> + <Skeleton variant="circular" width="8px" height="8px" /> + </DotsContainer> + ) +} + +const classes = { + root: "MitAiChat--root", + conversationStarter: "MitAiChat--conversationStarter", + messagesContainer: "MitAiChat--messagesContainer", + messageRow: "MitAiChat--messageRow", + message: "MitAiChat--message", + avatar: "MitAiChat--avatar", + input: "MitAiChat--input", +} + +const AiChat: React.FC<AiChatProps> = function AiChat({ + className, + conversationStarters, + requestOpts, + initialMessages: initMsgs, + parseContent, + srLoadingMessages, +}) { + const [showStarters, setShowStarters] = React.useState(true) + const messagesRef = React.useRef<HTMLDivElement>(null) + const initialMessages = React.useMemo(() => { + const prefix = Math.random().toString().slice(2) + return initMsgs.map((m, i) => ({ ...m, id: `initial-${prefix}-${i}` })) + }, [initMsgs]) + const { + messages: unparsed, + input, + handleInputChange, + handleSubmit, + append, + isLoading, + } = useAiChat(requestOpts, { + initialMessages: initialMessages, + }) + + const messages = React.useMemo(() => { + const initial = initialMessages.map((m) => m.id) + return unparsed.map((m) => { + if (m.role === "assistant" && !initial.includes(m.id)) { + const content = parseContent ? parseContent(m.content) : m.content + return { ...m, content } + } + return m + }) + }, [parseContent, unparsed, initialMessages]) + + const waiting = + !showStarters && messages[messages.length - 1]?.role === "user" + + const scrollToBottom = () => { + messagesRef.current?.scrollBy({ + behavior: "instant", + top: messagesRef.current.scrollHeight, + }) + } + + const lastMsg = messages[messages.length - 1] + + return ( + <ChatContainer className={classNames(className, classes.root)}> + <MessagesContainer + className={classes.messagesContainer} + ref={messagesRef} + > + {messages.map((m) => ( + <MessageRow + key={m.id} + reverse={m.role === "user"} + data-chat-role={m.role} + className={classes.messageRow} + > + <Avatar /> + <Message className={classes.message}> + <Markdown>{m.content}</Markdown> + </Message> + </MessageRow> + ))} + {showStarters ? ( + <StarterContainer> + {conversationStarters?.map((m) => ( + <Starter + className={classes.conversationStarter} + key={m.content} + onClick={() => { + setShowStarters(false) + scrollToBottom() + append({ role: "user", content: m.content }) + }} + > + {m.content} + </Starter> + ))} + </StarterContainer> + ) : null} + {waiting ? ( + <MessageRow key={"loading"}> + <Avatar className={classes.avatar} /> + <Message> + <Dots /> + </Message> + </MessageRow> + ) : null} + </MessagesContainer> + <Controls> + <Form + onSubmit={(e) => { + setShowStarters(false) + scrollToBottom() + handleSubmit(e) + }} + > + <Input + className={classes.input} + placeholder="Type a message..." + name="message" + sx={{ flex: 1 }} + value={input} + onChange={handleInputChange} + /> + <ActionButton + aria-label="Send" + type="submit" + disabled={isLoading || !input} + > + <RiSendPlaneFill /> + </ActionButton> + </Form> + </Controls> + <SrAnnouncer + isLoading={isLoading} + loadingMessages={srLoadingMessages} + message={lastMsg.role === "assistant" ? lastMsg.content : ""} + /> + </ChatContainer> + ) +} + +export { AiChat } +export type { AiChatProps } diff --git a/src/components/AiChat/story-utils.ts b/src/components/AiChat/story-utils.ts new file mode 100644 index 0000000..30dd6b5 --- /dev/null +++ b/src/components/AiChat/story-utils.ts @@ -0,0 +1,95 @@ +const SAMPLE_RESPONSES = [ + `For exploring AI applications in business, I recommend the following course from MIT: + +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. + +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. +`, + ` +To understand global warming, I recommend the following resources from MIT: + +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. + +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. + +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. +`, + ` +Here are some courses on linear algebra that you can explore: + +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. + +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. + +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. + +These courses provide a comprehensive introduction to linear algebra and its applications across various fields. +`, +] + +const rand = (min: number, max: number) => { + // min and max included + return Math.floor(Math.random() * (max - min + 1) + min) +} + +const mockStreaming = async function mockApi() { + let timerId: NodeJS.Timeout + const response = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)] + const chunks: string[] = response.split(" ").reduce((acc, word) => { + const last = acc[acc.length - 1] + if (acc.length === 0) { + acc.push(word) + } else if (Math.random() < 0.75) { + acc[acc.length - 1] = `${last} ${word}` + } else { + acc.push(` ${word}`) + } + return acc + }, [] as string[]) + + const num = chunks.length + let i = 0 + + await new Promise((resolve) => setTimeout(resolve, 1500)) + + const body = new ReadableStream({ + start(controller) { + timerId = setInterval(() => { + const msg = new TextEncoder().encode(chunks[i]) + controller.enqueue(msg) + i++ + if (i === num) { + controller.close() + clearInterval(timerId) + } + }, 250) + }, + cancel() { + if (timerId) { + clearInterval(timerId) + } + }, + }) + + return Promise.resolve( + new Response(body, { + headers: { + "Content-Type": "text/event-stream", + }, + }), + ) +} + +const mockJson = async () => { + const message = SAMPLE_RESPONSES[rand(0, SAMPLE_RESPONSES.length - 1)] + await new Promise((res) => setTimeout(res, 2000)) + return Promise.resolve( + new Response(JSON.stringify({ message }), { + headers: { + "Content-Type": "application/json", + }, + }), + ) +} + +export { mockStreaming, mockJson } diff --git a/src/components/AiChat/types.ts b/src/components/AiChat/types.ts new file mode 100644 index 0000000..2117c8d --- /dev/null +++ b/src/components/AiChat/types.ts @@ -0,0 +1,48 @@ +// Some of these are based on (compatible, but simplfied / restricted) versions of ai/react types. + +type Role = "assistant" | "user" +type ChatMessage = { + id: string + content: string + role: Role +} + +type RequestOpts = { + apiUrl: string + /** + * Transforms array of chat messages into request body. Messages + * are ordered oldest to newest. + * + * JSON.stringify is applied to the return value. + */ + transformBody?: (messages: ChatMessage[]) => unknown + /** + * Extra options to pass to fetch. + * + * If headers are specified, they will override the headersOpts. + */ + fetchOpts?: RequestInit + /** + * Extra headers to pass to fetch. + */ + headersOpts?: HeadersInit +} + +type AiChatProps = { + className?: string + initialMessages: Omit<ChatMessage, "id">[] + conversationStarters?: { content: string }[] + requestOpts: RequestOpts + parseContent?: (content: unknown) => string + /** + * A message to display while the component is in a loading state. + * + * Identical consecutive messages may not be read on some screen readers. + */ + srLoadingMessages?: { + delay: number + text: string + }[] +} + +export type { RequestOpts, AiChatProps, ChatMessage } diff --git a/src/components/AiChat/utils.ts b/src/components/AiChat/utils.ts new file mode 100644 index 0000000..ef1fdd9 --- /dev/null +++ b/src/components/AiChat/utils.ts @@ -0,0 +1,39 @@ +import { useChat, UseChatOptions } from "ai/react" +import type { RequestOpts, ChatMessage } from "./types" +import { useMemo } from "react" + +const identity = <T>(x: T): T => x + +const getFetcher: (requestOpts: RequestOpts) => typeof fetch = + (requestOpts: RequestOpts) => async (url, opts) => { + if (typeof opts?.body !== "string") { + console.error("Unexpected body type.") + return window.fetch(url, opts) + } + const messages: ChatMessage[] = JSON.parse(opts?.body).messages + const transformBody: RequestOpts["transformBody"] = + requestOpts.transformBody ?? identity + const options: RequestInit = { + ...opts, + body: JSON.stringify(transformBody(messages)), + headers: { + ...opts?.headers, + "Content-Type": "application/json", + ...requestOpts.headersOpts, + }, + ...requestOpts.fetchOpts, + } + return fetch(url, options) + } + +const useAiChat = (requestOpts: RequestOpts, opts: UseChatOptions) => { + const fetcher = useMemo(() => getFetcher(requestOpts), [requestOpts]) + return useChat({ + api: requestOpts.apiUrl, + streamProtocol: "text", + fetch: fetcher, + ...opts, + }) +} + +export { useAiChat } diff --git a/src/components/ScrollSnap/ScrollSnap.stories.tsx b/src/components/ScrollSnap/ScrollSnap.stories.tsx new file mode 100644 index 0000000..fd5f3e4 --- /dev/null +++ b/src/components/ScrollSnap/ScrollSnap.stories.tsx @@ -0,0 +1,61 @@ +import * as React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import { ScrollSnap } from "./ScrollSnap" +import styled from "@emotion/styled" +import { faker } from "@faker-js/faker/locale/en" +import { useInterval } from "@/utils/useInterval" +import Slider from "@mui/material/Slider" +import Stack from "@mui/material/Stack" +import Typography from "@mui/material/Typography" +import { Button } from "../Button/Button" + +const Scroller = styled(ScrollSnap)({ + width: "200px", + height: "350px", + border: "1pt solid black", +}) + +const meta: Meta<typeof ScrollSnap> = { + title: "smoot-design/ScrollSnap", + component: ScrollSnap, + render: function Render(args) { + const MAX = 3000 + const [updateInterval, setUpdateInterval] = React.useState(MAX) + const [children, setChildren] = React.useState(faker.lorem.sentence()) + const appendText = () => + setChildren((current) => { + return `${current} ${faker.lorem.sentence()}` + }) + useInterval( + () => { + appendText() + }, + updateInterval === MAX ? null : updateInterval, + ) + return ( + <Stack gap="12px" alignItems="start"> + <Typography>Update interval (ms)</Typography> + <Slider + sx={{ width: "350px" }} + marks={[ + { value: 100, label: "0.1s" }, + { value: 2000, label: "2s" }, + { value: MAX, label: "off" }, + ]} + value={updateInterval} + min={100} + max={MAX} + onChange={(_e, val) => setUpdateInterval(val as number)} + /> + <Scroller {...args}>{children}</Scroller> + <Button onClick={appendText}>Append Sentence</Button> + </Stack> + ) + }, +} + +export default meta + +type Story = StoryObj<typeof ScrollSnap> + +export const Chat: Story = {} diff --git a/src/components/ScrollSnap/ScrollSnap.tsx b/src/components/ScrollSnap/ScrollSnap.tsx new file mode 100644 index 0000000..bda482f --- /dev/null +++ b/src/components/ScrollSnap/ScrollSnap.tsx @@ -0,0 +1,79 @@ +import { composeRefs } from "@/utils/composeRefs" +import styled from "@emotion/styled" +import * as React from "react" + +/** + * Returns the distance between visible content and the bottom of the element. + */ +const distanceFromBottom = (el: HTMLElement) => { + return el.scrollHeight - el.clientHeight - el.scrollTop +} + +/** + * Scrolls to the bottom of the element. + */ +const scrollToBottom = (el: HTMLElement) => { + el.scrollTop = el.scrollHeight +} + +const Scroller = styled.div({ + overflow: "auto", +}) + +type ScrollSnapProps = { + /** + * Tolerance within which scroll will be considered "at the bottom" of the element. + */ + threshold?: number + /** + * Content to be displayed + */ + children: React.ReactNode + className?: string +} + +/** + * Component that automatically scrolls to the bottom of the element when new + * content is added, unless the user has scrolled up. + */ +const ScrollSnap = React.forwardRef<HTMLDivElement, ScrollSnapProps>( + function ScrollSnap({ children, threshold = 2, className }, ref) { + const el = React.useRef<HTMLDivElement>() + + // `content` a delayed version of children to allow measuring scroll position + // using the old children. + const [content, setContent] = React.useState(children) + const wasAtBottom = React.useRef<boolean | null>(null) + + /** + * The next two effects: + * 1. Check if the element is at the bottom. + * 2. Then set children -> content + * 3. Then scroll to bottom (if needed) + * + * In this way, we can measure the scroll position before the new content is set. + */ + React.useEffect(() => { + if (!el.current) return + wasAtBottom.current = distanceFromBottom(el.current) < threshold + setContent(children) + }, [children, threshold]) + React.useEffect(() => { + if (!el.current) return + const atBottom = distanceFromBottom(el.current) < threshold + if (wasAtBottom.current && !atBottom) { + scrollToBottom(el.current) + wasAtBottom.current = null + } + }, [content, threshold]) + + return ( + <Scroller className={className} ref={composeRefs(el, ref)}> + {content} + </Scroller> + ) + }, +) + +export { ScrollSnap } +export type { ScrollSnapProps } diff --git a/src/components/SrAnnouncer/SrAnnouncer.stories.tsx b/src/components/SrAnnouncer/SrAnnouncer.stories.tsx new file mode 100644 index 0000000..2557cba --- /dev/null +++ b/src/components/SrAnnouncer/SrAnnouncer.stories.tsx @@ -0,0 +1,57 @@ +import * as React from "react" +import type { Meta, StoryObj } from "@storybook/react" +import { SrAnnouncer } from "./SrAnnouncer" +import styled from "@emotion/styled" + +const Container = styled.div<{ forceVisible?: boolean }>(({ forceVisible }) => [ + forceVisible && { + width: "100% !important", + height: "100px !important", + "& > *:first-of-type": { + width: "unset !important", + height: "unset !important", + clipPath: "none !important", + clip: "unset !important", + position: "unset !important" as "unset", + }, + }, +]) + +const meta: Meta<typeof SrAnnouncer> = { + title: "smoot-design/ScreenreaderAnnouncer", + component: SrAnnouncer, + decorators: function Decorator(Story) { + const [forceVisible, setForceVisible] = React.useState(true) + return ( + <> + <label> + Force Visible: + <input + type="checkbox" + checked={forceVisible} + onChange={(e) => setForceVisible(e.target.checked)} + /> + <p>By default, the content of this story is visually hidden.</p> + </label> + <hr /> + <Container forceVisible={forceVisible}> + <Story /> + </Container> + </> + ) + }, + args: { + message: "A message to read to user", + isLoading: true, + loadingMessages: [ + { delay: 1000, text: "Loading" }, + { delay: 3000, text: "Still loading" }, + ], + }, +} + +export default meta + +type Story = StoryObj<typeof SrAnnouncer> + +export const ScreenreaderAnnouncements: Story = {} diff --git a/src/components/SrAnnouncer/SrAnnouncer.test.tsx b/src/components/SrAnnouncer/SrAnnouncer.test.tsx new file mode 100644 index 0000000..96efee8 --- /dev/null +++ b/src/components/SrAnnouncer/SrAnnouncer.test.tsx @@ -0,0 +1,91 @@ +import * as React from "react" +import { render, act } from "@testing-library/react" +import { SrAnnouncer } from "./SrAnnouncer" + +const sleep = (ms: number) => { + act(() => { + jest.advanceTimersByTime(ms) + }) +} + +describe("SrAnnouncer", () => { + beforeEach(() => { + jest.useFakeTimers() + jest.clearAllTimers() + }) + + test("Renders a message when not loading", () => { + const { container } = render( + <SrAnnouncer message="Hello, world!" isLoading={false} />, + ) + expect(container.textContent).toBe("Hello, world!") + }) + + test("Renders a loading message when loading", async () => { + const loadingMessages = [ + { delay: 100, text: "Loading 1" }, + { delay: 200, text: "Loading 2" }, + ] + const { container, rerender } = render( + <SrAnnouncer + message="Hello, world!" + loadingMessages={loadingMessages} + isLoading={true} + />, + ) + + expect(container.textContent).toBe("") + + sleep(100) + expect(container.textContent).toBe("Loading 1") + sleep(100) + expect(container.textContent).toBe("Loading 1") + sleep(100) + expect(container.textContent).toBe("Loading 2") + sleep(1000) + expect(container.textContent).toBe("Loading 2") + + rerender( + <SrAnnouncer + message="Hello, world!" + loadingMessages={loadingMessages} + isLoading={false} + />, + ) + expect(container.textContent).toBe("Hello, world!") + + rerender( + <SrAnnouncer + message="Hello, world!" + loadingMessages={loadingMessages} + isLoading={true} + />, + ) + + expect(container.textContent).toBe("") + sleep(100) + expect(container.textContent).toBe("Loading 1") + sleep(100) + expect(container.textContent).toBe("Loading 1") + sleep(100) + expect(container.textContent).toBe("Loading 2") + }) + + test("Warns if loadingMessages changes unexpectedly", () => { + const error = jest.spyOn(console, "error").mockImplementation(() => {}) + const { rerender } = render( + <SrAnnouncer message="Hello, world!" isLoading={true} />, + ) + + rerender( + <SrAnnouncer + message="Hello, world!" + isLoading={true} + loadingMessages={[{ delay: 100, text: "Loading" }]} + />, + ) + + expect(error).toHaveBeenCalled() + error.mockRestore() + }) +}) diff --git a/src/components/SrAnnouncer/SrAnnouncer.tsx b/src/components/SrAnnouncer/SrAnnouncer.tsx new file mode 100644 index 0000000..7ce15fe --- /dev/null +++ b/src/components/SrAnnouncer/SrAnnouncer.tsx @@ -0,0 +1,77 @@ +import * as React from "react" +import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden" +import { useEffect } from "react" +import { useDevCheckStable } from "@/utils/useDevCheckStable" + +type SrAnnouncerProps = { + /** + * Message text to be read to user. + * + * Cannot contain HTML elements—only text. + */ + message: string + /** + * Messages to display while the component is in a loading state. + * + * Identical consecutive messages may not be read on some screen readers. + */ + loadingMessages?: { + delay: number + text: string + }[] + isLoading: boolean +} + +const DEFAULT_PROPS: Pick<Required<SrAnnouncerProps>, "loadingMessages"> = { + loadingMessages: [ + { delay: 1500, text: "Loading" }, + { delay: 4000, text: "Still loading" }, + ], +} + +/** + * A component that announces messages to screen readers as they come in. + */ +const SrAnnouncer: React.FC<SrAnnouncerProps> = ({ + message, + isLoading, + loadingMessages = DEFAULT_PROPS.loadingMessages, +}) => { + const [loadingMsgIndex, setLoadingMsgIndex] = React.useState(-1) + + /** + * If loadingMessages changes, the timeouts are reset. + * Desirable if the change is real, undesirable if it's a mistake (e.g., by + * passing an array literal as a prop). + */ + useDevCheckStable( + loadingMessages, + "SrAnnouncer: loadingMessages changed (by ===) unexpectedly. This may interfere with loading message visibility", + ) + + useEffect(() => { + setLoadingMsgIndex(-1) + }, [isLoading, loadingMessages]) + + useEffect(() => { + const next = loadingMessages[loadingMsgIndex + 1] + if (!isLoading || !next) return () => {} + const id = setTimeout(() => { + setLoadingMsgIndex(loadingMsgIndex + 1) + }, next.delay) + return () => { + clearTimeout(id) + } + }, [isLoading, loadingMsgIndex, loadingMessages]) + + const loadingTxt: string | undefined = loadingMessages[loadingMsgIndex]?.text + + return ( + <VisuallyHidden aria-atomic="true" aria-live="polite"> + {isLoading ? loadingTxt : message} + </VisuallyHidden> + ) +} + +export { SrAnnouncer } +export type { SrAnnouncerProps } diff --git a/src/components/VisuallyHidden/VisuallyHidden.stories.tsx b/src/components/VisuallyHidden/VisuallyHidden.stories.tsx new file mode 100644 index 0000000..fb85197 --- /dev/null +++ b/src/components/VisuallyHidden/VisuallyHidden.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from "@storybook/react" +import { VisuallyHidden } from "./VisuallyHidden" + +const meta: Meta<typeof VisuallyHidden> = { + title: "smoot-design/VisuallyHidden", + component: VisuallyHidden, + args: { + children: "Not visible, but screen readers can still read this text.", + }, +} + +export default meta + +type Story = StoryObj<typeof VisuallyHidden> + +export const ScreenreaderOnly: Story = {} diff --git a/src/components/VisuallyHidden/VisuallyHidden.tsx b/src/components/VisuallyHidden/VisuallyHidden.tsx new file mode 100644 index 0000000..cecd502 --- /dev/null +++ b/src/components/VisuallyHidden/VisuallyHidden.tsx @@ -0,0 +1,32 @@ +import styled from "@emotion/styled" + +/** + * VisuallyHidden is a utility component that hides its children from sighted + * users, but keeps them accessible to screen readers. + * + * Often, screenreader-only content can be handled with an `aria-label`. However, + * occasionally we need actual elements. + * + * Example: + * - a visually hidden aria-live section that reads announcements that + * isual users can ascertain in some other way. + * - a visually hidden Heading for a section whose purpose is clear for sighted users + * - a visually hidden description used for aria-describeddby + * - There is an aria-description attribute that can be used to provide a + * without an actual element on the page. However, it is introduced in + * ARIA 1.3 (working draft), not compatible with some screen readers, and + * flagged problematic by our linting. + * + * The CSS here is based on https://inclusive-components.design/tooltips-toggletips/ + */ +const VisuallyHidden = styled.span({ + clipPath: "inset(100%)", + clip: "rect(1px, 1px, 1px, 1px)", + height: "1px", + overflow: "hidden", + position: "absolute", + whiteSpace: "nowrap", + width: "1px", +}) + +export { VisuallyHidden } diff --git a/src/index.ts b/src/index.ts index 0675276..f413eed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,3 +28,7 @@ export { Input } from "./components/Input/Input" export type { InputProps } from "./components/Input/Input" export { TextField } from "./components/TextField/TextField" export type { TextFieldProps } from "./components/TextField/TextField" + +export { SrAnnouncer } from "./components/SrAnnouncer/SrAnnouncer" +export type { SrAnnouncerProps } from "./components/SrAnnouncer/SrAnnouncer" +export { VisuallyHidden } from "./components/VisuallyHidden/VisuallyHidden" diff --git a/src/jest-setup.ts b/src/jest-setup.ts index df6631e..799e1f1 100644 --- a/src/jest-setup.ts +++ b/src/jest-setup.ts @@ -1 +1,19 @@ import "@testing-library/jest-dom" +// eslint-disable-next-line @typescript-eslint/no-require-imports +const failOnConsole = require("jest-fail-on-console") + +failOnConsole() + +beforeAll(() => { + const scrollBy = jest.fn() + HTMLElement.prototype.scrollBy = scrollBy +}) + +afterEach(() => { + /** + * Clear all mock call counts between tests. + * This does NOT clear mock implementations. + * Mock implementations are always cleared between test files. + */ + jest.clearAllMocks() +}) diff --git a/src/jsdom-extended.ts b/src/jsdom-extended.ts new file mode 100644 index 0000000..433ab6d --- /dev/null +++ b/src/jsdom-extended.ts @@ -0,0 +1,16 @@ +// Jest environment extended with Web APIs para tranbajar con MSW +import { TestEnvironment } from "jest-environment-jsdom" +import { EnvironmentContext, JestEnvironmentConfig } from "@jest/environment" + +class JSDOMEnvironmentExtended extends TestEnvironment { + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { + super(config, context) + + this.global.TransformStream = TransformStream + this.global.ReadableStream = ReadableStream + this.global.Response = Response + this.global.TextDecoderStream = TextDecoderStream + } +} + +export default JSDOMEnvironmentExtended diff --git a/src/story-utils/index.ts b/src/story-utils/index.ts index 3024e4c..b540788 100644 --- a/src/story-utils/index.ts +++ b/src/story-utils/index.ts @@ -7,4 +7,11 @@ const enumValues = <T extends string | undefined>( return Object.keys(obj) as NonNullable<T>[] } -export { enumValues } +const gitLink = (filepath: string) => { + if (!filepath.startsWith("src/")) { + throw new Error(`Invalid filepath: ${filepath}\nShould start with "src/"`) + } + return `https://github.com/mitodl/smoot-design/blob/${process.env.STORYBOOK_GIT_SHA}/${filepath}` +} + +export { enumValues, gitLink } diff --git a/src/utils/composeRefs.test.tsx b/src/utils/composeRefs.test.tsx new file mode 100644 index 0000000..22ec337 --- /dev/null +++ b/src/utils/composeRefs.test.tsx @@ -0,0 +1,25 @@ +import * as React from "react" +import { composeRefs } from "./composeRefs" +import { render, screen } from "@testing-library/react" + +describe("composeRefs", () => { + test("Composing object + fn ref", () => { + const objRef1: React.RefObject<HTMLDivElement> = { current: null } + const objRef2: React.RefObject<HTMLDivElement> = { current: null } + const fnRef1 = jest.fn() + const fnRef2 = jest.fn() + + render( + <div + data-testid="my-div" + ref={composeRefs(objRef1, objRef2, fnRef1, fnRef2)} + />, + ) + + const el = screen.getByTestId("my-div") + expect(objRef1.current).toBe(el) + expect(objRef2.current).toBe(el) + expect(fnRef1).toHaveBeenCalledWith(el) + expect(fnRef2).toHaveBeenCalledWith(el) + }) +}) diff --git a/src/utils/composeRefs.ts b/src/utils/composeRefs.ts new file mode 100644 index 0000000..dd3de7a --- /dev/null +++ b/src/utils/composeRefs.ts @@ -0,0 +1,25 @@ +import type { MutableRefObject, ForwardedRef, RefCallback } from "react" + +/** + * Compose 2+ refs. Useful when a reusable component needs a ref itself, but + * consumers may also need the ref. + */ +const composeRefs = <T>( + ...refs: ( + | ForwardedRef<T> + | MutableRefObject<T | undefined> + | RefCallback<T> + )[] +): RefCallback<T> => { + return (value) => { + refs.forEach((ref) => { + if (typeof ref === "function") { + ref(value) + } else if (ref) { + ref.current = value + } + }) + } +} + +export { composeRefs } diff --git a/src/utils/useDevCheckStable.ts b/src/utils/useDevCheckStable.ts new file mode 100644 index 0000000..0c6df18 --- /dev/null +++ b/src/utils/useDevCheckStable.ts @@ -0,0 +1,36 @@ +import { useEffect, useRef } from "react" + +/** + * Emits `console.error` if two subsequent values of `jsonSerializable` serialize + * to the same thing but are different references. + * + * This hook does NOT run in production. + */ +const useDevCheckStable = ( + jsonSerializable: unknown, + msg = "The value has changed. This may cause unnecessary re-renders", +) => { + if (process.env.NODE_ENV !== "production") { + /** + * Calling hooks conditionally based on env vars is not really a problem. + * In a given environment, the hook will always run or always not run. + */ + // eslint-disable-next-line react-hooks/rules-of-hooks + const valRef = useRef(jsonSerializable) + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + const sameJson = + JSON.stringify(valRef.current) === JSON.stringify(jsonSerializable) + const differentRefs = valRef.current !== jsonSerializable + if (!sameJson || differentRefs) { + console.error( + `useDevCheckStable: ${msg}`, + valRef.current, + jsonSerializable, + ) + } + }, [jsonSerializable, msg]) + } +} + +export { useDevCheckStable } diff --git a/src/utils/useInterval.ts b/src/utils/useInterval.ts new file mode 100644 index 0000000..4a1d1f7 --- /dev/null +++ b/src/utils/useInterval.ts @@ -0,0 +1,25 @@ +import { useRef, useEffect } from "react" + +/** + * Calls a function at a specified interval. + * + * Based on https://overreacted.io/making-setinterval-declarative-with-react-hooks/ + */ +const useInterval = (callback: () => void, delay: number | null) => { + const savedCallback = useRef<() => void>() + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + if (delay !== null) { + const id = setInterval(() => savedCallback.current?.(), delay) + return () => clearInterval(id) + } else { + return () => {} + } + }, [delay]) +} + +export { useInterval } diff --git a/yarn.lock b/yarn.lock index e8c6cc8..0c30626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,68 @@ __metadata: languageName: node linkType: hard +"@ai-sdk/provider-utils@npm:2.0.3": + version: 2.0.3 + resolution: "@ai-sdk/provider-utils@npm:2.0.3" + dependencies: + "@ai-sdk/provider": "npm:1.0.1" + eventsource-parser: "npm:^3.0.0" + nanoid: "npm:^3.3.7" + secure-json-parse: "npm:^2.7.0" + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + checksum: 10/16f1876f0884ccd6ccd2b0de63bac16f89d19122d74d1ec3db1169cfadf0d5ddbae347ff8944648efa8899653ff722451ea36f0894dbf995af5c1a5ea39d19dc + languageName: node + linkType: hard + +"@ai-sdk/provider@npm:1.0.1": + version: 1.0.1 + resolution: "@ai-sdk/provider@npm:1.0.1" + dependencies: + json-schema: "npm:^0.4.0" + checksum: 10/bdcffb0475a021e17065f9161fcf0d171490c23a27162f400b57e0dfcd895e2bf25f94faff044e1faefca0811e3d1f5ca3209d577485f2f27bd21491531d57fc + languageName: node + linkType: hard + +"@ai-sdk/react@npm:1.0.5": + version: 1.0.5 + resolution: "@ai-sdk/react@npm:1.0.5" + dependencies: + "@ai-sdk/provider-utils": "npm:2.0.3" + "@ai-sdk/ui-utils": "npm:1.0.4" + swr: "npm:^2.2.5" + throttleit: "npm:2.1.0" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + zod: + optional: true + checksum: 10/58be5694c75d862b485a561f62fe915b9ba57b14aaa73fd0d6d9ecbf097ceb325475f69cef741bfd706e281a8aef97f5510b3ed106e6358055707a5803d7e910 + languageName: node + linkType: hard + +"@ai-sdk/ui-utils@npm:1.0.4": + version: 1.0.4 + resolution: "@ai-sdk/ui-utils@npm:1.0.4" + dependencies: + "@ai-sdk/provider": "npm:1.0.1" + "@ai-sdk/provider-utils": "npm:2.0.3" + zod-to-json-schema: "npm:^3.23.5" + peerDependencies: + zod: ^3.0.0 + peerDependenciesMeta: + zod: + optional: true + checksum: 10/d9b13bc954fa917401eb9d8ca5a66ff695f000177584e64d9864ac03b951cdc6ad66103a0db34191a55cb4ee8fc7a61d03658cc3d07b989cd9c0eb40c677a00c + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" @@ -1396,7 +1458,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": +"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.24.4, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7": version: 7.26.0 resolution: "@babel/runtime@npm:7.26.0" dependencies: @@ -2491,25 +2553,26 @@ __metadata: "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@faker-js/faker": "npm:^9.0.0" + "@jest/environment": "npm:^29.7.0" "@mui/base": "npm:5.0.0-beta.66" "@mui/lab": "npm:6.0.0-beta.19" "@mui/material": "npm:^6.1.6" "@mui/material-nextjs": "npm:^6.1.6" "@mui/system": "npm:^6.1.6" "@remixicon/react": "npm:^4.2.0" - "@storybook/addon-actions": "npm:^8.4.2" - "@storybook/addon-essentials": "npm:^8.4.2" - "@storybook/addon-interactions": "npm:^8.4.2" - "@storybook/addon-links": "npm:^8.4.2" - "@storybook/addon-onboarding": "npm:^8.4.2" + "@storybook/addon-actions": "npm:^8.4.7" + "@storybook/addon-essentials": "npm:^8.4.7" + "@storybook/addon-interactions": "npm:^8.4.7" + "@storybook/addon-links": "npm:^8.4.7" + "@storybook/addon-onboarding": "npm:^8.4.7" "@storybook/addon-webpack5-compiler-swc": "npm:^1.0.5" - "@storybook/blocks": "npm:^8.4.2" - "@storybook/nextjs": "npm:^8.4.2" - "@storybook/preview-api": "npm:^8.4.2" - "@storybook/react": "npm:^8.4.2" - "@storybook/react-webpack5": "npm:^8.4.2" - "@storybook/test": "npm:^8.4.2" - "@storybook/types": "npm:^8.4.2" + "@storybook/blocks": "npm:^8.4.7" + "@storybook/nextjs": "npm:^8.4.7" + "@storybook/preview-api": "npm:^8.4.7" + "@storybook/react": "npm:^8.4.7" + "@storybook/react-webpack5": "npm:^8.4.7" + "@storybook/test": "npm:^8.4.7" + "@storybook/types": "npm:^8.4.7" "@swc/jest": "npm:^0.2.37" "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/react": "npm:^16.0.1" @@ -2519,6 +2582,7 @@ __metadata: "@types/react-dom": "npm:^18.3.0" "@typescript-eslint/eslint-plugin": "npm:^8.13.0" "@typescript-eslint/typescript-estree": "npm:^8.13.0" + ai: "npm:^4.0.13" classnames: "npm:^2.5.1" conventional-changelog-conventionalcommits: "npm:^8.0.0" eslint: "npm:8.57.1" @@ -2535,14 +2599,14 @@ __metadata: jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.5.0" jest-extended: "npm:^4.0.2" - jest-fail-on-console: "npm:^3.2.0" + jest-fail-on-console: "npm:^3.3.1" jest-watch-typeahead: "npm:^2.2.2" lodash: "npm:^4.17.21" - material-ui-popup-state: "npm:^5.1.0" next: "npm:^15.0.2" prettier: "npm:^3.3.3" react: "npm:18.3.1" react-dom: "npm:^18.3.1" + react-markdown: "npm:^9.0.1" react-router: "npm:^6.22.2" react-router-dom: "npm:^6.22.2" semantic-release: "npm:^24.2.0" @@ -2552,6 +2616,7 @@ __metadata: tsconfig-paths-webpack-plugin: "npm:^4.1.0" type-fest: "npm:^4.26.1" typescript: "npm:^5.6.3" + zod: "npm:^3.23.8" peerDependencies: react: "*" react-dom: "*" @@ -3391,6 +3456,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:1.9.0": + version: 1.9.0 + resolution: "@opentelemetry/api@npm:1.9.0" + checksum: 10/a607f0eef971893c4f2ee2a4c2069aade6ec3e84e2a1f5c2aac19f65c5d9eeea41aa72db917c1029faafdd71789a1a040bdc18f40d63690e22ccae5d7070f194 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -3755,9 +3827,9 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-actions@npm:8.4.2, @storybook/addon-actions@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-actions@npm:8.4.2" +"@storybook/addon-actions@npm:8.4.7, @storybook/addon-actions@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-actions@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" @@ -3765,169 +3837,169 @@ __metadata: polished: "npm:^4.2.2" uuid: "npm:^9.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/c00b213e42ea085a19162448b5c35d2439be7aa18425fc4c535e50b2cb187c3c93d6603e7c7727258e1b8abdb04d20d60eacf3bd0e2bec86a1a5df2c043bf3d7 + storybook: ^8.4.7 + checksum: 10/a691f172f2899bf97ee2d454948a53f94fde29038b1dfc8b1fd902cf0912f72b02f484f3ab4abd6df52237edbed2a7f430a6b7f1b6ba8ee2be1e357c586466bd languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-backgrounds@npm:8.4.2" +"@storybook/addon-backgrounds@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-backgrounds@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/56f2c8bcd5c32fd071baf14764b1f78dacfc25d12549686451043c8864c3e8ef56dc064ff91308dc4f37c842feb7a2993f0e29185e19194b6063bbadaafbd044 + storybook: ^8.4.7 + checksum: 10/504ecd09fcdd8bd8525233469df386944a7baff7c8aaeb737532987d27d113db4ded72e394cfcb6b00262602e9fd070cce801cffbb157be6242ee56e0491577c languageName: node linkType: hard -"@storybook/addon-controls@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-controls@npm:8.4.2" +"@storybook/addon-controls@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-controls@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" dequal: "npm:^2.0.2" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/ee23a051be689ac6d7b4d4a5e2cc5429dadfe556a4daa9d347f55bef0a6110dce4a5d68f7afe18c04817d4960f048b041eeb6cc9555031f620d78a52a19e4229 + storybook: ^8.4.7 + checksum: 10/29a0d760622cc09517416a5775d8ae7e937fe90ede9d9739a56cdec4bc52564c0d8de535040ed540df912c1c3c04c6f557bc78f792c8af07da91753972f9a512 languageName: node linkType: hard -"@storybook/addon-docs@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-docs@npm:8.4.2" +"@storybook/addon-docs@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-docs@npm:8.4.7" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.4.2" - "@storybook/csf-plugin": "npm:8.4.2" - "@storybook/react-dom-shim": "npm:8.4.2" + "@storybook/blocks": "npm:8.4.7" + "@storybook/csf-plugin": "npm:8.4.7" + "@storybook/react-dom-shim": "npm:8.4.7" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/06510b9894ac6b4d9324dec037e22b1fd882be8962fce4213a10746a2d23660a231373d70aa56f8763a5b65f2eb1a3e7e64f3228a687e36818768665e5f8e01e + storybook: ^8.4.7 + checksum: 10/d09fefeefb462a1b6c368e781f4abbb1dfdf0c58e6f9311bc8a2c320699e9e694153ebf3274f4fc54fb85953eb10ced6de11a848c718ffb38a0f59e1b1717220 languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-essentials@npm:8.4.2" - dependencies: - "@storybook/addon-actions": "npm:8.4.2" - "@storybook/addon-backgrounds": "npm:8.4.2" - "@storybook/addon-controls": "npm:8.4.2" - "@storybook/addon-docs": "npm:8.4.2" - "@storybook/addon-highlight": "npm:8.4.2" - "@storybook/addon-measure": "npm:8.4.2" - "@storybook/addon-outline": "npm:8.4.2" - "@storybook/addon-toolbars": "npm:8.4.2" - "@storybook/addon-viewport": "npm:8.4.2" +"@storybook/addon-essentials@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-essentials@npm:8.4.7" + dependencies: + "@storybook/addon-actions": "npm:8.4.7" + "@storybook/addon-backgrounds": "npm:8.4.7" + "@storybook/addon-controls": "npm:8.4.7" + "@storybook/addon-docs": "npm:8.4.7" + "@storybook/addon-highlight": "npm:8.4.7" + "@storybook/addon-measure": "npm:8.4.7" + "@storybook/addon-outline": "npm:8.4.7" + "@storybook/addon-toolbars": "npm:8.4.7" + "@storybook/addon-viewport": "npm:8.4.7" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/951da2192a63d985e9af2e1e803bbe8bd6d64a87f50644034a55eb8bdc2ad0844e87836437d5c494eff3e94c8eae894d5f28bbef4b9cb99ef7e5fad573e1980d + storybook: ^8.4.7 + checksum: 10/d8731c18935fbc130beee7236b4e80c1621c6964a4109741512b50f065cd8d322446f8ecd84b4120ad1ce2ea829d0d3b5b764cca19c1bd8b73fc77d04dc13f17 languageName: node linkType: hard -"@storybook/addon-highlight@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-highlight@npm:8.4.2" +"@storybook/addon-highlight@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-highlight@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/81bdd9d758aa2b2faaa19ae6e27b6367e8522db3e8f6f2c72a452079ab35abf220472ff7ee6f7ed105dcfa7c23a2300df4753f8b5b5850a706a4c60492567735 + storybook: ^8.4.7 + checksum: 10/2d77ce06eaf69445ed6d7c23a666e67576376d770f8fd33055fd35e33c248c2c78f6333461cb92aa21f45bbf06a1255f1977ec3d349fdef531416fc51da809be languageName: node linkType: hard -"@storybook/addon-interactions@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-interactions@npm:8.4.2" +"@storybook/addon-interactions@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-interactions@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.4.2" - "@storybook/test": "npm:8.4.2" + "@storybook/instrumenter": "npm:8.4.7" + "@storybook/test": "npm:8.4.7" polished: "npm:^4.2.2" ts-dedent: "npm:^2.2.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/a0ac4c473d5ce8a0cc2c56897faf3845ab3fa51c0d6ff217d009de9850033549ffe9653d7821284c7e1947efb9a38beb34bd4cf4788d009f3edc027a6f5b37eb + storybook: ^8.4.7 + checksum: 10/24d5c55eb7f320a002d54cc638a58f196d243b248df7735d68bba21e5b2b4cd0ba0369b78e7b67522ef741516b022e9e627db9a59476e0ea2da153736950d1bc languageName: node linkType: hard -"@storybook/addon-links@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-links@npm:8.4.2" +"@storybook/addon-links@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-links@npm:8.4.7" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 peerDependenciesMeta: react: optional: true - checksum: 10/f23a29ffe9c7d8eb16032c4abba6b325dfea010840f9f3aa8cf27ea6699631a64aa065a796bd685bd35c39dcfdee9a3db51a38bc0bed452ce2c5818b523820b5 + checksum: 10/3d64225348f1c72dec069551044c7781de03a4775acfefb8ebe2d0c1a6e0171692a1222e15191bccd57b76ca9a995032df14974b7a6271f7a9b283c90bff1a00 languageName: node linkType: hard -"@storybook/addon-measure@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-measure@npm:8.4.2" +"@storybook/addon-measure@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-measure@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.2 - checksum: 10/48a2f3eccee2504777ac9c0fe267d3f91785674bb9a106c2610fa57912f65b2c2d32de76d3c62fe88a9d95985099283f0e55ac17b7593dece6ab3fb1657085c1 + storybook: ^8.4.7 + checksum: 10/d7c39c6048add359aa43ae10a65dda738f9b893a1963a9485a5ac0337f2961495fbdcf3e3907c2f19e7fb5380089f16c57a54113ed097cbf915bfe7f8b756ede languageName: node linkType: hard -"@storybook/addon-onboarding@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-onboarding@npm:8.4.2" +"@storybook/addon-onboarding@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-onboarding@npm:8.4.7" dependencies: react-confetti: "npm:^6.1.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/9b7e70775c72b8112cc03a91edd4c06033f83143b1dcd37ba7ab23f8a7c1add3835153efd4d6337cc8a0cdcff0c34e8a1c0df0f9f32b64e86d3e5a8147826c6a + storybook: ^8.4.7 + checksum: 10/d5a87f8aae2519a1b2c0f1da93497f7bfe7987c154613895e079780dc80eccf9c9b8fece4a7485dea019eda1b99a17121b0955b797bad9fe4582aca0dc344390 languageName: node linkType: hard -"@storybook/addon-outline@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-outline@npm:8.4.2" +"@storybook/addon-outline@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-outline@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/b43804fbbca21ee8790c82cd32559de551e14bd010507d9c6d8eac2afe0849372a44701d91d2c4baba524bef9519193ee9cf87b1e9cfaa4d868bcce82b7d57b3 + storybook: ^8.4.7 + checksum: 10/b213e725b3b150b3346e91206cd62bf348f537bfec999a6ca8c7c3a9f772ae69b0e67c50b29e48aaa3315753459bd66782d571a014cafe131d88e2ec3b68f060 languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-toolbars@npm:8.4.2" +"@storybook/addon-toolbars@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-toolbars@npm:8.4.7" peerDependencies: - storybook: ^8.4.2 - checksum: 10/294019d0081874ff15fc846768f3743e11278f6a1d211f413bfcdbc5ca9415a80ac478523cae51fcfe1c77277a32bad9745362c8a0a1bd053570e07df615af95 + storybook: ^8.4.7 + checksum: 10/dff15abb4942a95e89d8d84dfa210388b3fec845e2deee473752f340638348c314b68cb5c052644f3a12b1adba2b3b82dd2dd07a6ac427f6043e26993b81722d languageName: node linkType: hard -"@storybook/addon-viewport@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/addon-viewport@npm:8.4.2" +"@storybook/addon-viewport@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/addon-viewport@npm:8.4.7" dependencies: memoizerific: "npm:^1.11.3" peerDependencies: - storybook: ^8.4.2 - checksum: 10/7d2c0fa6ed73030cf718a306dd0c327006f54747801e13a7037416ca746ff92f3d341820744de08e651ed746e0f5bb50104a2c57b17b6f4de3ef0f8987861020 + storybook: ^8.4.7 + checksum: 10/8eaf261e43d70b6453a4ec93a3b6ace728a13db0cf49c6c2f38ca49ad987f7b9268dccf71de2b2dd15cacb8862c9de86689ce258565e2c6fa21c20690ff5761a languageName: node linkType: hard @@ -3941,9 +4013,9 @@ __metadata: languageName: node linkType: hard -"@storybook/blocks@npm:8.4.2, @storybook/blocks@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/blocks@npm:8.4.2" +"@storybook/blocks@npm:8.4.7, @storybook/blocks@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/blocks@npm:8.4.7" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/icons": "npm:^1.2.12" @@ -3951,21 +4023,21 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 peerDependenciesMeta: react: optional: true react-dom: optional: true - checksum: 10/88880e7c13752fe1323cd8eeb93005ab4493c4f024ef3119462dc160e57a985979756a64e0d6e9f374c0f510adcd0e7141d5b7f52bbe255e1c3fbf4ce0cbb896 + checksum: 10/d1b92f08b7a829800b16d7a6c6b540eb9b855ca6b6dd7d87cd9c67d211590e76eb43b03d04685950839e764ac96fb6062872868f204fec91bfc1ec4624dbcd6c languageName: node linkType: hard -"@storybook/builder-webpack5@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/builder-webpack5@npm:8.4.2" +"@storybook/builder-webpack5@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/builder-webpack5@npm:8.4.7" dependencies: - "@storybook/core-webpack": "npm:8.4.2" + "@storybook/core-webpack": "npm:8.4.7" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7.3.4" browser-assert: "npm:^1.2.1" @@ -3991,32 +4063,32 @@ __metadata: webpack-hot-middleware: "npm:^2.25.1" webpack-virtual-modules: "npm:^0.6.0" peerDependencies: - storybook: ^8.4.2 + storybook: ^8.4.7 peerDependenciesMeta: typescript: optional: true - checksum: 10/d67458e1337c8e403f6b4914d5f00d99d3dbd0ec2d1e3bdd7f2eb7a9f0c5acc4d78db45e30c6be1fea48b502d37bda46be41508ed258152f97e7008249c95aed + checksum: 10/169d12e25780ec5801c051bc3abc3de12d236327f6ea035cfb6938f59db009e6bea88d4bbf1e13ceecb9fa726abd317a11fde88b3143b1e35608e62775d4761d languageName: node linkType: hard -"@storybook/components@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/components@npm:8.4.2" +"@storybook/components@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/components@npm:8.4.7" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/52c7885763f3154215b8f9fda57fe4af62e5194722bca90f35769a74aee411bf552d96455d1c2101404e30b2b0a4ed2c57c21a365ecbf18124a252538e15e83e + checksum: 10/e39fb81e8386db4f3f76cbf4f82e50512fed2f65a581951c0b61e00c9834c20cfff7f717e936353275dadfe6a25ffaac5d47151adbe1e3be85e709f8a64f6a15 languageName: node linkType: hard -"@storybook/core-webpack@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/core-webpack@npm:8.4.2" +"@storybook/core-webpack@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/core-webpack@npm:8.4.7" dependencies: "@types/node": "npm:^22.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.4.2 - checksum: 10/8e6d75e70720743bd299855277cbd5a7bb00c286e715050fa8c1c7369d2e4fce7b69cee1a2cd56475760ad3ea3931e4a76c4eb61d8d70a447310414057a6bfb9 + storybook: ^8.4.7 + checksum: 10/561d28962e201086d9f0d739b377aaa5bdaad9eff0dd78cbb6cc9746b70fa3ad86d223e396f414345d19720807a3084ade16c9f2c634d07ed6b8b3355b96be91 languageName: node linkType: hard @@ -4044,14 +4116,14 @@ __metadata: languageName: node linkType: hard -"@storybook/csf-plugin@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/csf-plugin@npm:8.4.2" +"@storybook/csf-plugin@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/csf-plugin@npm:8.4.7" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.4.2 - checksum: 10/ec787be0a4fe2928a2675fe6bca5799f0bff8c66e50cea7bed198a188b19ee57257266b0255c5226a7da97928758b86c978afecc3ee0e2bb1f21fe0e7fdaf0c5 + storybook: ^8.4.7 + checksum: 10/d9006d1a506796717528ee81948be89c8ca7e4a4ad463e024936d828b8e91e12940a41f054db4d5b1f1b058146113aaeb415eca87ca94142c3ef1ef501aead17 languageName: node linkType: hard @@ -4081,30 +4153,30 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/instrumenter@npm:8.4.2" +"@storybook/instrumenter@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/instrumenter@npm:8.4.7" dependencies: "@storybook/global": "npm:^5.0.0" "@vitest/utils": "npm:^2.1.1" peerDependencies: - storybook: ^8.4.2 - checksum: 10/b6d48ffe6a1ad1fca0d296438e2114df6631667ccda803595d54c71732d0bb7dad086d5465254ba914f4666363018bf1df48cdc8304ad6b6c7a24b87164f7d95 + storybook: ^8.4.7 + checksum: 10/8142789e7dd32f881cf9de551078fb3574cc54b47bb8fd2c8b66ea1fb100f14af702f4cbd4bc11a8d1dd4c89f5d0ce7574d2e232b197c43bbebd0a30c06c7e75 languageName: node linkType: hard -"@storybook/manager-api@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/manager-api@npm:8.4.2" +"@storybook/manager-api@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/manager-api@npm:8.4.7" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/61b8f845f3271c2dd84597a94c2905a785949a2467582663165e476f455f1942b9c3be77a08f5101848d8a75ca23aa9f7db620d81f202d19093af7695d6ca8a0 + checksum: 10/2b826ec55de7ea0b5b5151dfa896f3e7eddfd36ede61f8a7ad14a37733d5d5645565f863dbde7e2272f1e9b5717f26de7802ae60e297a2647ee2c4c072ed3069 languageName: node linkType: hard -"@storybook/nextjs@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/nextjs@npm:8.4.2" +"@storybook/nextjs@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/nextjs@npm:8.4.7" dependencies: "@babel/core": "npm:^7.24.4" "@babel/plugin-syntax-bigint": "npm:^7.8.3" @@ -4120,10 +4192,10 @@ __metadata: "@babel/preset-typescript": "npm:^7.24.1" "@babel/runtime": "npm:^7.24.4" "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11" - "@storybook/builder-webpack5": "npm:8.4.2" - "@storybook/preset-react-webpack": "npm:8.4.2" - "@storybook/react": "npm:8.4.2" - "@storybook/test": "npm:8.4.2" + "@storybook/builder-webpack5": "npm:8.4.7" + "@storybook/preset-react-webpack": "npm:8.4.7" + "@storybook/react": "npm:8.4.7" + "@storybook/test": "npm:8.4.7" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7.3.4" babel-loader: "npm:^9.1.3" @@ -4146,10 +4218,10 @@ __metadata: tsconfig-paths: "npm:^4.0.0" tsconfig-paths-webpack-plugin: "npm:^4.0.1" peerDependencies: - next: ^13.5.0 || ^14.0.0 + next: ^13.5.0 || ^14.0.0 || ^15.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 webpack: ^5.0.0 dependenciesMeta: sharp: @@ -4159,16 +4231,16 @@ __metadata: optional: true webpack: optional: true - checksum: 10/651339571dfac8fe34b44f811aa6dd5c8b34bc0ed3da19d8c6044656e6f8a554adad74af707a72cd4844595476635c5b7a3bc8076642c0e0f82a2294b5441e6e + checksum: 10/730e6c1a845f8498106151b49ad9a792b8db12089d398213f0e28bfdfe5833491e48c6298cfc489a6e9d46cbf02a849fbe9a2ad3738a163a6623e3017006b8d1 languageName: node linkType: hard -"@storybook/preset-react-webpack@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/preset-react-webpack@npm:8.4.2" +"@storybook/preset-react-webpack@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/preset-react-webpack@npm:8.4.7" dependencies: - "@storybook/core-webpack": "npm:8.4.2" - "@storybook/react": "npm:8.4.2" + "@storybook/core-webpack": "npm:8.4.7" + "@storybook/react": "npm:8.4.7" "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/node": "npm:^22.0.0" "@types/semver": "npm:^7.3.4" @@ -4182,20 +4254,20 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 peerDependenciesMeta: typescript: optional: true - checksum: 10/5c08b8c8d59262fa390ad35214179f17b3ddddd7b0956efd0d73542b6c82771b8e6814ab45c57b7099bd4026b49594106222cd620b34a2871427d59050a13e0b + checksum: 10/d338fa45547126ee35ec0433a9811d9c816cebf27ec7598539b62bb08b5a9c39634986670e1cbcf11778a13691ee0695fc71e4dea68c393e5feb6ae478d047f5 languageName: node linkType: hard -"@storybook/preview-api@npm:8.4.2, @storybook/preview-api@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/preview-api@npm:8.4.2" +"@storybook/preview-api@npm:8.4.7, @storybook/preview-api@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/preview-api@npm:8.4.7" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/5e57c276bc30afd106dbe89b88dd8b4265d538bc4c55d011b6277d24d0210a7b1b9921f7f2403f06ad824fc50b5846d9037803c613a6f437239a667a52841eea + checksum: 10/1c467bb2c16c5998b9bc4c2c013e6786936d5f6a373ad8d8ab1beb626616c3187329fdfc3a709663b4af963c7e5789a1401166c6e2a3a66a12f66e858aa94e91 languageName: node linkType: hard @@ -4217,95 +4289,95 @@ __metadata: languageName: node linkType: hard -"@storybook/react-dom-shim@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/react-dom-shim@npm:8.4.2" +"@storybook/react-dom-shim@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/react-dom-shim@npm:8.4.7" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 - checksum: 10/76c058e1a2397dfc4fcd5a5fe74bc3e389754b1d7149c0ecc53afb5f706926d1ca2a76e7da04990cd500b378e3e09ffaead827808c12d1da73ea73a1973b630f + storybook: ^8.4.7 + checksum: 10/c45af3e1320f131231aad794c8f0d565677313ba0edbac31e3551bab371927f31ec780151fbc451c57205bd0b73a157b95901d2c4d06c6a63ce868866948f328 languageName: node linkType: hard -"@storybook/react-webpack5@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/react-webpack5@npm:8.4.2" +"@storybook/react-webpack5@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/react-webpack5@npm:8.4.7" dependencies: - "@storybook/builder-webpack5": "npm:8.4.2" - "@storybook/preset-react-webpack": "npm:8.4.2" - "@storybook/react": "npm:8.4.2" + "@storybook/builder-webpack5": "npm:8.4.7" + "@storybook/preset-react-webpack": "npm:8.4.7" + "@storybook/react": "npm:8.4.7" "@types/node": "npm:^22.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 typescript: ">= 4.2.x" peerDependenciesMeta: typescript: optional: true - checksum: 10/a4f98edcc918e1f0e0f03baf3846fe6f8dcb744873218a9f4ed9c0f4fb813e659b1782060f823c12da4ab66ff7b717be5f51d056650d144d243532f849a7742a + checksum: 10/368565a6f8173025dbaa621d161562d0076f87e7ffd72e4bcd5a145501aa6376bc6a6fadad52a9876b586cc1ff82ffd0774faa9fc4833db41447121a8d6bae86 languageName: node linkType: hard -"@storybook/react@npm:8.4.2, @storybook/react@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/react@npm:8.4.2" +"@storybook/react@npm:8.4.7, @storybook/react@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/react@npm:8.4.7" dependencies: - "@storybook/components": "npm:8.4.2" + "@storybook/components": "npm:8.4.7" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.4.2" - "@storybook/preview-api": "npm:8.4.2" - "@storybook/react-dom-shim": "npm:8.4.2" - "@storybook/theming": "npm:8.4.2" + "@storybook/manager-api": "npm:8.4.7" + "@storybook/preview-api": "npm:8.4.7" + "@storybook/react-dom-shim": "npm:8.4.7" + "@storybook/theming": "npm:8.4.7" peerDependencies: - "@storybook/test": 8.4.2 + "@storybook/test": 8.4.7 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.4.2 + storybook: ^8.4.7 typescript: ">= 4.2.x" peerDependenciesMeta: "@storybook/test": optional: true typescript: optional: true - checksum: 10/df7dd512d2e4604b7af859f594c058be959495dc0c25467f571f1e6b4e5058c2ee0d3caaed47742e3a602eef871143b1d48aa164646d61923885ebc7eec87b56 + checksum: 10/4138b11118a313dca2551de307b994f84121c306f2d3a66c29ef9fb07352451a899ce91fd8736149182f8806a7c03dbbe7a4a7d463b0ab3eddbd195057c4cbf8 languageName: node linkType: hard -"@storybook/test@npm:8.4.2, @storybook/test@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/test@npm:8.4.2" +"@storybook/test@npm:8.4.7, @storybook/test@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/test@npm:8.4.7" dependencies: "@storybook/csf": "npm:^0.1.11" "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.4.2" + "@storybook/instrumenter": "npm:8.4.7" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.5.0" "@testing-library/user-event": "npm:14.5.2" "@vitest/expect": "npm:2.0.5" "@vitest/spy": "npm:2.0.5" peerDependencies: - storybook: ^8.4.2 - checksum: 10/934518e629d2798df10bb892184e18dca0f3ea7b426636cef3f29a12302a3c471b59d808024ea90c58b9e183ce1b3477a0c8827824f7e74111ceaf4518f0212e + storybook: ^8.4.7 + checksum: 10/e6e8c2b5b63337e297362716a9de81818f8d94107cc1eea6c1aef75d0ad93d417d277fa90068ee1960acba98ea2658660514148d106a547419c9088c20905f02 languageName: node linkType: hard -"@storybook/theming@npm:8.4.2": - version: 8.4.2 - resolution: "@storybook/theming@npm:8.4.2" +"@storybook/theming@npm:8.4.7": + version: 8.4.7 + resolution: "@storybook/theming@npm:8.4.7" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/8d0a6d20d3589815dea0bcdae9255216b8fc5459225871881a52e0fbc94a136a9972bef02c2ec6167ffd8eea24afab68962e01e83a8d4f60d12b4b787b9b23f3 + checksum: 10/47d29993c33bb29994d227af30e099579b7cf760652ed743020f5d7e5a5974f59a6ebeb1cc8995e6158da9cf768a8d2f559d1d819cc082d0bcdb056d85fdcb29 languageName: node linkType: hard -"@storybook/types@npm:^8.4.2": - version: 8.4.2 - resolution: "@storybook/types@npm:8.4.2" +"@storybook/types@npm:^8.4.7": + version: 8.4.7 + resolution: "@storybook/types@npm:8.4.7" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/2fb61f73bafb55e19c65f903dd87ac01281d0214c7938a90fc925a9ace327e4e566784b641a3dcf6f2975c904f9cd97d1d70403d421396b1398346e29a0624dd + checksum: 10/9a772267b43639fa51d078d4c9f45314d05321fdd92c59f016fe22ee695a71f47936aba0ab98e8e736e17be646076f7db89ac7b9539b4efe66a956c0efb0a1e7 languageName: node linkType: hard @@ -4675,6 +4747,13 @@ __metadata: languageName: node linkType: hard +"@types/diff-match-patch@npm:^1.0.36": + version: 1.0.36 + resolution: "@types/diff-match-patch@npm:1.0.36" + checksum: 10/7d7ce03422fcc3e79d0cda26e4748aeb176b75ca4b4e5f38459b112bf24660d628424bdb08d330faefa69039d19a5316e7a102a8ab68b8e294c8346790e55113 + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" @@ -4872,7 +4951,7 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.13, @types/prop-types@npm:^15.7.3": +"@types/prop-types@npm:*, @types/prop-types@npm:^15.7.13": version: 15.7.13 resolution: "@types/prop-types@npm:15.7.13" checksum: 10/8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c @@ -4904,7 +4983,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18.0.26": +"@types/react@npm:*": version: 18.3.12 resolution: "@types/react@npm:18.3.12" dependencies: @@ -5211,6 +5290,13 @@ __metadata: languageName: node linkType: hard +"@ungap/structured-clone@npm:^1.0.0": + version: 1.2.1 + resolution: "@ungap/structured-clone@npm:1.2.1" + checksum: 10/6770f71e8183311b2871601ddb02d62a26373be7cf2950cb546a345a2305c75b502e36ce80166120aa2f5f1ea1562141684651ebbfcc711c58acd32035d3e545 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -5560,6 +5646,29 @@ __metadata: languageName: node linkType: hard +"ai@npm:^4.0.13": + version: 4.0.13 + resolution: "ai@npm:4.0.13" + dependencies: + "@ai-sdk/provider": "npm:1.0.1" + "@ai-sdk/provider-utils": "npm:2.0.3" + "@ai-sdk/react": "npm:1.0.5" + "@ai-sdk/ui-utils": "npm:1.0.4" + "@opentelemetry/api": "npm:1.9.0" + jsondiffpatch: "npm:0.6.0" + zod-to-json-schema: "npm:^3.23.5" + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.0.0 + peerDependenciesMeta: + react: + optional: true + zod: + optional: true + checksum: 10/5860ad6c03df566358fa84c472f95417b5558fee5ed9c9f71a44b9026943ac5d147c5c8523eea7d94719030946616734f4c695666d0288d515b83de04c2d7a1f + languageName: node + linkType: hard + "ajv-formats@npm:^2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" @@ -6716,7 +6825,7 @@ __metadata: languageName: node linkType: hard -"classnames@npm:^2.2.6, classnames@npm:^2.5.1": +"classnames@npm:^2.5.1": version: 2.5.1 resolution: "classnames@npm:2.5.1" checksum: 10/58eb394e8817021b153bb6e7d782cfb667e4ab390cb2e9dac2fc7c6b979d1cc2b2a733093955fc5c94aa79ef5c8c89f11ab77780894509be6afbb91dddd79d15 @@ -6787,7 +6896,7 @@ __metadata: languageName: node linkType: hard -"client-only@npm:0.0.1": +"client-only@npm:0.0.1, client-only@npm:^0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" checksum: 10/0c16bf660dadb90610553c1d8946a7fdfb81d624adea073b8440b7d795d5b5b08beb3c950c6a2cf16279365a3265158a236876d92bce16423c485c322d7dfaf8 @@ -6912,6 +7021,13 @@ __metadata: languageName: node linkType: hard +"comma-separated-tokens@npm:^2.0.0": + version: 2.0.3 + resolution: "comma-separated-tokens@npm:2.0.3" + checksum: 10/e3bf9e0332a5c45f49b90e79bcdb4a7a85f28d6a6f0876a94f1bb9b2bfbdbbb9292aac50e1e742d8c0db1e62a0229a106f57917e2d067fca951d81737651700d + languageName: node + linkType: hard + "commander@npm:^2.20.0": version: 2.20.3 resolution: "commander@npm:2.20.3" @@ -7527,6 +7643,13 @@ __metadata: languageName: node linkType: hard +"diff-match-patch@npm:^1.0.5": + version: 1.0.5 + resolution: "diff-match-patch@npm:1.0.5" + checksum: 10/fd1ab417eba9559bda752a4dfc9a8ac73fa2ca8b146d29d153964b437168e301c09d8a688fae0cd81d32dc6508a4918a94614213c85df760793f44e245173bb6 + languageName: node + linkType: hard + "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -8608,6 +8731,13 @@ __metadata: languageName: node linkType: hard +"eventsource-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "eventsource-parser@npm:3.0.0" + checksum: 10/8215adf5d8404105ecd0658030b0407e06987ceb9aadcea28a38d69bacf02e5d0fc8bba5fa7c3954552c89509c8ef5e1fa3895e000c061411c055b4bbc26f4b0 + languageName: node + linkType: hard + "evp_bytestokey@npm:^1.0.0, evp_bytestokey@npm:^1.0.3": version: 1.0.3 resolution: "evp_bytestokey@npm:1.0.3" @@ -9450,6 +9580,38 @@ __metadata: languageName: node linkType: hard +"hast-util-to-jsx-runtime@npm:^2.0.0": + version: 2.3.2 + resolution: "hast-util-to-jsx-runtime@npm:2.3.2" + dependencies: + "@types/estree": "npm:^1.0.0" + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + comma-separated-tokens: "npm:^2.0.0" + devlop: "npm:^1.0.0" + estree-util-is-identifier-name: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + mdast-util-mdx-expression: "npm:^2.0.0" + mdast-util-mdx-jsx: "npm:^3.0.0" + mdast-util-mdxjs-esm: "npm:^2.0.0" + property-information: "npm:^6.0.0" + space-separated-tokens: "npm:^2.0.0" + style-to-object: "npm:^1.0.0" + unist-util-position: "npm:^5.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10/3d72f83e2d8c29adc6576d2c6b41479902fd51fac8cfb2b67c35fd68fcb9c25c274699442e4dee901a7ab926a0ff6851713ed5d92448ac09ae0f10daf293476c + languageName: node + linkType: hard + +"hast-util-whitespace@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-whitespace@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10/8c7e9eeb8131fc18702f3a42623eb6b0b09d470347aa8badacac70e6d91f79657ab8c6b57c4c6fee3658cff405fac30e816d1cdfb3ed1fbf6045d0a4555cf4d4 + languageName: node + linkType: hard + "he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" @@ -9551,6 +9713,13 @@ __metadata: languageName: node linkType: hard +"html-url-attributes@npm:^3.0.0": + version: 3.0.1 + resolution: "html-url-attributes@npm:3.0.1" + checksum: 10/494074c2f730c5c0e517aa1b10111fb36732534a2d2b70427582c4a615472b47da472cf3a17562cc653826d378d20960f2783e0400f4f7cf0c3c2d91c6188d13 + languageName: node + linkType: hard + "html-webpack-plugin@npm:^5.5.0": version: 5.6.3 resolution: "html-webpack-plugin@npm:5.6.3" @@ -9839,6 +10008,13 @@ __metadata: languageName: node linkType: hard +"inline-style-parser@npm:0.2.4": + version: 0.2.4 + resolution: "inline-style-parser@npm:0.2.4" + checksum: 10/80814479d1f3c9cbd102f9de4cd6558cf43cc2e48640e81c4371c3634f1e8b6dfeb2f21063cfa31d46cc83e834c20cd59ed9eeed9bfd45ef5bc02187ad941faf + languageName: node + linkType: hard + "internal-slot@npm:^1.0.7": version: 1.0.7 resolution: "internal-slot@npm:1.0.7" @@ -10643,7 +10819,7 @@ __metadata: languageName: node linkType: hard -"jest-fail-on-console@npm:^3.2.0": +"jest-fail-on-console@npm:^3.3.1": version: 3.3.1 resolution: "jest-fail-on-console@npm:3.3.1" checksum: 10/59d72906c20390dcd79be51c2ff0d65fe66fb68c2a4633339b95c3bd5dce5745adb06ac91b095de03f367cc2b4d6868d44abf97e1fb21d7e7835a9710b0630cd @@ -11116,6 +11292,13 @@ __metadata: languageName: node linkType: hard +"json-schema@npm:^0.4.0": + version: 0.4.0 + resolution: "json-schema@npm:0.4.0" + checksum: 10/8b3b64eff4a807dc2a3045b104ed1b9335cd8d57aa74c58718f07f0f48b8baa3293b00af4dcfbdc9144c3aafea1e97982cc27cc8e150fc5d93c540649507a458 + languageName: node + linkType: hard + "json-stable-stringify-without-jsonify@npm:^1.0.1": version: 1.0.1 resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" @@ -11157,6 +11340,19 @@ __metadata: languageName: node linkType: hard +"jsondiffpatch@npm:0.6.0": + version: 0.6.0 + resolution: "jsondiffpatch@npm:0.6.0" + dependencies: + "@types/diff-match-patch": "npm:^1.0.36" + chalk: "npm:^5.3.0" + diff-match-patch: "npm:^1.0.5" + bin: + jsondiffpatch: bin/jsondiffpatch.js + checksum: 10/124b9797c266c693e69f8d23216e64d5ca4b21a4ec10e3a769a7b8cb19602ba62522f9a3d0c55299c1bfbe5ad955ca9ad2852439ca2c6b6316b8f91a5c218e94 + languageName: node + linkType: hard + "jsonfile@npm:^6.0.1, jsonfile@npm:^6.1.0": version: 6.1.0 resolution: "jsonfile@npm:6.1.0" @@ -11727,22 +11923,6 @@ __metadata: languageName: node linkType: hard -"material-ui-popup-state@npm:^5.1.0": - version: 5.3.1 - resolution: "material-ui-popup-state@npm:5.3.1" - dependencies: - "@babel/runtime": "npm:^7.20.6" - "@types/prop-types": "npm:^15.7.3" - "@types/react": "npm:^18.0.26" - classnames: "npm:^2.2.6" - prop-types: "npm:^15.7.2" - peerDependencies: - "@mui/material": ^5.0.0 || ^6.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 10/d88baa569a7cf5a091c5a113f65208b1234bec4bbab5f5df701b9ecc92442295c9cc579604d615da50e7d2953d555b68d64cfffa8c575b33ed7e4b9fa898cf23 - languageName: node - linkType: hard - "md5.js@npm:^1.3.4": version: 1.3.5 resolution: "md5.js@npm:1.3.5" @@ -11858,6 +12038,23 @@ __metadata: languageName: node linkType: hard +"mdast-util-to-hast@npm:^13.0.0": + version: 13.2.0 + resolution: "mdast-util-to-hast@npm:13.2.0" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + "@ungap/structured-clone": "npm:^1.0.0" + devlop: "npm:^1.0.0" + micromark-util-sanitize-uri: "npm:^2.0.0" + trim-lines: "npm:^3.0.0" + unist-util-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + checksum: 10/b17ee338f843af31a1c7a2ebf0df6f0b41c9380b7119a63ab521d271df665456578e1234bb7617883e8d860fe878038dcf2b76ab2f21e0f7451215a096d26cce + languageName: node + linkType: hard + "mdast-util-to-markdown@npm:^2.0.0": version: 2.1.2 resolution: "mdast-util-to-markdown@npm:2.1.2" @@ -14023,7 +14220,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.6.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -14034,6 +14231,13 @@ __metadata: languageName: node linkType: hard +"property-information@npm:^6.0.0": + version: 6.5.0 + resolution: "property-information@npm:6.5.0" + checksum: 10/fced94f3a09bf651ad1824d1bdc8980428e3e480e6d01e98df6babe2cc9d45a1c52eee9a7736d2006958f9b394eb5964dedd37e23038086ddc143fc2fd5e426c + languageName: node + linkType: hard + "proto-list@npm:~1.2.1": version: 1.2.4 resolution: "proto-list@npm:1.2.4" @@ -14249,6 +14453,27 @@ __metadata: languageName: node linkType: hard +"react-markdown@npm:^9.0.1": + version: 9.0.1 + resolution: "react-markdown@npm:9.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + devlop: "npm:^1.0.0" + hast-util-to-jsx-runtime: "npm:^2.0.0" + html-url-attributes: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.0.0" + unified: "npm:^11.0.0" + unist-util-visit: "npm:^5.0.0" + vfile: "npm:^6.0.0" + peerDependencies: + "@types/react": ">=18" + react: ">=18" + checksum: 10/71ce31f200982f641d363888a26e8fb52a199a589124f20295e9be870fa3aed26fcfa14d1dc766d83df666a15cb82359291bfda207bd55d5728ff376d217e079 + languageName: node + linkType: hard + "react-refresh@npm:^0.14.0": version: 0.14.2 resolution: "react-refresh@npm:0.14.2" @@ -14571,6 +14796,19 @@ __metadata: languageName: node linkType: hard +"remark-rehype@npm:^11.0.0": + version: 11.1.1 + resolution: "remark-rehype@npm:11.1.1" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/mdast": "npm:^4.0.0" + mdast-util-to-hast: "npm:^13.0.0" + unified: "npm:^11.0.0" + vfile: "npm:^6.0.0" + checksum: 10/39404bd19c57b2b69660be7e3d587ddb2240495845d42fad3bcc506c9c132d07abacb0a20182b73c530857b2da0c463ad5658382b448243ce432152ab49af08d + languageName: node + linkType: hard + "remark-stringify@npm:^11.0.0": version: 11.0.0 resolution: "remark-stringify@npm:11.0.0" @@ -14891,6 +15129,13 @@ __metadata: languageName: node linkType: hard +"secure-json-parse@npm:^2.7.0": + version: 2.7.0 + resolution: "secure-json-parse@npm:2.7.0" + checksum: 10/974386587060b6fc5b1ac06481b2f9dbbb0d63c860cc73dc7533f27835fdb67b0ef08762dbfef25625c15bc0a0c366899e00076cb0d556af06b71e22f1dede4c + languageName: node + linkType: hard + "semantic-release@npm:^24.2.0": version: 24.2.0 resolution: "semantic-release@npm:24.2.0" @@ -15283,6 +15528,13 @@ __metadata: languageName: node linkType: hard +"space-separated-tokens@npm:^2.0.0": + version: 2.0.2 + resolution: "space-separated-tokens@npm:2.0.2" + checksum: 10/202e97d7ca1ba0758a0aa4fe226ff98142073bcceeff2da3aad037968878552c3bbce3b3231970025375bbba5aee00c5b8206eda408da837ab2dc9c0f26be990 + languageName: node + linkType: hard + "spawn-error-forwarder@npm:~1.0.0": version: 1.0.0 resolution: "spawn-error-forwarder@npm:1.0.0" @@ -15698,6 +15950,15 @@ __metadata: languageName: node linkType: hard +"style-to-object@npm:^1.0.0": + version: 1.0.8 + resolution: "style-to-object@npm:1.0.8" + dependencies: + inline-style-parser: "npm:0.2.4" + checksum: 10/530b067325e3119bfaf75bdbe25cc86b02b559db00d881a74b98a2d5bb10ac953d1b455ed90c825963cf3b4bdaa1bda45f406d78d987391434b8d8ab3835df4e + languageName: node + linkType: hard + "styled-jsx@npm:5.1.6, styled-jsx@npm:^5.1.6": version: 5.1.6 resolution: "styled-jsx@npm:5.1.6" @@ -15794,6 +16055,18 @@ __metadata: languageName: node linkType: hard +"swr@npm:^2.2.5": + version: 2.2.5 + resolution: "swr@npm:2.2.5" + dependencies: + client-only: "npm:^0.0.1" + use-sync-external-store: "npm:^1.2.0" + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + checksum: 10/f02b3bd5a198a0f62f9a53d7c0528c4a58aa61a43310bea169614b6e873dadb52599e856ef0775405b6aa7409835343da0cf328948aa892aa309bf4b7e7d6902 + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -15937,6 +16210,13 @@ __metadata: languageName: node linkType: hard +"throttleit@npm:2.1.0": + version: 2.1.0 + resolution: "throttleit@npm:2.1.0" + checksum: 10/a2003947aafc721c4a17e6f07db72dc88a64fa9bba0f9c659f7997d30f9590b3af22dadd6a41851e0e8497d539c33b2935c2c7919cf4255922509af6913c619b + languageName: node + linkType: hard + "through2@npm:~2.0.0": version: 2.0.5 resolution: "through2@npm:2.0.5" @@ -16044,6 +16324,13 @@ __metadata: languageName: node linkType: hard +"trim-lines@npm:^3.0.0": + version: 3.0.1 + resolution: "trim-lines@npm:3.0.1" + checksum: 10/7a1325e4ce8ff7e9e52007600e9c9862a166d0db1f1cf0c9357e359e410acab1278fcd91cc279dfa5123fc37b69f080de02f471e91dbbc61b155b9ca92597929 + languageName: node + linkType: hard + "trough@npm:^2.0.0": version: 2.2.0 resolution: "trough@npm:2.2.0" @@ -16543,6 +16830,15 @@ __metadata: languageName: node linkType: hard +"unist-util-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-position@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10/89d4da00e74618d7562ac7ac288961df9bcd4ccca6df3b5a90650f018eceb6b95de6e771e88bdbef46cc9d96861d456abe57b7ad1108921e0feb67c6292aa29d + languageName: node + linkType: hard + "unist-util-stringify-position@npm:^2.0.0": version: 2.0.3 resolution: "unist-util-stringify-position@npm:2.0.3" @@ -16668,6 +16964,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.0": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/08bf581a8a2effaefc355e9d18ed025d436230f4cc973db2f593166df357cf63e47b9097b6e5089b594758bde322e1737754ad64905e030d70f8ff7ee671fd01 + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -17276,6 +17581,22 @@ __metadata: languageName: node linkType: hard +"zod-to-json-schema@npm:^3.23.5": + version: 3.23.5 + resolution: "zod-to-json-schema@npm:3.23.5" + peerDependencies: + zod: ^3.23.3 + checksum: 10/53d07a419f0f194e0b96711dc11e7e6fa52a366b0ed5fceb405dc55f13252a1bf433712e4fb496c2a5fdc851018ee1acba7b39c2265c43d6fbb180e12c110c3b + languageName: node + linkType: hard + +"zod@npm:^3.23.8": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 10/846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"