diff --git a/package.json b/package.json index 728194187..71d93df8f 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,12 @@ "immer": "^9.0.7", "lodash": "^4.17.21", "motion": "^11.18.1", + "nanoid": "^5.1.5", "next": "15.1.4", "notistack": "^3.0.1", "number-flip": "^1.2.3", "numbro": "^2.3.6", + "openai": "^4.94.0", "path-to-regexp": "^6.2.1", "react": "19.0.0", "react-awesome-reveal": "^4.2.5", diff --git a/src/app/globals.css b/src/app/globals.css index 49d886643..cf2ac4954 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -5,6 +5,8 @@ @config "../../tailwind.config.ts"; +@import "../assets/css/assistant-message.css"; + @theme { --breakpoint-sm: 600px; --breakpoint-md: 900px; @@ -47,7 +49,7 @@ body { background-color: var(--theme-bg); } -body.mobile-top-nav-open { +body.disable-body-scroll { overflow: hidden !important; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1b0607603..a92dc7e7f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,7 @@ import { GoogleAnalytics } from "@next/third-parties/google" import { SpeedInsights } from "@vercel/speed-insights/next" import clsx from "clsx" import { Metadata } from "next" -import { Roboto } from "next/font/google" +import { Inter, Roboto } from "next/font/google" import localFont from "next/font/local" import React, { Suspense } from "react" @@ -22,6 +22,8 @@ import "./globals.css" export const metadata: Metadata = ROOT_METADATA +export const maxDuration = 90 + // same as scroll documnet const robotoFont = Roboto({ variable: "--font-developer", @@ -30,6 +32,13 @@ const robotoFont = Roboto({ subsets: ["latin"], }) +const interFont = Inter({ + variable: "--font-inter", + weight: ["400", "500", "700"], + display: "swap", + subsets: ["latin"], +}) + const titleFont = localFont({ src: [ { @@ -55,7 +64,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {/* */} - + diff --git a/src/assets/css/assistant-message.css b/src/assets/css/assistant-message.css new file mode 100644 index 000000000..a5895a8ec --- /dev/null +++ b/src/assets/css/assistant-message.css @@ -0,0 +1,91 @@ +.assistant-message { + font-size: 1.6rem; + line-height: 2.4rem; + font-family: var(--font-inter); +} + +.assistant-message p { + margin: 0 0 16px; + max-width: 100%; + line-height: 2.6rem; +} + +.assistant-message h1, +.assistant-message h2, +.assistant-message h3 { + font-weight: 700; + margin-bottom: 1.6rem; +} + +.assistant-message ol { + list-style-type: decimal; + margin-left: 1.8rem; + margin-bottom: 1.6rem; +} +.assistant-message > ol > li { + margin-bottom: 1.6rem; +} +.assistant-message > ol > li::marker { + font-weight: 700; +} +.assistant-message > ol > li p { + margin-bottom: 0; +} + +.assistant-message ul { + list-style-type: disc; + margin-left: 1.5rem; + margin-bottom: 1.6rem; +} + +.assistant-message li { + margin-bottom: 0.8rem; +} + +.assistant-message blockquote:not(:empty) { + border-left: 2px solid #9b9b9b; + font-style: italic; + font-size: 15px; + color: #777; + padding: 8px 8px 8px 16px; + margin-bottom: 1.6rem; +} +.assistant-message blockquote p:last-child { + margin-bottom: 0; +} + +.assistant-message pre { + background-color: #1010100d; + padding: 0.8rem 1.6rem; + border-radius: 0.4rem; + margin-bottom: 1.6rem; +} + +.assistant-message pre code { + background-color: transparent; + padding: 0; + margin: 0; + font-size: inherit; + line-height: inherit; +} + +.assistant-message code { + background-color: #1010100d; + padding: 0.2rem 0.4rem; + border-radius: 0.2rem; +} + +.assistant-message a { + color: #ff684b; + font-weight: 500; + text-decoration: underline; + text-underline-position: from-font; +} + +.assistant-message img { + background-color: transparent; +} + +.assistant-message strong { + font-weight: 700; +} diff --git a/src/assets/images/common/ai-bot.png b/src/assets/images/common/ai-bot.png new file mode 100644 index 000000000..c3f8c1733 Binary files /dev/null and b/src/assets/images/common/ai-bot.png differ diff --git a/src/assets/images/common/scrolly-cool.png b/src/assets/images/common/scrolly-cool.png new file mode 100644 index 000000000..5c33069b3 Binary files /dev/null and b/src/assets/images/common/scrolly-cool.png differ diff --git a/src/assets/svgs/header/checked.svg b/src/assets/svgs/header/checked.svg new file mode 100644 index 000000000..67ff60fd7 --- /dev/null +++ b/src/assets/svgs/header/checked.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/close.svg b/src/assets/svgs/header/close.svg new file mode 100644 index 000000000..88da81ec6 --- /dev/null +++ b/src/assets/svgs/header/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/copy.svg b/src/assets/svgs/header/copy.svg new file mode 100644 index 000000000..cb923e597 --- /dev/null +++ b/src/assets/svgs/header/copy.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/enter.svg b/src/assets/svgs/header/enter.svg new file mode 100644 index 000000000..924766945 --- /dev/null +++ b/src/assets/svgs/header/enter.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/re-send.svg b/src/assets/svgs/header/re-send.svg new file mode 100644 index 000000000..aa3a251e1 --- /dev/null +++ b/src/assets/svgs/header/re-send.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/send.svg b/src/assets/svgs/header/send.svg new file mode 100644 index 000000000..6e75ee0a0 --- /dev/null +++ b/src/assets/svgs/header/send.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/svgs/header/spin.svg b/src/assets/svgs/header/spin.svg new file mode 100644 index 000000000..3e7f55507 --- /dev/null +++ b/src/assets/svgs/header/spin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svgs/header/thumb-down.svg b/src/assets/svgs/header/thumb-down.svg new file mode 100644 index 000000000..afe0becb7 --- /dev/null +++ b/src/assets/svgs/header/thumb-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/svgs/header/thumb-up.svg b/src/assets/svgs/header/thumb-up.svg new file mode 100644 index 000000000..778f4d676 --- /dev/null +++ b/src/assets/svgs/header/thumb-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/AIModal/AIInput.tsx b/src/components/AIModal/AIInput.tsx new file mode 100644 index 000000000..2a6ad58e0 --- /dev/null +++ b/src/components/AIModal/AIInput.tsx @@ -0,0 +1,52 @@ +import { IconButton, Stack, TextareaAutosize } from "@mui/material" + +import SendSvg from "@/assets/svgs/header/send.svg" + +const AIInput = props => { + const { value, disabled, onChat, ...restProps } = props + + const handleKeyDown = e => { + if (e.key === "Enter" && !e.shiftKey && !disabled) { + e.preventDefault() + onChat(value.trim()) + } + } + + return ( + + + onChat(value)}> + + + + ) +} + +export default AIInput diff --git a/src/components/AIModal/AssistantMessage.tsx b/src/components/AIModal/AssistantMessage.tsx new file mode 100644 index 000000000..126171a74 --- /dev/null +++ b/src/components/AIModal/AssistantMessage.tsx @@ -0,0 +1,48 @@ +import React, { useState } from "react" +import ReactMarkdown from "react-markdown" +import remarkGfm from "remark-gfm" + +import { Box } from "@mui/material" + +import Operation from "./Operation" + +const AssistantMessage = props => { + const { children, feedback, allowOperation, isLast, onRetry, onThumbUp, onThumbDown } = props + + const [operationVisible, setOperationVisible] = useState(false) + + const handlePopoverOpen = (event: React.MouseEvent) => { + setOperationVisible(true) + } + + const handlePopoverClose = () => { + setOperationVisible(false) + } + + return ( + <> + + , + }} + className="assistant-message" + /> + + + + + + ) +} + +export default AssistantMessage diff --git a/src/components/AIModal/FeedbackAlert.tsx b/src/components/AIModal/FeedbackAlert.tsx new file mode 100644 index 000000000..aa5e607e8 --- /dev/null +++ b/src/components/AIModal/FeedbackAlert.tsx @@ -0,0 +1,44 @@ +import { AnimatePresence, motion } from "motion/react" +import { useEffect } from "react" + +import { Box } from "@mui/material" + +const MotionBox = motion(Box) +const FeedbackAlert = props => { + const { sx, open, duration, children, onClose } = props + + useEffect(() => { + if (open) { + const timer = setTimeout(() => { + onClose?.() + }, duration || 2e3) // Auto close after 3 seconds + return () => clearTimeout(timer) + } + }, [open]) + + return ( + + {open && ( + + {children} + + )} + + ) +} + +export default FeedbackAlert diff --git a/src/components/AIModal/InitialPanel.tsx b/src/components/AIModal/InitialPanel.tsx new file mode 100644 index 000000000..d16eebc11 --- /dev/null +++ b/src/components/AIModal/InitialPanel.tsx @@ -0,0 +1,71 @@ +import { sendGAEvent } from "@next/third-parties/google" +import { sampleSize } from "lodash" +import Image from "next/image" +import { useEffect, useState } from "react" + +import { Stack, Typography } from "@mui/material" + +import ScrollyCool from "@/assets/images/common/scrolly-cool.png" +import EnterSvg from "@/assets/svgs/header/enter.svg" +import { AI_QUESTION_LIST } from "@/constants" +import useGlobalStore from "@/stores/globalStore" + +const InitialPanel = props => { + const { onChat } = props + const [initialQuestionList, setInitialQuestionList] = useState([]) + const { aiModalVisible } = useGlobalStore() + + useEffect(() => { + if (aiModalVisible) { + setInitialQuestionList(sampleSize(AI_QUESTION_LIST, 5)) + } + }, [aiModalVisible]) + + const handleClickDefaultQuestion = (question: string) => { + onChat(question) + + sendGAEvent("event", "click_ai_default_question", { + label: question, + }) + } + + return ( + + Scrolly + Welcome, Scroll AI is here to help! + {initialQuestionList.map((item, index) => ( + handleClickDefaultQuestion(item)} + > + + {item} + + ))} + + ) +} + +export default InitialPanel diff --git a/src/components/AIModal/MessagePanel.tsx b/src/components/AIModal/MessagePanel.tsx new file mode 100644 index 000000000..ce1edce06 --- /dev/null +++ b/src/components/AIModal/MessagePanel.tsx @@ -0,0 +1,87 @@ +import { sendGAEvent } from "@next/third-parties/google" +import clsx from "clsx" +import { useEffect, useRef, useState } from "react" + +import { Box } from "@mui/material" + +import SpinSvg from "@/assets/svgs/header/spin.svg" + +import AssistantMessage from "./AssistantMessage" +import FeedbackAlert from "./FeedbackAlert" +import UserMessage from "./UserMessage" + +const MessagePanel = props => { + const { data, fetching, streaming, onRetry, onUpdateData } = props + + // Reference to the bottom of the message panel for auto-scrolling + const bottomRef = useRef(null) + + useEffect(() => { + bottomRef.current?.scrollIntoView({ behavior: "smooth" }) + }, [data[data.length - 1]?.text]) + + if (!data || data.length === 0) { + return null + } + + const handleThumbUp = id => { + onUpdateData({ id, feedback: "good" }) + + const messageIndex = data.findIndex(message => message.id === id) + + sendGAEvent("event", "click_ai_feedback", { + label: data[messageIndex - 1].text, + id, + feedback: "good", + }) + } + + const handleThumbDown = id => { + onUpdateData({ id, feedback: "bad" }) + + const messageIndex = data.findIndex(message => message.id === id) + + sendGAEvent("event", "click_ai_feedback", { + label: data[messageIndex - 1].text, + id, + feedback: "bad", + }) + } + return ( + + + {data.map((message, index) => ( + + {message.type === "input_text" ? ( + {message.text} + ) : message.text ? ( + handleThumbUp(message.id)} + onThumbDown={() => handleThumbDown(message.id)} + onRetry={() => onRetry(message.id)} + > + {message.text} + + ) : null} + + ))} + + +
+
+ ) +} + +export default MessagePanel diff --git a/src/components/AIModal/Operation.tsx b/src/components/AIModal/Operation.tsx new file mode 100644 index 000000000..c237edd4d --- /dev/null +++ b/src/components/AIModal/Operation.tsx @@ -0,0 +1,130 @@ +import { AnimatePresence, LayoutGroup, motion } from "motion/react" +import { useState } from "react" + +import { Box, IconButton, Stack, Tooltip } from "@mui/material" + +import CheckedSvg from "@/assets/svgs/header/checked.svg" +import CopySvg from "@/assets/svgs/header/copy.svg" +import ReSendSvg from "@/assets/svgs/header/re-send.svg" +import ThumbDownSvg from "@/assets/svgs/header/thumb-down.svg" +import ThumbUpSvg from "@/assets/svgs/header/thumb-up.svg" + +const MotionBox = motion(Box) + +const MotionStack = motion(Stack) + +const MotionIconButton = motion(IconButton) + +const Operation = props => { + const { sx, visible, feedback, message, onRetry, onThumbUp, onThumbDown } = props + const [tip, setTip] = useState("") + + const [copied, setCopied] = useState(false) + + const operations = [ + { + key: "copy", + icon: copied ? CheckedSvg : CopySvg, + tooltip: "Copy", + onClick: () => { + navigator.clipboard.writeText(message) + setCopied(true) + setTip("Copied") + setTimeout(() => { + setCopied(false) + setTip("") + }, 2e3) + }, + }, + { + key: "thumbUp", + icon: ThumbUpSvg, + hidden: feedback === "bad", + tooltip: "Good Response", + disabled: feedback === "good", + onClick: () => { + onThumbUp?.() + }, + }, + { + key: "thumbDown", + icon: ThumbDownSvg, + hidden: feedback === "good", + tooltip: "Bad Response", + disabled: feedback === "bad", + onClick: () => { + onThumbDown?.() + }, + }, + { + key: "retry", + icon: ReSendSvg, + tooltip: "Try Again", + onClick: () => { + onRetry?.() + }, + }, + ] + + return ( + + {visible && ( + + + + {operations + .filter(({ hidden }) => !hidden) + .map(({ icon: Icon, key, tooltip, disabled, onClick }) => ( + + + + + + ))} + + + + )} + + ) +} + +export default Operation diff --git a/src/components/AIModal/UserMessage.tsx b/src/components/AIModal/UserMessage.tsx new file mode 100644 index 000000000..e7a924ee6 --- /dev/null +++ b/src/components/AIModal/UserMessage.tsx @@ -0,0 +1,25 @@ +import { Box, Typography } from "@mui/material" + +const UserMessage = props => { + const { children } = props + return ( + + + {children} + + + ) +} + +export default UserMessage diff --git a/src/components/AIModal/actions.ts b/src/components/AIModal/actions.ts new file mode 100644 index 000000000..05f0ef315 --- /dev/null +++ b/src/components/AIModal/actions.ts @@ -0,0 +1,31 @@ +"use server" + +import OpenAI from "openai" + +import { AI_PROMPT } from "@/constants" + +const openai = new OpenAI({ + apiKey: process.env.AI_KEY as string, +}) + +type InputMessage = { + role: "developer" | "user" | "assistant" + content: string +} + +export const chatWithAI = async ({ message, prevId }: { message: string; prevId?: string }) => { + const input = [...(prevId ? [] : [{ role: "developer", content: AI_PROMPT }]), { role: "user", content: message }] + + const response = await openai.responses.create({ + model: "gpt-4o-mini", + tools: [{ type: "web_search_preview" }], + input: input as InputMessage[], + previous_response_id: prevId ?? null, + stream: true, + metadata: { + env: process.env.NEXT_PUBLIC_SCROLL_ENVIRONMENT, + }, + }) + + return response.toReadableStream() +} diff --git a/src/components/AIModal/index.tsx b/src/components/AIModal/index.tsx new file mode 100644 index 000000000..4b3a27e5b --- /dev/null +++ b/src/components/AIModal/index.tsx @@ -0,0 +1,292 @@ +"use client" + +import { AnimatePresence, motion } from "motion/react" +import { nanoid } from "nanoid" +import Image from "next/image" +import React, { useEffect, useState } from "react" + +import { Box, Card, IconButton, Stack, Typography } from "@mui/material" + +import AIBot from "@/assets/images/common/ai-bot.png" +import CloseSvg from "@/assets/svgs/header/close.svg" +import useCheckViewport from "@/hooks/useCheckViewport" +import useGlobalStore from "@/stores/globalStore" +import { lockBodyScroll } from "@/utils" + +import AIInput from "./AIInput" +import FeedbackAlert from "./FeedbackAlert" +import InitialPanel from "./InitialPanel" +import MessagePanel from "./MessagePanel" +import { chatWithAI } from "./actions" + +const MotionCard = motion(Card) + +interface Message { + id: string + type: "input_text" | "output_text" | "output_text_error" + text: string + feedback?: "good" | "bad" +} + +type LoadingStatus = "none" | "fetching" | "streaming" + +const AIModal = () => { + const { aiModalVisible, changeAIModalVisible } = useGlobalStore() + + const { isMobile } = useCheckViewport() + + const [searchText, setSearchText] = useState("") + const [feedbackAlertVisible, setFeedbackAlertVisible] = useState(false) + + const [messages, setMessages] = useState([]) + + const [responseId, setResponseId] = useState() + + const [loadingStatus, setLoadingStatus] = useState("none") + + useEffect(() => { + if (aiModalVisible) { + lockBodyScroll(true) + } else { + setResponseId(undefined) + setSearchText("") + setMessages([]) + setLoadingStatus("none") + lockBodyScroll(false) + } + }, [aiModalVisible]) + + // useLayoutEffect(() => { + // function adjustLayout() { + // const viewportHeight = window.visualViewport!.height || "100vh" + // modalRef.current!.style.height = `${viewportHeight}px` + // } + // function preventTouchScroll(e) { + // e.preventDefault() + // } + + // if (isMobile) { + // document.body.addEventListener("touchmove", preventTouchScroll) + // window.visualViewport!.addEventListener("resize", adjustLayout) + // } + + // return () => { + // if (isMobile) { + // window.visualViewport!.removeEventListener("resize", adjustLayout) + // document.body.removeEventListener("touchmove", preventTouchScroll) + // } + // } + // }, [isMobile]) + + const handleChangeSearchText = e => { + setSearchText(e.target.value) + } + + const handleSendMessage = async userMessage => { + setMessages(preValue => { + return preValue.concat({ + id: nanoid(), + type: "input_text", + text: userMessage, + }) + }) + setSearchText("") + chatWithScrollAI(userMessage) + } + + const handleReSendMessage = async (id: string) => { + const messageIndex = messages.findIndex(message => message.id === id) + const reservedMessage = messages.slice(0, messageIndex) + setMessages(reservedMessage) + chatWithScrollAI(reservedMessage[messageIndex - 1].text) + } + + const chatWithScrollAI = async (message: string) => { + setLoadingStatus("fetching") + + let stream + try { + stream = await chatWithAI({ + message, + prevId: responseId, + }) + } catch (error) { + setMessages(preValue => { + return preValue.concat({ + id: nanoid(), + type: "output_text_error", + text: "Network error, please try again.", + }) + }) + setLoadingStatus("none") + return + } + + const reader = stream.getReader() + const decoder = new TextDecoder("utf-8") + + let currentResponseId = nanoid() + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + if (value) { + decodeValue(value, decoder, currentResponseId) + } + } + } catch (error) { + setMessages(preValue => { + return preValue.concat({ + id: nanoid(), + type: "output_text_error", + text: error.message, + }) + }) + await reader.cancel() + } finally { + setLoadingStatus("none") + reader.releaseLock() + } + } + + const decodeValue = async (value, decoder, currentResponseId) => { + const chunk = decoder.decode(value, { stream: true }) + + const lines = chunk.split("\n").filter(line => line.trim()) + + for (const line of lines) { + const event = JSON.parse(line) + + if (event.type === "response.created") { + currentResponseId = event.response.id + setResponseId(currentResponseId) + + setMessages(preValue => { + return preValue.concat({ + id: currentResponseId, + type: "output_text", + text: "", + }) + }) + } else if (event.type === "response.failed") { + throw new Error("Failed to generate AI response, please try again.") + } else if (event.type === "response.output_text.delta") { + setLoadingStatus("streaming") + setMessages(preValue => { + const lastMessage = preValue[preValue.length - 1] + const newMessage = { + id: lastMessage.id, + type: "output_text", + text: lastMessage.text + event.delta, + } as Message + return [...preValue.slice(0, -1), newMessage] + }) + } else if (event.type === "response.completed") { + setLoadingStatus("none") + } else if (event.type === "error") { + throw new Error("Connection error, please try again.") + } + } + } + + const handleUpdateData = ({ id, feedback }) => { + // only feedback update the messages + setFeedbackAlertVisible(true) + setMessages(preValue => { + return preValue.map(message => { + if (message.id === id) { + return { + ...message, + feedback: feedback, + } + } + return message + }) + }) + } + return ( + + {aiModalVisible ? ( + + + {!!messages?.length ? ( + <> + AI bot + Scroll AI + + ) : ( + + )} + + changeAIModalVisible(false)}> + + + + {messages?.length ? ( + + ) : ( + + )} + + + + + setFeedbackAlertVisible(false)}> + Thanks for your feedback! + + + + ) : null} + + ) +} + +export default AIModal diff --git a/src/components/GlobalComponents/index.tsx b/src/components/GlobalComponents/index.tsx index 43ac84c46..2264802d5 100644 --- a/src/components/GlobalComponents/index.tsx +++ b/src/components/GlobalComponents/index.tsx @@ -1,9 +1,14 @@ +import { isDesktop } from "react-device-detect" + import TxHistoryDialog from "@/app/bridge/TxHistoryDialog" +import AIModal from "../AIModal" + const GlobalComponents = () => { return ( <> + {isDesktop && } ) } diff --git a/src/components/Header/AskAI.tsx b/src/components/Header/AskAI.tsx new file mode 100644 index 000000000..3031bc58b --- /dev/null +++ b/src/components/Header/AskAI.tsx @@ -0,0 +1,52 @@ +import { sendGAEvent } from "@next/third-parties/google" +import { useEffect, useRef } from "react" + +import { ButtonBase } from "@mui/material" + +import useGlobalStore from "@/stores/globalStore" + +const AskAI = props => { + const { isMobile } = props + const { aiModalVisible, changeAIModalVisible } = useGlobalStore() + const aiDurationRef = useRef(null) + + useEffect(() => { + if (aiModalVisible) { + aiDurationRef.current = Date.now() + } else { + if (aiDurationRef.current) { + const duration = Date.now() - aiDurationRef.current + sendGAEvent("event", "ai_modal_duration", { duration }) + aiDurationRef.current = null // reset the start time + } + } + }, [aiModalVisible]) + + const handleToggleAIModal = () => { + if (aiModalVisible) { + changeAIModalVisible(false) + } else { + changeAIModalVisible(true) + sendGAEvent("event", "click_ask_ai") + } + } + + return ( + + Ask Scroll AI + + ) +} + +export default AskAI diff --git a/src/components/Header/MobileGasPriceViewer.tsx b/src/components/Header/MobileGasPriceViewer.tsx index d403b4e45..8d5649bef 100644 --- a/src/components/Header/MobileGasPriceViewer.tsx +++ b/src/components/Header/MobileGasPriceViewer.tsx @@ -88,9 +88,7 @@ const MobileGasPriceViewer = props => { position: "relative", borderRadius: "0.5rem", width: "100%", - height: "21.6rem", - mt: "2.4rem", - mb: "4.8rem", + my: "2.4rem", overflow: "hidden", }} ref={gasPriceRef} @@ -112,7 +110,7 @@ const MobileGasPriceViewer = props => { }, }, closed: { - clipPath: `polygon(0 0, 100px 0, 100px 50px, 0 50px)`, + clipPath: `polygon(0 0, 100px 0, 100px 30px, 0 30px)`, transition: { ease: [0.165, 0.84, 0.44, 1], delay: 0.2, @@ -129,7 +127,7 @@ const MobileGasPriceViewer = props => { width: "min-content", alignItems: "center", borderRadius: "0.5rem", - padding: "1.6rem", + padding: "0.8rem 1.6rem", color: dark ? "primary.contrastText" : "text.primary", backgroundColor: dark ? "#333" : "background.default", }} diff --git a/src/components/Header/desktop_header.tsx b/src/components/Header/desktop_header.tsx index 05acb1549..0392b13cb 100644 --- a/src/components/Header/desktop_header.tsx +++ b/src/components/Header/desktop_header.tsx @@ -7,10 +7,10 @@ import ScrollLink from "@/components/Link" import Logo from "@/components/ScrollLogo" import WalletToolkit from "@/components/WalletToolkit" import useCheckViewport from "@/hooks/useCheckViewport" -import useShowLanguageSelect from "@/hooks/useShowLanguageSelect" import useShowWalletConnector from "@/hooks/useShowWalletToolkit" import { isSepolia } from "@/utils" +import AskAI from "./AskAI" import GasPriceViewer from "./GasPriceViewer" import MenuItem from "./MenuItem" import NavbarItem from "./NavbarItem" @@ -27,7 +27,6 @@ const DesktopHeader = ({ currentMenu }) => { const [hoveringNavbarItemKey, setHoveringNavbarItemKey] = useState("") const showWalletConnector = useShowWalletConnector() - const showLanguageSelect = useShowLanguageSelect() const [anchorEl, setAnchorEl] = useState(null) @@ -170,6 +169,7 @@ const DesktopHeader = ({ currentMenu }) => { {!isSepolia && } {showWalletConnector && } + {!isSepolia && } diff --git a/src/components/Header/mobile_header.tsx b/src/components/Header/mobile_header.tsx index 5c0b10b71..7b6712cff 100644 --- a/src/components/Header/mobile_header.tsx +++ b/src/components/Header/mobile_header.tsx @@ -8,8 +8,10 @@ import Link from "@/components/Link" import WalletToolkit from "@/components/WalletToolkit" import useShowLanguageSelect from "@/hooks/useShowLanguageSelect" import useShowWalletConnector from "@/hooks/useShowWalletToolkit" +import { isSepolia } from "@/utils" import Logo from "../ScrollLogo" +import AskAI from "./AskAI" import MenuItem from "./MenuItem" import MobileGasPriceViewer from "./MobileGasPriceViewer" import MobileNavbarItem from "./MobileNavBarItem" @@ -40,9 +42,9 @@ const MobileHeader = ({ currentMenu }) => { useLayoutEffect(() => { if (open) { - window.document.body.classList.add("mobile-top-nav-open") + window.document.body.classList.add("disable-body-scroll") } else { - window.document.body.classList.remove("mobile-top-nav-open") + window.document.body.classList.remove("disable-body-scroll") } }, [open]) @@ -172,12 +174,14 @@ const MobileHeader = ({ currentMenu }) => { sx={{ flex: 1, backgroundColor: dark ? "themeBackground.dark" : "themeBackground.light", + paddingBottom: "4.8rem", overflowY: "auto", }} > {renderList()} - + {!isSepolia && } + {/* {!isSepolia && } */} )} diff --git a/src/constants/ai-assistant.ts b/src/constants/ai-assistant.ts new file mode 100644 index 000000000..af5e77bd8 --- /dev/null +++ b/src/constants/ai-assistant.ts @@ -0,0 +1,227 @@ +export const AI_QUESTION_LIST = [ + "What is Scroll?", + "How can I get SCR?", + "How do I bridge my assets from Ethereum to the Scroll mainnet?", + "Which tokens and assets are supported on the Scroll bridge?", + "How long does the bridging process usually take on Scroll mainnet?", + "What are the best yeild DeFi protocols on Scroll?", + "I’m a Builder interested in building on Scroll. Where can I find Scroll’s technical docs and developer tools?", + "How can I join the Scroll community for updates, support, and discussions?", +] + +export const AI_PROMPT = ` + ### **1. Role Definition:** + + You are an AI bot for the [Scroll.io](http://scroll.io/) project. Your primary responsibility is to provide friendly, clear, and informative Q&A for users visiting [Scroll.io](https://scroll.io/). Most user questions will focus on [Scroll.io](http://scroll.io/) topics and can be categorized as general, technical, token, UX, or ecosystem questions. + + ### **2. Knowledge Priority:** + + - **Primary Sources:** + - All content from official [Scroll.io](http://scroll.io/) pages and their subdomains. + - Content from Scroll’s official Twitter account: https://x.com/scroll_zkp. + - Blockchain information from ScrollScan: https://scrollscan.com/. + - **Secondary Source:** + - Perform web searches using the keywords “Scroll”, “Crypto”, and the user’s input (user_input) as constraints. + - For other blockchain or DeFi data, you can refer to [https://l2beat.com](https://l2beat.com/scaling/summary), https://defillama.com/, dashboards on [https://dune.com/](https://dune.com/discover/content/trending), and + - **Fallback:** + - Only answer a user’s question if you can retrieve relevant information from the above sources or make a well-founded prediction based on them. Otherwise, respond with: + + "I only can answer Scroll related questions yet. You can try to ask something else!" + + + ### **3. Output Requirements:** + + - **Chain-of-Thought Display:** + - **For complex or multi-part questions:** + - Begin your answer with a brief chain-of-thought enclosed in a quoted block (using \` > \`). + - **Important:** Place the chain-of-thought in a separate quoted block followed by a newline before displaying the final answer. + - **For simple questions:** + - Provide the final answer directly without displaying your chain-of-thought. + - **Word Limit:** + - Ensure your complete answer is within 400 words. + - **Structure and Formatting:** + - Use clear structure with proper line breaks and highlight key points. + - Format your answer using Markdown. + - **Citations and Hyperlinks:** + - If your answer includes any direct quotes, provide the reference and clickable link for each quote. + - Any mentioned webpages must be directly linked (e.g., [scroll official website](https://scroll.io/)). + + ### **4. Sample Q&A – Format and Style Guidance:** + + Example 1: General Question (Simple Case) + + - **User Question:** "What is Scroll?" + - **Response:** + + Scroll is a Layer 2 scaling solution for Ethereum that leverages ZK proofs to bundle off-chain transactions into succinct on-chain proofs, delivering up to 2,000+ TPS with 90% lower fees while maintaining full EVM compatibility and inheriting Ethereum’s security model. + + + Example 2: Technical Question (Simple Case) + + - **User Question:** "How can I view transaction data on Scroll?" + - **Response:** + + You can view transaction data on Scroll by visiting ScrollScan. This platform provides comprehensive blockchain information including transaction histories and network statistics. + + + Example 3: Technical Issue – Running a Node (Complex Case) + + - **User Question:** "I'm encountering errors like \`unknown ancestor\` and \`Unexpected queue index in SyncService\` while running a node." + - **Response:** + + > Recognize the error as a known issue linked to unstable L1 node connections. Identify that the solution involves resetting the L1 height using the geth console and referencing a specific block number from historical data. + > + + To resolve the issue, first find an L1 block number roughly 15 minutes before the error occurred. Then, in the geth console, execute: + + \`\`\`go + let resetBlockHeight = ; // replace with the correct block number + admin.setL1MessageSyncedL1Height(resetBlockHeight); + + \`\`\` + + For further details, please refer to the [issue reference](https://scrollco.slack.com/archives/C07MFNLSQCD/p1734593749064919). + + + ### 5. Sample Questions - Any similar questions, no need to think, just reply to user with these prepared answers + + **Example 1** + + - **User Question: “What is Scroll?”** + - Response: + Scroll is a Layer 2 scaling solution for Ethereum that leverages ZK proofs to bundle off-chain transactions into succinct on-chain proofs, delivering up to 2,000+ TPS with 90% lower fees while maintaining full EVM compatibility and inheriting Ethereum’s security model. + + **Example 2** + + - **User Question: “How can I get SCR?”** + - Response: + + You can get SCR quickly through the following: + + **Swap on Scroll DEXs** + + - [Ambient](https://scroll-us.ambient.finance/swap) + - [Maverick](https://app.mav.xyz/) + - [Oku Trade](https://oku.trade/app/scroll) + - [SyncSwap](https://syncswap.xyz/scroll) + + **Buy on Centralized Exchanges** + + - [Binance (SCR/USDT)](https://www.binance.com/en/trade/SCR_USDT) + - [Bithumb (SCR/KRW)](https://www.bithumb.com/react/trade/order/SCR-KRW) + - [OKX](https://www.okx.com/how-to-buy/scroll-scr) + + Please verify all links and do your own research. Scroll is not responsible for any financial losses. + + + **Example 3** + + - **User Question: “How do I bridge my assets from Ethereum to the Scroll mainnet?”** + - Response: + 1. Go to [portal.scroll.io/bridge](https://portal.scroll.io/bridge) and connect your wallet + 2. Click “Deposit to Scroll,” select your token and amount, approve and confirm + 3. Wait for the deposit to complete, typically within 20 minutes + + **Example 4** + + - **User Question: “Which tokens and assets are supported on the Scroll bridge?”** + - Response: + The bridge supports: + - ETH (native) + - All ERC-20 tokens (e.g., USDC, USDT, DAI, WBTC) + - ERC-721 and ERC-1155 NFTs + + **Example 5** + + - **User Question: “How long does the bridging process usually take on Scroll mainnet?”** + - Response: + - Deposits to Scroll: usually 2–20 minutes + - Withdrawals to Ethereum: up to 1 hour to claim + + **Example 6** + + - **User Question: “What are the best yield DeFi protocols on Scroll?”** + - Response: + + Scroll's DeFi ecosystem includes a range of lending, yield, and AMM protocols: + + - [Aave V3](https://app.aave.com/?marketName=proto_scroll_v3): Lending and borrowing with low fees + - [Stargate Finance](https://stargate.finance/bridge): Cross-chain bridge powered by LayerZero + - [Ambient](https://scroll.ambient.finance/swap): Single-contract AMM with dynamic fees and vaults + - [Mitosis](https://app.mitosis.org/?asset=weETH&type=deposit&to=scroll): LP aggregation and points farming + - [Pencils Protocol](https://pencilsprotocol.io/): Vaults, liquid staking, and community rewards + - [Skydrome](https://app.skydrome.finance/): ve(3,3)-style DEX with bribes + - [PunkSwap](https://app.punkswap.exchange/): NFT-farming enabled DEX + + For more, visit [scroll.io/ecosystem](https://scroll.io/ecosystem) + + + **Example 7** + + - **User Question: “I’m a Builder. Where can I find Scroll’s developer docs and tools?”** + - Response: + Explore the developer portal at [docs.scroll.io/en/home](https://docs.scroll.io/en/home), where you’ll find: + - Quickstart guides for popular frameworks + - Architecture overviews and zkEVM design + - SDKs, APIs, tutorials, and GitHub links + + **Example 8** + + - **User Question: “How can I join the Scroll community?”** + - Response: + - [Discord](https://discord.com/invite/scroll): Join live chats and support channels + - [Twitter (X)](https://x.com/Scroll_ZKP): Follow for announcements and updates + - [Telegram](https://t.me/+0tvdw8QMJBMyOTli): Talk with other builders and contributors + + **Example 9** + + - **User Question: “When will Scroll have airdrop 2?”** + - Response: + Scroll has not announced a date for the second airdrop yet. Stay tuned to official channels for news. + + **Example 10** + + - **User Question: “What happens to my Session 2 Marks?”** + - Response: + - A snapshot was taken on May 1, 2025. Marks no longer accrue + - You can still view your data on the [Sessions dashboard](https://portal.scroll.io/sessions) + - The airdrop is still coming—watch for more updates + + ### **6. Potential Hard Cases and Corner Cases:** + + 1. **Ambiguous Queries:** + - *Example:* "Tell me about the latest updates." + - *Issue:* Unclear whether the query refers to Scroll.io-specific updates or general crypto news. + - *Mitigation:* Ask clarifying questions or restrict responses to [Scroll.io](http://scroll.io/) knowledge sources. + 2. **Out-of-Scope Questions:** + - *Example:* "What is the best crypto investment strategy?" + - *Issue:* This falls outside [Scroll.io](http://scroll.io/)'s domain. + - *Mitigation:* Respond with the fallback message: + + "I only can answer Scroll related questions yet. You can try to ask something else!" + + 3. **Complex Multi-Part Queries:** + - *Example:* "Can you compare Scroll’s scalability with other blockchain projects and detail the technical differences?" + - *Issue:* Detailed technical comparisons might exceed the 400-word limit or lead to opinion-based responses. + - *Mitigation:* Provide a summarized comparison, cite reliable sources, and suggest contacting the Scroll team for more in-depth information if necessary. + 4. **Direct Quotes and Citations:** + - *Requirement:* Verify any direct quote’s source and include the proper reference and clickable URL. + + ### 6. DONT DOs + + 1. **Do NOT answer any questions about Scroll’s fundraising, investors, or investment details.** + + If asked, respond with: + + *“I don’t have information related to your question. Please try asking something else.”* + + 2. **Only provide the real-time SCR price if explicitly asked.** + + For any other SCR-related price questions (e.g., predictions, historical prices, price charts), politely decline using this response: + + *“Sorry, I can’t provide details beyond the current SCR price. Please refer to trusted market platforms for price history or projections.”* + + 3. **Always include the following disclaimer in any response involving tokens, price, or trading:** + + *“Scroll does not provide financial advice or recommendations. Please verify the validity of this information and conduct your own research to understand the risks involved. Scroll is not responsible for any financial losses you may incur.”* +` diff --git a/src/constants/index.ts b/src/constants/index.ts index 6024ef807..617d2b104 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -13,3 +13,4 @@ export * from "./canvas" export * from "./badge" export * from "./canvas-badge" export * from "./community" +export * from "./ai-assistant" diff --git a/src/stores/globalStore.ts b/src/stores/globalStore.ts new file mode 100644 index 000000000..cd5caa974 --- /dev/null +++ b/src/stores/globalStore.ts @@ -0,0 +1,19 @@ +import { create } from "zustand" + +interface GlobalStore { + aiModalVisible: boolean + + changeAIModalVisible: (visible: boolean) => void +} + +const useGlobalStore = create()((set, get) => ({ + aiModalVisible: false, + + changeAIModalVisible: visible => { + set({ + aiModalVisible: visible, + }) + }, +})) + +export default useGlobalStore diff --git a/src/theme/options.ts b/src/theme/options.ts index ef1ee3d58..59cffc7b1 100644 --- a/src/theme/options.ts +++ b/src/theme/options.ts @@ -200,6 +200,10 @@ export const typographyOptions = { fontFamily: "var(--font-developer)", }, + inter: { + fontFamily: "var(--font-inter)", + }, + H1: { fontSize: "6.4rem", lineHeight: "normal", diff --git a/src/utils/dom.ts b/src/utils/dom.ts index 89d72d761..22f4742ed 100644 --- a/src/utils/dom.ts +++ b/src/utils/dom.ts @@ -7,3 +7,14 @@ export const isBelowScreen = element => { const rect = element.getBoundingClientRect() return rect.top > window.innerHeight } + +export const lockBodyScroll = (lock: boolean) => { + if (lock) { + const scrollbarWidth = window.innerWidth - document.body.offsetWidth + document.body.style.overflow = "hidden" + document.body.style.paddingRight = `${scrollbarWidth}px` + } else { + document.body.style.overflow = "auto" + document.body.style.paddingRight = "0px" + } +} diff --git a/yarn.lock b/yarn.lock index 189e00f0d..ebfe6b250 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4006,6 +4006,14 @@ dependencies: "@types/node" "*" +"@types/node-fetch@^2.6.4": + version "2.6.12" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03" + integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA== + dependencies: + "@types/node" "*" + form-data "^4.0.0" + "@types/node@*", "@types/node@^20.10.6": version "20.11.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e" @@ -4023,6 +4031,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== +"@types/node@^18.11.18": + version "18.19.86" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.86.tgz#a7e1785289c343155578b9d84a0e3e924deb948b" + integrity sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ== + dependencies: + undici-types "~5.26.4" + "@types/parse-json@^4.0.0": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.2.tgz#5950e50960793055845e956c427fc2b0d70c5239" @@ -4697,6 +4712,13 @@ agent-base@6: dependencies: debug "4" +agentkeepalive@^4.2.1: + version "4.6.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.6.0.tgz#35f73e94b3f40bf65f105219c623ad19c136ea6a" + integrity sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ== + dependencies: + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -4928,6 +4950,11 @@ async-mutex@^0.2.6: dependencies: tslib "^2.0.0" +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + atomic-sleep@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" @@ -5504,6 +5531,13 @@ colorette@^2.0.7: resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -5882,6 +5916,11 @@ defu@^6.1.3, defu@^6.1.4: resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + denque@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" @@ -7046,6 +7085,29 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" +form-data-encoder@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040" + integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A== + +form-data@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c" + integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.12" + +formdata-node@^4.3.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2" + integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ== + dependencies: + node-domexception "1.0.0" + web-streams-polyfill "4.0.0-beta.3" + forwarded-parse@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/forwarded-parse/-/forwarded-parse-2.1.2.tgz#08511eddaaa2ddfd56ba11138eee7df117a09325" @@ -7749,6 +7811,13 @@ human-signals@^5.0.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + husky@^8.0.2: version "8.0.3" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" @@ -9464,6 +9533,18 @@ micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.2" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" @@ -9639,7 +9720,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@^2.1.1, ms@^2.1.3: +ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -9681,6 +9762,11 @@ nanoid@^3.3.8: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.8.tgz#b1be3030bee36aaff18bacb375e5cce521684baf" integrity sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w== +nanoid@^5.1.5: + version "5.1.5" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.1.5.tgz#f7597f9d9054eb4da9548cdd53ca70f1790e87de" + integrity sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw== + napi-wasm@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/napi-wasm/-/napi-wasm-1.1.0.tgz#bbe617823765ae9c1bc12ff5942370eae7b2ba4e" @@ -9737,6 +9823,11 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.0.0.tgz#8136add2f510997b3b94814f4af1cce0b0e3962e" integrity sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA== +node-domexception@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch-native@^1.4.0, node-fetch-native@^1.4.1, node-fetch-native@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.1.tgz#f95c74917d3cebc794cdae0cd2a9c7594aad0cb4" @@ -9970,6 +10061,19 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openai@^4.94.0: + version "4.94.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.94.0.tgz#d5fb3f39ec0e2090687dc9f97db4ca05200e27ca" + integrity sha512-WVmr9HWcwfouLJ7R3UHd2A93ClezTPuJljQxkCYQAL15Sjyt+FBNoqEz5MHSdH/ebQrVyvRhFyn/bvdqtSPyIA== + dependencies: + "@types/node" "^18.11.18" + "@types/node-fetch" "^2.6.4" + abort-controller "^3.0.0" + agentkeepalive "^4.2.1" + form-data-encoder "1.7.2" + formdata-node "^4.3.2" + node-fetch "^2.6.7" + optionator@^0.9.3: version "0.9.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" @@ -12614,6 +12718,11 @@ web-namespaces@^2.0.0: resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== +web-streams-polyfill@4.0.0-beta.3: + version "4.0.0-beta.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38" + integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug== + webauthn-p256@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.5.tgz#0baebd2ba8a414b21cc09c0d40f9dd0be96a06bd"