Add layout switch in all-in-one page

This commit is contained in:
wong2
2023-06-19 14:53:54 +08:00
parent 525b5b08a2
commit 6ea7a6ecd7
19 changed files with 193 additions and 65 deletions

View File

@@ -72,7 +72,7 @@
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"immer": "^9.0.19", "immer": "^9.0.19",
"inter-ui": "^3.19.3", "inter-ui": "^3.19.3",
"jotai": "^2.1.0", "jotai": "^2.2.1",
"jotai-immer": "^0.2.0", "jotai-immer": "^0.2.0",
"js-base64": "^3.7.5", "js-base64": "^3.7.5",
"langchain": "^0.0.84", "langchain": "^0.0.84",
@@ -95,6 +95,7 @@
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"react-textarea-autosize": "^8.4.1", "react-textarea-autosize": "^8.4.1",
"react-viewport-list": "^7.1.1", "react-viewport-list": "^7.1.1",
"react-wrap-balancer": "^1.0.0",
"rehype-highlight": "^6.0.0", "rehype-highlight": "^6.0.0",
"rehype-katex": "^6.0.3", "rehype-katex": "^6.0.3",
"rehype-sanitize": "^5.0.1", "rehype-sanitize": "^5.0.1",

View File

@@ -1,4 +1,4 @@
import { incrTokenUsage } from '~services/storage' import { incrTokenUsage } from '~services/storage/token-usage'
import { ChatMessage } from './consts' import { ChatMessage } from './consts'
import GPT3Tokenizer from 'gpt3-tokenizer' import GPT3Tokenizer from 'gpt3-tokenizer'

View File

@@ -1,5 +1,4 @@
import cx from 'classnames' import cx from 'classnames'
import { useSetAtom } from 'jotai'
import { FC, useCallback, useMemo, useState } from 'react' import { FC, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import clearIcon from '~/assets/icons/clear.svg' import clearIcon from '~/assets/icons/clear.svg'
@@ -8,7 +7,6 @@ import shareIcon from '~/assets/icons/share.svg'
import { CHATBOTS } from '~app/consts' import { CHATBOTS } from '~app/consts'
import { ConversationContext, ConversationContextValue } from '~app/context' import { ConversationContext, ConversationContextValue } from '~app/context'
import { trackEvent } from '~app/plausible' import { trackEvent } from '~app/plausible'
import { multiPanelBotsAtom } from '~app/state'
import { ChatMessageModel } from '~types' import { ChatMessageModel } from '~types'
import { BotId, BotInstance } from '../../bots' import { BotId, BotInstance } from '../../bots'
import Button from '../Button' import Button from '../Button'
@@ -28,7 +26,7 @@ interface Props {
generating: boolean generating: boolean
stopGenerating: () => void stopGenerating: () => void
mode?: 'full' | 'compact' mode?: 'full' | 'compact'
index?: number onSwitchBot?: (botId: BotId) => void
} }
const ConversationPanel: FC<Props> = (props) => { const ConversationPanel: FC<Props> = (props) => {
@@ -38,7 +36,6 @@ const ConversationPanel: FC<Props> = (props) => {
const marginClass = 'mx-5' const marginClass = 'mx-5'
const [showHistory, setShowHistory] = useState(false) const [showHistory, setShowHistory] = useState(false)
const [showShareDialog, setShowShareDialog] = useState(false) const [showShareDialog, setShowShareDialog] = useState(false)
const setCompareBots = useSetAtom(multiPanelBotsAtom)
const context: ConversationContextValue = useMemo(() => { const context: ConversationContextValue = useMemo(() => {
return { return {
@@ -69,21 +66,6 @@ const ConversationPanel: FC<Props> = (props) => {
trackEvent('open_share_dialog', { botId: props.botId }) trackEvent('open_share_dialog', { botId: props.botId })
}, [props.botId]) }, [props.botId])
const onSwitchBot = useCallback(
(botId: BotId) => {
if (props.index === undefined) {
return
}
trackEvent('switch_bot', { botId })
setCompareBots((bots) => {
const newBots = [...bots]
newBots[props.index!] = botId
return newBots
})
},
[props.index, setCompareBots],
)
return ( return (
<ConversationContext.Provider value={context}> <ConversationContext.Provider value={context}>
<div className={cx('flex flex-col overflow-hidden bg-primary-background h-full rounded-[20px]')}> <div className={cx('flex flex-col overflow-hidden bg-primary-background h-full rounded-[20px]')}>
@@ -98,7 +80,9 @@ const ConversationPanel: FC<Props> = (props) => {
<Tooltip content={props.bot.name || botInfo.name}> <Tooltip content={props.bot.name || botInfo.name}>
<span className="font-semibold text-primary-text text-sm cursor-default">{botInfo.name}</span> <span className="font-semibold text-primary-text text-sm cursor-default">{botInfo.name}</span>
</Tooltip> </Tooltip>
{mode === 'compact' && <SwitchBotDropdown excludeBotId={props.botId} onChange={onSwitchBot} />} {mode === 'compact' && props.onSwitchBot && (
<SwitchBotDropdown selectedBotId={props.botId} onChange={props.onSwitchBot} />
)}
</div> </div>
<div className="flex flex-row items-center gap-3"> <div className="flex flex-row items-center gap-3">
<Tooltip content={t('Share conversation')}> <Tooltip content={t('Share conversation')}>

View File

@@ -0,0 +1,30 @@
import cx from 'classnames'
import { FC } from 'react'
import layoutFourIcon from '~assets/icons/layout-four.svg'
import layoutThreeIcon from '~assets/icons/layout-three.svg'
import layoutTwoIcon from '~assets/icons/layout-two.svg'
const Item: FC<{ icon: string; active: boolean; onClick: () => void }> = (props) => {
return (
<a className={cx(!!props.active && 'bg-[#00000014] dark:bg-[#ffffff26] rounded-[6px]')} onClick={props.onClick}>
<img src={props.icon} className="w-8 h-8 cursor-pointer" />
</a>
)
}
interface Props {
layout: number
onChange: (layout: number) => void
}
const LayoutSwitch: FC<Props> = (props) => {
return (
<div className="flex flex-row items-center gap-2 bg-primary-background rounded-[15px] px-4">
<Item icon={layoutTwoIcon} active={props.layout === 2} onClick={() => props.onChange(2)} />
<Item icon={layoutThreeIcon} active={props.layout === 3} onClick={() => props.onChange(3)} />
<Item icon={layoutFourIcon} active={props.layout === 4} onClick={() => props.onChange(4)} />
</div>
)
}
export default LayoutSwitch

View File

@@ -12,7 +12,7 @@ function Layout() {
style={{ backgroundColor: followArcTheme ? 'var(--arc-palette-foregroundPrimary)' : themeColor }} style={{ backgroundColor: followArcTheme ? 'var(--arc-palette-foregroundPrimary)' : themeColor }}
> >
<Sidebar /> <Sidebar />
<div className="p-[15px] h-full overflow-hidden"> <div className="px-[15px] py-3 h-full overflow-hidden">
<Outlet /> <Outlet />
</div> </div>
</main> </main>

View File

@@ -0,0 +1,33 @@
import { Link } from '@tanstack/react-router'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Button from './Button'
import Dialog from './Dialog'
interface Props {
open: boolean
setOpen: (open: boolean) => void
}
const PremiumFeatureModal: FC<Props> = (props) => {
const { t } = useTranslation()
return (
<Dialog
title={`🔒 ${t('Premium Feature')}`}
open={props.open}
onClose={() => props.setOpen(false)}
className="rounded-2xl w-[500px]"
>
<div className="flex flex-col items-center gap-4 py-5">
<p className="font-semibold text-primary-text text-center w-[70%]">
{t('Upgrade to premium to chat with more than two bots at once')}
</p>
<Link to="/premium" onClick={() => props.setOpen(false)} className="focus-visible:outline-none">
<Button color="primary" text={t('Upgrade')} />
</Link>
</div>
</Dialog>
)
}
export default PremiumFeatureModal

View File

@@ -3,6 +3,8 @@ import cx from 'classnames'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { BsLayoutSplit, BsLayoutThreeColumns } from 'react-icons/bs'
import { RxViewGrid } from 'react-icons/rx'
import allInOneIcon from '~/assets/all-in-one.svg' import allInOneIcon from '~/assets/all-in-one.svg'
import collapseIcon from '~/assets/icons/collapse.svg' import collapseIcon from '~/assets/icons/collapse.svg'
import feedbackIcon from '~/assets/icons/feedback.svg' import feedbackIcon from '~/assets/icons/feedback.svg'

View File

@@ -5,7 +5,7 @@ import { BotId } from '~app/bots'
import { useEnabledBots } from '~app/hooks/use-enabled-bots' import { useEnabledBots } from '~app/hooks/use-enabled-bots'
interface Props { interface Props {
excludeBotId: BotId selectedBotId: BotId
onChange: (botId: BotId) => void onChange: (botId: BotId) => void
} }
@@ -35,7 +35,7 @@ const SwitchBotDropdown: FC<Props> = (props) => {
> >
<Menu.Items className="absolute left-0 z-10 mt-2 rounded-md bg-secondary shadow-lg focus:outline-none"> <Menu.Items className="absolute left-0 z-10 mt-2 rounded-md bg-secondary shadow-lg focus:outline-none">
{enabledBots.map(({ botId, bot }) => { {enabledBots.map(({ botId, bot }) => {
if (botId === props.excludeBotId) { if (botId === props.selectedBotId) {
return null return null
} }
return ( return (

View File

@@ -79,6 +79,8 @@ const resources = {
Premium: '付费会员', Premium: '付费会员',
Chatbots: '聊天机器人', Chatbots: '聊天机器人',
'Manage order and devices': '管理订单与设备', 'Manage order and devices': '管理订单与设备',
'Upgrade to premium to chat with more than two bots at once': '升级会员,同时和两个以上的机器人聊天',
Upgrade: '升级',
}, },
}, },
de: { de: {

View File

@@ -1,24 +1,50 @@
import cx from 'classnames' import cx from 'classnames'
import { useAtomValue } from 'jotai' import { useAtomValue, useSetAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { uniqBy } from 'lodash-es' import { uniqBy } from 'lodash-es'
import { FC, Suspense, useCallback, useMemo } from 'react' import { FC, Suspense, useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '~app/components/Button' import Button from '~app/components/Button'
import ChatMessageInput from '~app/components/Chat/ChatMessageInput' import ChatMessageInput from '~app/components/Chat/ChatMessageInput'
import LayoutSwitch from '~app/components/Chat/LayoutSwitch'
import PremiumFeatureModal from '~app/components/PremiumFeatureModal'
import { useChat } from '~app/hooks/use-chat' import { useChat } from '~app/hooks/use-chat'
import { useUserConfig } from '~app/hooks/use-user-config' import { usePremium } from '~app/hooks/use-premium'
import { trackEvent } from '~app/plausible' import { trackEvent } from '~app/plausible'
import { multiPanelBotsAtom } from '~app/state' import { getAllInOneLayout, setAllInOneLayout } from '~services/storage/all-in-one-layout'
import { MultiPanelLayout } from '~services/user-config'
import { BotId } from '../bots' import { BotId } from '../bots'
import ConversationPanel from '../components/Chat/ConversationPanel' import ConversationPanel from '../components/Chat/ConversationPanel'
const GeneralChatPanel: FC<{ chats: ReturnType<typeof useChat>[] }> = ({ chats }) => { type OnLayoutChange = (layout: number) => void
const twoPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:2', ['chatgpt', 'bing'])
const threePanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:3', ['chatgpt', 'bing', 'bard'])
const fourPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots:4', ['chatgpt', 'bing', 'claude', 'bard'])
const GeneralChatPanel: FC<{
chats: ReturnType<typeof useChat>[]
botsAtom: typeof twoPanelBotsAtom
onLayoutChange: OnLayoutChange
}> = ({ chats, botsAtom, onLayoutChange }) => {
const { t } = useTranslation() const { t } = useTranslation()
const generating = useMemo(() => chats.some((c) => c.generating), [chats]) const generating = useMemo(() => chats.some((c) => c.generating), [chats])
const setBots = useSetAtom(botsAtom)
const [premiumModalOpen, setPremiumModalOpen] = useState(false)
const premiumState = usePremium()
const disabled = useMemo(() => !premiumState.isLoading && !premiumState.activated, [premiumState])
useEffect(() => {
if (disabled && chats.length > 2) {
setPremiumModalOpen(true)
}
}, [chats.length, disabled])
const onUserSendMessage = useCallback( const onUserSendMessage = useCallback(
(input: string, botId?: BotId) => { (input: string, botId?: BotId) => {
if (disabled && chats.length > 2) {
setPremiumModalOpen(true)
return
}
if (botId) { if (botId) {
const chat = chats.find((c) => c.botId === botId) const chat = chats.find((c) => c.botId === botId)
chat?.sendMessage(input) chat?.sendMessage(input)
@@ -27,7 +53,19 @@ const GeneralChatPanel: FC<{ chats: ReturnType<typeof useChat>[] }> = ({ chats }
} }
trackEvent('send_messages', { count: chats.length }) trackEvent('send_messages', { count: chats.length })
}, },
[chats], [chats, disabled],
)
const onSwitchBot = useCallback(
(botId: BotId, index: number) => {
trackEvent('switch_bot', { botId, panel: chats.length })
setBots((bots) => {
const newBots = [...bots]
newBots[index] = botId
return newBots
})
},
[chats.length, setBots],
) )
return ( return (
@@ -49,58 +87,68 @@ const GeneralChatPanel: FC<{ chats: ReturnType<typeof useChat>[] }> = ({ chats }
stopGenerating={chat.stopGenerating} stopGenerating={chat.stopGenerating}
mode="compact" mode="compact"
resetConversation={chat.resetConversation} resetConversation={chat.resetConversation}
index={index} onSwitchBot={(botId) => onSwitchBot(botId, index)}
/> />
))} ))}
</div> </div>
<ChatMessageInput <div className="flex flex-row gap-3">
mode="full" <LayoutSwitch layout={chats.length} onChange={onLayoutChange} />
className="rounded-[20px] bg-primary-background px-4 py-2" <ChatMessageInput
disabled={generating} mode="full"
onSubmit={onUserSendMessage} className="rounded-[15px] bg-primary-background px-4 py-2 grow"
actionButton={!generating && <Button text={t('Send')} color="primary" type="submit" />} disabled={generating}
autoFocus={true} onSubmit={onUserSendMessage}
/> actionButton={!generating && <Button text={t('Send')} color="primary" type="submit" />}
autoFocus={true}
/>
</div>
<PremiumFeatureModal open={premiumModalOpen} setOpen={setPremiumModalOpen} />
</div> </div>
) )
} }
const TwoBotChatPanel: FC = () => { const TwoBotChatPanel: FC<{ onLayoutChange: OnLayoutChange }> = (props) => {
const multiPanelBotIds = useAtomValue(multiPanelBotsAtom) const multiPanelBotIds = useAtomValue(twoPanelBotsAtom)
const chat1 = useChat(multiPanelBotIds[0]) const chat1 = useChat(multiPanelBotIds[0])
const chat2 = useChat(multiPanelBotIds[1]) const chat2 = useChat(multiPanelBotIds[1])
const chats = useMemo(() => [chat1, chat2], [chat1, chat2]) const chats = useMemo(() => [chat1, chat2], [chat1, chat2])
return <GeneralChatPanel chats={chats} /> return <GeneralChatPanel chats={chats} botsAtom={twoPanelBotsAtom} onLayoutChange={props.onLayoutChange} />
} }
const ThreeBotChatPanel: FC = () => { const ThreeBotChatPanel: FC<{ onLayoutChange: OnLayoutChange }> = (props) => {
const multiPanelBotIds = useAtomValue(multiPanelBotsAtom) const multiPanelBotIds = useAtomValue(threePanelBotsAtom)
const chat1 = useChat(multiPanelBotIds[0]) const chat1 = useChat(multiPanelBotIds[0])
const chat2 = useChat(multiPanelBotIds[1]) const chat2 = useChat(multiPanelBotIds[1])
const chat3 = useChat(multiPanelBotIds[2]) const chat3 = useChat(multiPanelBotIds[2])
const chats = useMemo(() => [chat1, chat2, chat3], [chat1, chat2, chat3]) const chats = useMemo(() => [chat1, chat2, chat3], [chat1, chat2, chat3])
return <GeneralChatPanel chats={chats} /> return <GeneralChatPanel chats={chats} botsAtom={threePanelBotsAtom} onLayoutChange={props.onLayoutChange} />
} }
const FourBotChatPanel: FC = () => { const FourBotChatPanel: FC<{ onLayoutChange: OnLayoutChange }> = (props) => {
const multiPanelBotIds = useAtomValue(multiPanelBotsAtom) const multiPanelBotIds = useAtomValue(fourPanelBotsAtom)
const chat1 = useChat(multiPanelBotIds[0]) const chat1 = useChat(multiPanelBotIds[0])
const chat2 = useChat(multiPanelBotIds[1]) const chat2 = useChat(multiPanelBotIds[1])
const chat3 = useChat(multiPanelBotIds[2]) const chat3 = useChat(multiPanelBotIds[2])
const chat4 = useChat(multiPanelBotIds[3]) const chat4 = useChat(multiPanelBotIds[3])
const chats = useMemo(() => [chat1, chat2, chat3, chat4], [chat1, chat2, chat3, chat4]) const chats = useMemo(() => [chat1, chat2, chat3, chat4], [chat1, chat2, chat3, chat4])
return <GeneralChatPanel chats={chats} /> return <GeneralChatPanel chats={chats} botsAtom={fourPanelBotsAtom} onLayoutChange={props.onLayoutChange} />
} }
const MultiBotChatPanel: FC = () => { const MultiBotChatPanel: FC = () => {
const { multiPanelLayout } = useUserConfig() const [layout, setLayout] = useState(() => getAllInOneLayout())
if (multiPanelLayout === MultiPanelLayout.Four) {
return <FourBotChatPanel /> const onLayoutChange = useCallback((layout: number) => {
setLayout(layout)
setAllInOneLayout(layout)
}, [])
if (layout === 4) {
return <FourBotChatPanel onLayoutChange={onLayoutChange} />
} }
if (multiPanelLayout === MultiPanelLayout.Three) { if (layout === 3) {
return <ThreeBotChatPanel /> return <ThreeBotChatPanel onLayoutChange={onLayoutChange} />
} }
return <TwoBotChatPanel /> return <TwoBotChatPanel onLayoutChange={onLayoutChange} />
} }
const MultiBotChatPanelPage: FC = () => { const MultiBotChatPanelPage: FC = () => {

View File

@@ -44,7 +44,7 @@ function SidePanelPage() {
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<img src={botInfo.avatar} className="w-4 h-4 object-contain rounded-full" /> <img src={botInfo.avatar} className="w-4 h-4 object-contain rounded-full" />
<span className="font-semibold text-primary-text text-xs">{botInfo.name}</span> <span className="font-semibold text-primary-text text-xs">{botInfo.name}</span>
<SwitchBotDropdown excludeBotId={botId} onChange={setBotId} /> <SwitchBotDropdown selectedBotId={botId} onChange={setBotId} />
</div> </div>
<div className="flex flex-row items-center gap-3"> <div className="flex flex-row items-center gap-3">
<img <img

View File

@@ -2,9 +2,9 @@ import { createHashHistory, ReactRouter, RootRoute, Route, useParams } from '@ta
import { BotId } from './bots' import { BotId } from './bots'
import Layout from './components/Layout' import Layout from './components/Layout'
import MultiBotChatPanel from './pages/MultiBotChatPanel' import MultiBotChatPanel from './pages/MultiBotChatPanel'
import PremiumPage from './pages/PremiumPage'
import SettingPage from './pages/SettingPage' import SettingPage from './pages/SettingPage'
import SingleBotChatPanel from './pages/SingleBotChatPanel' import SingleBotChatPanel from './pages/SingleBotChatPanel'
import PremiumPage from './pages/PremiumPage'
const rootRoute = new RootRoute() const rootRoute = new RootRoute()

View File

@@ -22,8 +22,7 @@ export const chatFamily = atomFamily(
(a, b) => a.botId === b.botId && a.page === b.page, (a, b) => a.botId === b.botId && a.page === b.page,
) )
export const multiPanelBotsAtom = atomWithStorage<BotId[]>('multiPanelBots', ['chatgpt', 'bing', 'claude', 'bard']) export const licenseKeyAtom = atomWithStorage('licenseKey', '', undefined, { unstable_getOnInit: true })
export const licenseKeyAtom = atomWithStorage('licenseKey', '')
export const sidebarCollapsedAtom = atomWithStorage('sidebarCollapsed', false) export const sidebarCollapsedAtom = atomWithStorage('sidebarCollapsed', false)
export const themeColorAtom = atomWithStorage('themeColor', getDefaultThemeColor()) export const themeColorAtom = atomWithStorage('themeColor', getDefaultThemeColor())
export const followArcThemeAtom = atomWithStorage('followArcTheme', false) export const followArcThemeAtom = atomWithStorage('followArcTheme', false)

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="7" width="22" height="18" rx="3" stroke="#BDBDBD" stroke-width="2"/>
<line x1="16" y1="7" x2="16" y2="25" stroke="#BDBDBD" stroke-width="2"/>
<line x1="27" y1="16" x2="5" y2="16" stroke="#BDBDBD" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="7" width="22" height="18" rx="3" stroke="#BDBDBD" stroke-width="2"/>
<line x1="12" y1="7" x2="12" y2="25" stroke="#BDBDBD" stroke-width="2"/>
<line x1="20" y1="6" x2="20" y2="24" stroke="#BDBDBD" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="7" width="22" height="18" rx="3" stroke="#BDBDBD" stroke-width="2"/>
<line x1="16" y1="7" x2="16" y2="25" stroke="#BDBDBD" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

View File

@@ -0,0 +1,10 @@
function getAllInOneLayout() {
const v = localStorage.getItem('allInOneLayout') || ''
return parseInt(v) || 2
}
function setAllInOneLayout(v: number) {
localStorage.setItem('allInOneLayout', v.toString())
}
export { getAllInOneLayout, setAllInOneLayout }

View File

@@ -2092,7 +2092,7 @@
"@tanstack/react-router@^0.0.1-beta.83": "@tanstack/react-router@^0.0.1-beta.83":
version "0.0.1-beta.83" version "0.0.1-beta.83"
resolved "https://registry.yarnpkg.com/@tanstack/react-router/-/react-router-0.0.1-beta.83.tgz#280fdcc77e755743d7709d1c823e044a3a7c4bc4" resolved "https://registry.npmmirror.com/@tanstack/react-router/-/react-router-0.0.1-beta.83.tgz#280fdcc77e755743d7709d1c823e044a3a7c4bc4"
integrity sha512-FcezDPKxXu7uP8tjTQpKBXkweiql053O7D3hRBg1kWuU3LyouHFGFALLr2xvWkue4WWSpa1mBzrINldYygDKLw== integrity sha512-FcezDPKxXu7uP8tjTQpKBXkweiql053O7D3hRBg1kWuU3LyouHFGFALLr2xvWkue4WWSpa1mBzrINldYygDKLw==
dependencies: dependencies:
"@babel/runtime" "^7.16.7" "@babel/runtime" "^7.16.7"
@@ -4265,10 +4265,10 @@ jotai-immer@^0.2.0:
resolved "https://registry.yarnpkg.com/jotai-immer/-/jotai-immer-0.2.0.tgz#9bfa1a5a7911c5b222d22a8388a801c05bc06c65" resolved "https://registry.yarnpkg.com/jotai-immer/-/jotai-immer-0.2.0.tgz#9bfa1a5a7911c5b222d22a8388a801c05bc06c65"
integrity sha512-hahK8EPiROS9RoNWmX/Z8rY9WkAijspX4BZ1O7umpcwI4kPNkbcCpu/PhiQ8FMcpEcF6KmbpbMpSSj/GFmo8NA== integrity sha512-hahK8EPiROS9RoNWmX/Z8rY9WkAijspX4BZ1O7umpcwI4kPNkbcCpu/PhiQ8FMcpEcF6KmbpbMpSSj/GFmo8NA==
jotai@^2.1.0: jotai@^2.2.1:
version "2.1.0" version "2.2.1"
resolved "https://registry.yarnpkg.com/jotai/-/jotai-2.1.0.tgz#b1a9525345518453802e4a64d99e2800598bab76" resolved "https://registry.npmmirror.com/jotai/-/jotai-2.2.1.tgz#0a95b88c5f3ea4fd656b5f79af6f84e895f84f5a"
integrity sha512-fR82PtHAmEQrc/daMEYGc4EteW96/b6wodtDSCzLvoJA/6y4YG70er4hh2f8CYwYjqwQ0eZUModGfG4DmwkTyQ== integrity sha512-Gz4tpbRQy9OiFgBwF9F7TieDn0UTE3C0IFSDuxHjOIvgn2tACH30UKz6p/wIlfoZROXSTCIxEvYEa7Y25WM+8g==
js-base64@^3.7.5: js-base64@^3.7.5:
version "3.7.5" version "3.7.5"
@@ -5881,6 +5881,11 @@ react-viewport-list@^7.1.1:
resolved "https://registry.yarnpkg.com/react-viewport-list/-/react-viewport-list-7.1.1.tgz#2f36ad1db8124e8a960bc006b95228b696cc81d6" resolved "https://registry.yarnpkg.com/react-viewport-list/-/react-viewport-list-7.1.1.tgz#2f36ad1db8124e8a960bc006b95228b696cc81d6"
integrity sha512-O3gxykg3DgpcyYH+/X2kFwWFaZjckJ0FK3UBb4vFhZe+CsGLcX8OVJb0VSYI+IupEuQ9pl8dvJak8JRkIuvNjw== integrity sha512-O3gxykg3DgpcyYH+/X2kFwWFaZjckJ0FK3UBb4vFhZe+CsGLcX8OVJb0VSYI+IupEuQ9pl8dvJak8JRkIuvNjw==
react-wrap-balancer@^1.0.0:
version "1.0.0"
resolved "https://registry.npmmirror.com/react-wrap-balancer/-/react-wrap-balancer-1.0.0.tgz#f45f8af7cf68d9d54d16a8cf271047b75d1431d7"
integrity sha512-yjDH+I8WGyDfh95gKhX/6ckfSBAltwQkxiYxtLPlyIRQNUVSjvz1uHR6Hpy+zHyOkJQw6GEC5RPglA41QXvzyw==
react@^18.2.0: react@^18.2.0:
version "18.2.0" version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"