Fixes all Nav Menu related errors and bugs (#331)

* chore(client): update lucide-react package to version 0.220.0
style(client): change color of MessageHeader component text to gray-500
style(client): change color of nav-close-button to gray-400 and nav-open-button to gray-500
feat(client): add Panel component to replace svg icons in Nav component

* fix: forwardRef errors in Nav Menu

* refactor(SearchBar.jsx): change clearSearch prop destructuring to props destructuring
refactor(SearchBar.jsx): add ref prop to SearchBar component
refactor(getIcon.jsx): remove unused imports
refactor(getIcon.jsx): add nullish coalescing operator to user.name and user.avatar properties

* fix (NavLinks): modals no longer close on nav menu close

* style(ExportModel.jsx): remove unnecessary z-index property from a div element

* style(ExportModel.jsx): remove trailing whitespace in input element

* refactor(Message.jsx): remove unused cancelled variable
fix(Message.jsx): fix error message length exceeding 512 characters
refactor(MenuItem.jsx): remove unused MenuItem component
This commit is contained in:
Danny Avila 2023-05-19 16:02:41 -04:00 committed by GitHub
parent ee2b3e4fb2
commit ec561fcd7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 230 additions and 165 deletions

View file

@ -53,7 +53,7 @@
"filenamify": "^6.0.0",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"lucide-react": "^0.113.0",
"lucide-react": "^0.220.0",
"pino": "^8.12.1",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import copy from 'copy-to-clipboard';
import SubRow from './Content/SubRow';
@ -31,7 +31,7 @@ export default function Message({
siblingCount,
setSiblingIdx
}) {
const { text, searchResult, isCreatedByUser, error, submitting, unfinished, cancelled } = message;
const { text, searchResult, isCreatedByUser, error, submitting, unfinished } = message;
const isSubmitting = useRecoilValue(store.isSubmitting);
const setLatestMessage = useSetRecoilState(store.latestMessage);
const [abortScroll, setAbort] = useState(false);
@ -74,6 +74,7 @@ export default function Message({
};
const getError = (text) => {
const errorMessage = text.length > 512 ? text.slice(0, 512) + '...' : text;
const match = text.match(/\{[^{}]*\}/);
var json = match ? match[0] : '';
if (isJson(json)) {
@ -83,10 +84,10 @@ export default function Message({
} else if (json.type === 'insufficient_quota') {
return "We're sorry, but the default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the top-left corner of the textbox.";
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${text}`;
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
}
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${text}`;
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${errorMessage}`;
}
};

View file

@ -45,7 +45,7 @@ const MessageHeader = ({ isSearchView = false }) => {
<>
<div
className={cn(
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-600 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100 dark:text-gray-500',
'dark:text-gray-450 w-full gap-1 border-b border-black/10 bg-gray-50 text-sm text-gray-500 transition-all hover:bg-gray-100 hover:bg-opacity-30 dark:border-gray-900/50 dark:bg-gray-700 dark:hover:bg-gray-600 dark:hover:bg-opacity-100 dark:text-gray-500',
endpoint === 'chatGPTBrowser' ? '' : 'cursor-pointer '
)}
onClick={() => (endpoint === 'chatGPTBrowser' ? null : setSaveAsDialogShow(true))}

View file

@ -1,11 +1,10 @@
import { useEffect } from 'react';
import store from '~/store';
import TrashIcon from '../svg/TrashIcon';
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
import { Dialog } from '../ui/Dialog.tsx';
import DialogTemplate from '../ui/DialogTemplate';
import { useClearConversationsMutation } from '~/data-provider';
export default function ClearConvos() {
const ClearConvos = ({ open, onOpenChange}) => {
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const clearConvosMutation = useClearConversationsMutation();
@ -23,13 +22,7 @@ export default function ClearConvos() {
}, [clearConvosMutation.isSuccess]);
return (
<Dialog>
<DialogTrigger asChild>
<button className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700">
<TrashIcon />
Clear conversations
</button>
</DialogTrigger>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTemplate
title="Clear conversations"
description="Are you sure you want to clear all conversations? This is irreversible."
@ -41,4 +34,6 @@ export default function ClearConvos() {
/>
</Dialog>
);
}
};
export default ClearConvos;

View file

@ -1,9 +1,9 @@
import React, { useState, useContext } from 'react';
import { forwardRef, useContext } from 'react';
import DarkModeIcon from '../svg/DarkModeIcon';
import LightModeIcon from '../svg/LightModeIcon';
import { ThemeContext } from '~/hooks/ThemeContext';
export default function DarkMode() {
const DarkMode = forwardRef(() => {
const { theme, setTheme } = useContext(ThemeContext);
const clickHandler = () => setTheme(theme === 'dark' ? 'light' : 'dark');
@ -18,4 +18,6 @@ export default function DarkMode() {
{mode}
</button>
);
}
});
export default DarkMode;

View file

@ -1,10 +1,10 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue, useRecoilCallback } from 'recoil';
import filenamify from 'filenamify';
import exportFromJSON from 'export-from-json';
import download from 'downloadjs';
import DialogTemplate from '~/components/ui/DialogTemplate.jsx';
import { Dialog, DialogClose, DialogButton } from '~/components/ui/Dialog.tsx';
import { Dialog, DialogButton } from '~/components/ui/Dialog.tsx';
import { Input } from '~/components/ui/Input.tsx';
import { Label } from '~/components/ui/Label.tsx';
import { Checkbox } from '~/components/ui/Checkbox.tsx';

View file

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import { useState, forwardRef } from 'react';
import { useRecoilValue } from 'recoil';
import { Download } from 'lucide-react';
import { cn } from '~/utils/';
@ -7,7 +7,7 @@ import ExportModel from './ExportModel';
import store from '~/store';
export default function ExportConversation() {
const ExportConversation = forwardRef(() => {
const [open, setOpen] = useState(false);
const conversation = useRecoilValue(store.conversation) || {};
@ -37,4 +37,6 @@ export default function ExportConversation() {
<ExportModel open={open} onOpenChange={setOpen} />
</>
);
}
});
export default ExportConversation;

View file

@ -1,8 +1,8 @@
import React from 'react';
import { forwardRef } from 'react';
import LogOutIcon from '../svg/LogOutIcon';
import { useAuthContext } from '~/hooks/AuthContext';
export default function Logout() {
const Logout = forwardRef(() => {
const { user, logout } = useAuthContext();
const handleLogout = () => {
@ -20,4 +20,6 @@ export default function Logout() {
<small>Log out</small>
</button>
);
}
});
export default Logout;

View file

@ -1,20 +1,25 @@
import React from 'react';
import { forwardRef } from 'react';
import { cn } from '~/utils/';
export default function NavLink({ svg, text, clickHandler }) {
const props = {
className:
'flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10'
};
const NavLink = forwardRef((props, ref) => {
const { svg, text, clickHandler, className = '' } = props;
const defaultProps = {};
defaultProps.className = cn(
'flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10',
className
);
if (clickHandler) {
props.onClick = clickHandler;
console.log('clickHandler: ', clickHandler);
defaultProps.onClick = clickHandler;
}
return (
<a {...props}>
<a {...defaultProps} ref={ref}>
{svg()}
{text}
</a>
);
}
});
export default NavLink;

View file

@ -1,70 +1,111 @@
import { Menu, Transition } from '@headlessui/react';
import { Fragment, useEffect, useRef, useState } from 'react';
import { Fragment, useState } from 'react';
import { useRecoilValue } from 'recoil';
import SearchBar from './SearchBar';
import TrashIcon from '../svg/TrashIcon';
import { Download } from 'lucide-react';
import NavLink from './NavLink';
import ExportModel from './ExportConversation/ExportModel';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
import Logout from './Logout';
import ExportConversation from './ExportConversation';
import { useAuthContext } from '~/hooks/AuthContext';
import { cn } from '~/utils/';
import DotsIcon from '../svg/DotsIcon';
import store from '~/store';
export default function NavLinks({ clearSearch, isSearchEnabled }) {
const { user, logout } = useAuthContext();
const [showExports, setShowExports] = useState(false);
const [showClearConvos, setShowClearConvos] = useState(false);
const { user } = useAuthContext();
const conversation = useRecoilValue(store.conversation) || {};
const exportable =
conversation?.conversationId &&
conversation?.conversationId !== 'new' &&
conversation?.conversationId !== 'search';
const clickHandler = () => {
if (exportable) setShowExports(true);
};
return (
<Menu as="div" className="group relative">
{({ open }) => (
<>
<Menu.Button
className={cn(
'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800',
open ? 'bg-gray-800' : ''
)}
>
<div className="-ml-0.5 h-5 w-5 flex-shrink-0">
<div className="relative flex">
<img
className="rounded-sm"
src={
user?.avatar || `https://avatars.dicebear.com/api/initials/${user?.name}.svg`
}
alt=""
/>
<>
<Menu as="div" className="group relative">
{({ open }) => (
<>
<Menu.Button
className={cn(
'group-ui-open:bg-gray-800 flex w-full items-center gap-2.5 rounded-md px-3 py-3 text-sm transition-colors duration-200 hover:bg-gray-800',
open ? 'bg-gray-800' : ''
)}
>
<div className="-ml-0.5 h-5 w-5 flex-shrink-0">
<div className="relative flex">
<img
className="rounded-sm"
src={
user?.avatar || `https://avatars.dicebear.com/api/initials/${user?.name}.svg`
}
alt=""
/>
</div>
</div>
</div>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-white">
{user?.name || 'USER'}
</div>
<DotsIcon />
</Menu.Button>
<div className="grow overflow-hidden text-ellipsis whitespace-nowrap text-left text-white">
{user?.name || 'USER'}
</div>
<DotsIcon />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute bottom-full left-0 z-20 mb-2 w-full translate-y-0 overflow-hidden rounded-md bg-[#050509] py-1.5 opacity-100 outline-none">
<Menu.Item>
{({}) => <>{!!isSearchEnabled && <SearchBar clearSearch={clearSearch} />}</>}
</Menu.Item>
<Menu.Item>{({}) => <ExportConversation />}</Menu.Item>
<div className="my-1.5 h-px bg-white/20" role="none"></div>
<Menu.Item>{({}) => <DarkMode />}</Menu.Item>
<Menu.Item>{({}) => <ClearConvos />}</Menu.Item>
<div className="my-1.5 h-px bg-white/20" role="none"></div>
<Menu.Item>
<Logout />
</Menu.Item>
</Menu.Items>
</Transition>
</>
)}
</Menu>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute bottom-full left-0 z-20 mb-2 w-full translate-y-0 overflow-hidden rounded-md bg-[#050509] py-1.5 opacity-100 outline-none">
<Menu.Item>
{!!isSearchEnabled && <SearchBar clearSearch={clearSearch} />}
</Menu.Item>
<Menu.Item>
<NavLink
className={cn(
'flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700',
exportable ? 'cursor-pointer text-white' : 'cursor-not-allowed text-gray-400'
)}
svg={() => <Download size={16} />}
text="Export conversation"
clickHandler={clickHandler}
/>
</Menu.Item>
<div className="my-1.5 h-px bg-white/20" role="none" />
<Menu.Item>
<DarkMode />
</Menu.Item>
<Menu.Item>
<NavLink
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
svg={() => <TrashIcon />}
text="Clear conversations"
clickHandler={() => setShowClearConvos(true)}
/>
</Menu.Item>
<div className="my-1.5 h-px bg-white/20" role="none" />
<Menu.Item>
<Logout />
</Menu.Item>
</Menu.Items>
</Transition>
</>
)}
</Menu>
{showExports && <ExportModel open={showExports} onOpenChange={setShowExports} />}
{showClearConvos && <ClearConvos open={showClearConvos} onOpenChange={setShowClearConvos} />}
</>
);
}

View file

@ -1,8 +1,10 @@
import { forwardRef } from 'react';
import { Search } from 'lucide-react';
import { useRecoilState } from 'recoil';
import store from '~/store';
export default function SearchBar({ clearSearch }) {
const SearchBar = forwardRef((props, ref) => {
const { clearSearch } = props;
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
const handleKeyUp = (e) => {
@ -19,7 +21,10 @@ export default function SearchBar({ clearSearch }) {
};
return (
<div className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700">
<div
ref={ref}
className="flex w-full cursor-pointer items-center gap-3 px-3 py-3 text-sm text-white transition-colors duration-200 hover:bg-gray-700"
>
{<Search className="h-4 w-4" />}
<input
type="text"
@ -34,4 +39,6 @@ export default function SearchBar({ clearSearch }) {
/>
</div>
);
}
});
export default SearchBar;

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useContext } from 'react';
import NewChat from './NewChat';
import Panel from '../svg/Panel';
import Spinner from '../svg/Spinner';
import Pages from '../Conversations/Pages';
import Conversations from '../Conversations';
@ -198,62 +199,21 @@ export default function Nav({ navVisible, setNavVisible }) {
</div>
<button
type="button"
className={cn('nav-close-button -ml-0.5 -mt-2.5 inline-flex h-10 w-10 items-center justify-center rounded-md focus:outline-none focus:ring-white md:-ml-1 md:-mt-2.5', theme === 'dark' ? 'text-gray-500 hover:text-gray-400' : 'text-gray-900 hover:text-gray-600')}
className={cn('nav-close-button -ml-0.5 -mt-2.5 inline-flex h-10 w-10 items-center justify-center rounded-md focus:outline-none focus:ring-white md:-ml-1 md:-mt-2.5', theme === 'dark' ? 'text-gray-500 hover:text-gray-400' : 'text-gray-400 hover:text-gray-500')}
onClick={toggleNavVisible}
>
<span className="sr-only">Close sidebar</span>
<svg
stroke="currentColor"
fill="none"
strokeWidth="1.5"
viewBox="0 0 24 24"
strokeLinecap="round"
strokeLinejoin="round"
className="block h-6 w-6 md:hidden"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<line x1="3" y1="6" x2="15" y2="18" />
<line x1="3" y1="18" x2="15" y2="6" />
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="hidden h-[26px] w-[26px] md:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75"
/>
</svg>
<Panel/>
</button>
</div>
{!navVisible && (
<button
type="button"
className="nav-open-button fixed left-2 top-0.5 z-10 inline-flex h-10 w-10 items-center justify-center rounded-md text-gray-900 hover:text-gray-600 focus:outline-none focus:ring-white dark:text-gray-500 dark:hover:text-gray-400"
className="nav-open-button fixed left-2 top-0.5 z-10 inline-flex h-10 w-10 items-center justify-center rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-white dark:text-gray-500 dark:hover:text-gray-400"
onClick={toggleNavVisible}
>
<span className="sr-only">Open sidebar</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="h-6 w-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15m3 0l3-3m0 0l-3-3m3 3H9"
/>
</svg>
<Panel open={true}/>
</button>
)}

View file

@ -0,0 +1,47 @@
import { cn } from '~/utils/';
export default function Panel({ open = false, className }) {
const openPanel = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={cn('lucide lucide-panel-right-close', className)}
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<line x1="15" x2="15" y1="3" y2="21" />
<path d="m8 9 3 3-3 3" />
</svg>
);
const closePanel = (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={cn('lucide lucide-panel-left-close', className)}
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<path d="M9 3v18" />
<path d="m16 15-3-3 3-3" />
</svg>
);
if (open) {
return openPanel;
} else {
return closePanel;
}
}

View file

@ -1,5 +1,4 @@
import React from 'react';
import { forwardRef } from 'react';
import {
DialogClose,
DialogContent,
@ -10,21 +9,22 @@ import {
} from './Dialog.tsx';
import { cn } from '~/utils/';
export default function DialogTemplate({
title,
description,
main,
buttons,
leftButtons,
selection,
className
}) {
const DialogTemplate = forwardRef((props, ref) => {
const {
title,
description,
main,
buttons,
leftButtons,
selection,
className
} = props;
const { selectHandler, selectClasses, selectText } = selection || {};
const defaultSelect =
'bg-gray-900 text-white transition-colors hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-gray-200 dark:focus:ring-gray-400 dark:focus:ring-offset-gray-900';
return (
<DialogContent className={cn('shadow-2xl dark:bg-gray-800', className || '')}>
<DialogContent ref={ref} className={cn('shadow-2xl dark:bg-gray-800', className || '')}>
<DialogHeader>
<DialogTitle className="text-gray-800 dark:text-white">{title}</DialogTitle>
<DialogDescription className="text-gray-600 dark:text-gray-300">
@ -51,4 +51,6 @@ export default function DialogTemplate({
</DialogFooter>
</DialogContent>
);
}
});
export default DialogTemplate;

View file

@ -1,6 +1,3 @@
import { clsx } from 'clsx';
import React from 'react';
import { twMerge } from 'tailwind-merge';
import GPTIcon from '../components/svg/GPTIcon';
import BingIcon from '../components/svg/BingIcon';
import { useAuthContext } from '~/hooks/AuthContext';
@ -8,12 +5,13 @@ import { useAuthContext } from '~/hooks/AuthContext';
const getIcon = (props) => {
// { size = 30, isCreatedByUser, model, chatGptLabel, error, ...props }
const { size = 30, isCreatedByUser, button, model } = props;
const { user, logout } = useAuthContext();
// eslint-disable-next-line react-hooks/rules-of-hooks
const { user } = useAuthContext();
if (isCreatedByUser)
return (
<div
title={user.name}
title={user?.name || 'User'}
style={{
width: size,
height: size
@ -24,7 +22,7 @@ const getIcon = (props) => {
className="rounded-sm"
src={
user?.avatar ||
`https://api.dicebear.com/6.x/initials/svg?seed=${user?.name}&fontFamily=Verdana&fontSize=36`
`https://api.dicebear.com/6.x/initials/svg?seed=${user?.name || 'User'}&fontFamily=Verdana&fontSize=36`
}
alt="avatar"
/>

13
package-lock.json generated
View file

@ -114,7 +114,7 @@
"filenamify": "^6.0.0",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"lucide-react": "^0.113.0",
"lucide-react": "^0.220.0",
"pino": "^8.12.1",
"rc-input-number": "^7.4.2",
"react": "^18.2.0",
@ -12104,8 +12104,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.113.0",
"license": "ISC",
"version": "0.220.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.220.0.tgz",
"integrity": "sha512-bYtGUsLAWBvZu+BzAU/ziP1gzE4LwMEXLnlgSr1yUKEPPalLG77JLd5GdYebOVkpm+GtqRqnp6tEKDX7Bm8ZlQ==",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
}
@ -22692,7 +22693,7 @@
"filenamify": "^6.0.0",
"html2canvas": "^1.4.1",
"lodash": "^4.17.21",
"lucide-react": "^0.113.0",
"lucide-react": "0.220.0",
"path": "^0.12.7",
"pino": "^8.12.1",
"postcss": "^8.4.21",
@ -26275,7 +26276,9 @@
}
},
"lucide-react": {
"version": "0.113.0",
"version": "0.220.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.220.0.tgz",
"integrity": "sha512-bYtGUsLAWBvZu+BzAU/ziP1gzE4LwMEXLnlgSr1yUKEPPalLG77JLd5GdYebOVkpm+GtqRqnp6tEKDX7Bm8ZlQ==",
"requires": {}
},
"magic-string": {